Golang 语言使用的注意事项

  1. 左大括号 { 一般不能单独放一行

    Golang的分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句

  2. 未使用的变量无法通过编译

    如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它。

  3. 未使用的import无法通过编译

    如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

  4. 简短声明的变量只能在函数内部使用

    :=只能在局部

  5. 不能使用简短声明来重复声明变量

  6. 不能使用简短声明来设置字段的值

  7. 不小心覆盖了变量

    func main() {
    	x := 1
    	println(x)		// 1
    	{
    		println(x)	// 1
    		x := 2
    		println(x)	// 2	// 新的 x 变量的作用域只在代码块内部
    	}
    	println(x)		// 1
    }
    

    可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用。

  8. 显式类型的变量无法使用 nil 来初始化

    nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。

    // 错误示例
    func main() {
        var x = nil	// error: use of untyped nil
    	_ = x
    }
    
    
    // 正确示例
    func main() {
    	var x interface{} = nil
    	_ = x
    }
    
  9. 直接使用值为 nil 的 slice、map

    允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic

  10. 在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小

  11. string 类型的变量值不能为 nil,字符串类型的零值是空串 “”

  12. Array 类型的值作为函数参数
    在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的。如果想修改参数数组,直接传递指向这个数组的指针类型。直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)。

  13. range 遍历 slice 和 array 时混淆了返回值。

    与其他编程语言中的 for-in 、foreach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值。

    // 错误示例
    func main() {
    	x := []string{"a", "b", "c"}
    	for v := range x {
    		fmt.Println(v)	// 1 2 3
    	}
    }
    
    
    // 正确示例
    func main() {
    	x := []string{"a", "b", "c"}
    	for _, v := range x {	// 使用 _ 丢弃索引
    		fmt.Println(v)
    	}
    }
    
  14. range 遍历 slice 和 array 时混淆了返回值。

  15. 访问 map 中不存在的 key。

    Go 则会返回元素对应数据类型的零值,比如 nil、’’ 、false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

    // 错误的 key 检测方式
    func main() {
    	x := map[string]string{"one": "2", "two": "", "three": "3"}
    	if v := x["two"]; v == "" {
    		fmt.Println("key two is no entry")	// 键 two 存不存在都会返回的空字符串
    	}
    }
    
    // 正确示例
    func main() {
    	x := map[string]string{"one": "2", "two": "", "three": "3"}
    	if _, ok := x["two"]; !ok {
    		fmt.Println("key two is no entry")
    	}
    }
    
  16. string 类型的值是常量,不可更改。

    尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可。

    // 修改字符串的错误示例
    func main() {
    	x := "text"
    	x[0] = "T"		// error: cannot assign to x[0]
    	fmt.Println(x)
    }
    
    
    // 修改示例
    func main() {
    	x := "text"
    	xBytes := []byte(x)
    	xBytes[0] = 'T'	// 注意此时的 T 是 rune 类型
    	x = string(xBytes)
    	fmt.Println(x)	// Text
    }
    
  17. string 与 byte slice 之间的转换

    当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。

  18. string 与索引操作符

    对字符串用索引访问返回的不是字符,而是一个 byte 值。

  19. 字符串并不都是 UTF8 文本

    string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。

  20. 字符串的长度

    Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

  21. 在多行 array、slice、map 语句中缺少 , 号

  22. log.Fatal 和 log.Panic 不只是 log

  23. 对内建数据结构的操作并不是同步的

    尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

    goroutine 和 channel 是进行原子操作的好方法,或使用 “sync” 包中的锁。

  24. range 迭代 string 得到的值

    range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。

    注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

    for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。

    func main() {
    	data := "A\xfe\x02\xff\x04"
    	for _, v := range data {
    		fmt.Printf("%#x ", v)	// 0x41 0xfffd 0x2 0xfffd 0x4	// 错误
    	}
    
    	for _, v := range []byte(data) {
    		fmt.Printf("%#x ", v)	// 0x41 0xfe 0x2 0xff 0x4	// 正确
    	}
    }
    
  25. range 迭代 map

    如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

    Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的迭代结果也是可能的。

  26. switch 中的 fallthrough 语句

    switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

  27. 自增和自减运算

    很多编程语言都自带前置后置的 ++、-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++、— 只作为运算符而非表达式。

  28. 按位取反

    很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反。

    同时 ^ 也是按位异或(XOR)操作符。

    一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02,与二元的 XOR 操作 0x22 XOR 0xff 是一致的。

    Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

  29. 运算符的优先级

    除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

    Precedence    Operator
        5             *  /  %  <<  >>  &  &^
        4             +  -  |  ^
        3             ==  !=  <  <=  >  >=
        2             &&
        1             ||
    
  30. 不导出的 struct 字段无法被 encode

    以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值。

    func main() {
    	in := MyData{1, "two"}
    	fmt.Printf("%#v\n", in)	// main.MyData{One:1, two:"two"}
    
    	encoded, _ := json.Marshal(in)
    	fmt.Println(string(encoded))	// {"One":1}	// 私有字段 two 被忽略了
    
    	var out MyData
    	json.Unmarshal(encoded, &out)
    	fmt.Printf("%#v\n", out) 	// main.MyData{One:1, two:""}
    }
    
  31. 程序退出时还有 goroutine 在执行

    常用解决办法:使用 “WaitGroup” 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

    如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:

    // 等待所有 goroutine 执行完毕
    // 进入死锁
    func main() {
    	var wg sync.WaitGroup
    	done := make(chan struct{})
    
    	workerCount := 2
    	for i := 0; i < workerCount; i++ {
    		wg.Add(1)
    		go doIt(i, done, wg)
    	}
    
    	close(done)
    	wg.Wait()
    	fmt.Println("all done!")
    }
    
    func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
    	fmt.Printf("[%v] is running\n", workerID)
    	defer wg.Done()
    	<-done
    	fmt.Printf("[%v] is done\n", workerID)
    }
    

    执行结果:
    fatal error: all goroutines are asleep - deadlock!

    为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

    原因是 goroutine 得到的 “WaitGroup” 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。

    // 等待所有 goroutine 执行完毕
    // 使用传址方式为 WaitGroup 变量传参
    // 使用 channel 关闭 goroutine
    
    func main() {
    	var wg sync.WaitGroup
    	done := make(chan struct{})
    	ch := make(chan interface{})
    
    	workerCount := 2
    	for i := 0; i < workerCount; i++ {
    		wg.Add(1)
            go doIt(i, ch, done, &wg)	// wg 传指针,doIt() 内部会改变 wg 的值
    	}
    
    	for i := 0; i < workerCount; i++ {	// 向 ch 中发送数据,关闭 goroutine
    		ch <- i
    	}
    
    	close(done)
    	wg.Wait()
    	close(ch)
    	fmt.Println("all done!")
    }
    
    func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
    	fmt.Printf("[%v] is running\n", workerID)
    	defer wg.Done()
    	for {
    		select {
    		case m := <-ch:
    			fmt.Printf("[%v] m => %v\n", workerID, m)
    		case <-done:
    			fmt.Printf("[%v] is done\n", workerID)
    			return
    		}
    	}
    }
    
  32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

    只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

    func main() {
    	ch := make(chan string)
    
    	go func() {
    		for m := range ch {
    			fmt.Println("Processed:", m)
    			time.Sleep(1 * time.Second)	// 模拟需要长时间运行的操作
    		}
    	}()
    
    	ch <- "cmd.1"
    	ch <- "cmd.2" // 不会被接收处理
    }
    
  33. 向已关闭的 channel 发送数据会造成 panic

  34. 在一个值为 nil 的 channel 上发送和接收数据将永久阻塞

  35. 若函数 receiver 传参是传值方式,则无法修改参数的原有值

    方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

    除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值。

  36. 关闭 HTTP 的响应体

    使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置。应该先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体。

    resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

    如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

    _, err = io.Copy(ioutil.Discard, resp.Body)	// 手动丢弃读取完毕的数据
    

    如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

    json.NewDecoder(resp.Body).Decode(&data)
    

    如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

  37. 关闭 HTTP 连接

    一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 “net/http” 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:

    • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
    • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。

    根据需求选择使用场景:

    • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
    • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。
  38. 将 JSON 中的数字解码为 interface 类型

    在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:

    func main() {
    	var data = []byte(`{"status": 200}`)
    	var result map[string]interface{}
    
    	if err := json.Unmarshal(data, &result); err != nil {
    		log.Fatalln(err)
    	}
    
    	fmt.Printf("%T\n", result["status"])	// float64
    	var status = result["status"].(int)	// 类型断言错误
    	fmt.Println("Status value: ", status)
    }
    

    如果你尝试 decode 的 JSON 字段是整型,你可以:

    • 将 int 值转为 float 统一使用

    • 将 decode 后需要的 float 值转为 int 使用

    // 将 decode 的值转为 int 使用
    func main() {
        var data = []byte(`{"status": 200}`)
        var result map[string]interface{}
    
        if err := json.Unmarshal(data, &result); err != nil {
            log.Fatalln(err)
        }
    
        var status = uint64(result["status"].(float64))
        fmt.Println("Status value: ", status)
    }
    

    使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型:

    // 指定字段类型
    func main() {
    	var data = []byte(`{"status": 200}`)
    	var result map[string]interface{}
        
    	var decoder = json.NewDecoder(bytes.NewReader(data))
    	decoder.UseNumber()
    
    	if err := decoder.Decode(&result); err != nil {
    		log.Fatalln(err)
    	}
    
    	var status, _ = result["status"].(json.Number).Int64()
    	fmt.Println("Status value: ", status)
    }
    
     // 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
     // 将数据转为 decode 为 string
     func main() {
     	var data = []byte({"status": 200})
      	var result map[string]interface{}
      	var decoder = json.NewDecoder(bytes.NewReader(data))
      	decoder.UseNumber()
      	if err := decoder.Decode(&result); err != nil {
      		log.Fatalln(err)
      	}
        var status uint64
      	err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
    	checkError(err)
       	fmt.Println("Status value: ", status)
    }
    

    使用 struct 类型将你需要的数据映射为数值型:

    // struct 中指定字段类型`在这里插入代码片`
    func main() {
      	var data = []byte(`{"status": 200}`)
      	var result struct {
      		Status uint64 `json:"status"`
      	}
    
      	err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
      	checkError(err)
    	fmt.Printf("Result: %+v", result)
    }
    

    可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型

    适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:

    // 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
    func main() {
    	records := [][]byte{
    		[]byte(`{"status":200, "tag":"one"}`),
    		[]byte(`{"status":"ok", "tag":"two"}`),
    	}
    
    	for idx, record := range records {
    		var result struct {
    			StatusCode uint64
    			StatusName string
    			Status     json.RawMessage `json:"status"`
    			Tag        string          `json:"tag"`
    		}
    
    		err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
    		checkError(err)
    
    		var name string
    		err = json.Unmarshal(result.Status, &name)
    		if err == nil {
    			result.StatusName = name
    		}
    
    		var code uint64
    		err = json.Unmarshal(result.Status, &code)
    		if err == nil {
    			result.StatusCode = code
    		}
    
    		fmt.Printf("[%v] result => %+v\n", idx, result)
    	}
    }
    
  39. struct、array、slice 和 map 的值比较

    可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

    type data struct {
    	num     int
    	fp      float32
    	complex complex64
    	str     string
    	char    rune
    	yes     bool
    	events  <-chan string
    	handler interface{}
    	ref     *byte
    	raw     [10]byte
    }
    
    func main() {
    	v1 := data{}
    	v2 := data{}
    	fmt.Println("v1 == v2: ", v1 == v2)	// true
    }
    

    如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

    type data struct {
    	num    int
    	checks [10]func() bool		// 无法比较
    	doIt   func() bool		// 无法比较
    	m      map[string]string	// 无法比较
    	bytes  []byte			// 无法比较
    }
    
    func main() {
    	v1 := data{}
    	v2 := data{}
    
    	fmt.Println("v1 == v2: ", v1 == v2)
    }
    
    invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)
    

    Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 “reflect” 包的 DeepEqual()

    // 比较相等运算符无法比较的元素
    func main() {
    	v1 := data{}
    	v2 := data{}
    	fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))	// true
    
    	m1 := map[string]string{"one": "a", "two": "b"}
    	m2 := map[string]string{"two": "b", "one": "a"}
    	fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2))	// true
    
    	s1 := []int{1, 2, 3}
    	s2 := []int{1, 2, 3}
       	// 注意两个 slice 相等,值和顺序必须一致
    	fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2))	// true
    }
    

    这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法

    func main() {
    	var b1 []byte = nil
    	b2 := []byte{}
    	fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2))	// false
    }
    
发布了248 篇原创文章 · 获赞 13 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105076363