go语言中有一些被人误用或忽视的细节。

这里只列出了一些一两句话能表达清楚的问题。对于需要对问题展开才能理解清楚的会在后续文章中给出。

本文内容不包含reflect及unsafe内容。

  1. 几种创建变量别名的情形:
  • 对一个变量取地址操作
  • 当copy一个引用类型(如slice,map,channel,function,pointer)
  • 当copy一个包含引用类型的array,struct,或interface时

go语言的垃圾回收器需要找到每一个变量的访问者,也就是必须知道一个变量的所有别名。

  1. 在考虑性能优化的时,需要记住逃逸的变量需要在堆上额外分配内存(以及分配时间更长)

  2. 在赋值之前,赋值语句右边的所有表达式将会先进行求值。

  3. 某些类型转换会改变值的数据表示:

  • 某些数字类型之间互相转换
  • string类型转换成[]byte会allocate一个string的copy
  1. 每个文件都可以包含任意数量的init function,并按照声明的顺序初始化。

  2. if, for, switch 控制流语句,含有两个词法域:一个是由显式的花括号组成,更外一层是隐式的。包含循环的初始化,条件测试,循环后的迭代。

  3. switch或select的分支也有独立的语法块

  4. rune类型和int32类型等价,通常表示一个unicode码点

  5. byte类型和uint8类型等价,通常表示数值的原始字节表示,而很少用于表示一个整数。

  6. 为什么内置len函数返回一个有符号int?

    通常我们可能会认为uint无符号类型表示len更合理。事实上用有符号的int,我们可以用来处理逆循环。

1
2
3
4
ints := []int{1, 2, 3} 
for i := len(ints) - 1; i >= 0; i-- {     
    fmt.Println(ints[i]) // 3, 2, 1 
}

​ 如果len函数返回是一个无符号类型,那么迭代三次之后i==0时, i—语句将不会产生-1,而是变成一个超大的正整数,ints[i]发生panic

  1. 无符号数往往只有在位运算或其它特殊的运算场景才会使用,例如 bit集合、 分析二进制文件格式、或者哈希和加密操作。并不是单纯的表达非负的场合。

  2. 通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大。

1
2
var f float32 = 16777216 // 1<< 24
fmt.Println(f == f+1)    // "true"
  1. 浮点数的比较测试需要小心精度问题。

  2. 字符串的不可变性体现在:

  • 当用字符串变量赋值或者使用+进行追加时,会重新分配新的字符串。
  • 只能访问,不能用索引对其内部修改。
  • copy字符串是低廉的,因为会共享底层数据。同理字符串的切片操作也会共享底层数据。
  1. 字符串的索引操作s[i]返回的是字节类型值

  2. 字符串的切片操作s[i:j]返回的是字符串类型值。

  3. range循环在处理字符串的时候,会自动隐式解码UTF8字符串成rune。需要注意的是对于非ASCII,索引更新的步长将超过1个字节。

  4. 常量是一种表达式,它的求值发生在编译期,而不是在运行期。对常量间进行的运算的结果也是常量。

  5. 编译器为无类型常量提供更高精度的算术运算;你可以认为至少有256bit的运算精度。

  6. %取模运算符的符号和被取模数的符号总是一致的,因此 -5%3 和 -5%-3 结果都是-2。

  7. 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是不同数组类型。

  8. struct声明时field顺序对类型标识符也有重要影响。顺序不同对应了不同的struct类型。

  9. 类型S的struct的filed不可以是S类型。但可以是*S指针类型来创建递归的数据结构。

  10. struct{} 和[0]int类型的size为0。

  11. “.“操作符应用于struct类型变量或者struct值(例如函数返回值)访问field时,如果”.“前面的是变量,那么”.“表示变量,如果前面的是值,.操作符表示一个值。

  12. nil slice 没有底层数组,length和capacity都是0。

  13. slice切片操作s[i:j],0 ≤ i≤ j≤ cap(s),j可以超过len(s)。

  14. nil值的slice和空slice行为一致。把nil slice 当成空slice是安全的行为。go函数应当以相同的方式对待nil slice 和长度为0的slice。

  15. 大部分map操作对于nil map和空map行为一致:如查找,删除,len,range。但当往nil map 中分配一个元素将会导致panic。

  16. map的元素不是一个变量,不能进行取地址操作。原因是:map增长可能会rehashing,把存在的元素分配到一个新的空间。可能会导致悬挂指针。

  17. map是间接引用,所以元素value值可以是其自身

  18. 大部分函数使用固定大小的的函数调用栈,而go语言使用可变栈,栈的大小按需增加。

  19. 函数的零值是nil,调用你一个nil 函数会引发panic

  20. 当循环中有闭包环境时候(如匿名函数,go 语句),要注意捕获迭代量可能产生的问题。

  21. 在一个函数中执行多条defer语句时,他们的执行顺序和声明顺序相反。

  22. defer 函数在return更新返回值后立即执行。之后函数再返回给调用者。如果外层函数返回值有名字,defer匿名函数可以捕获函数的返回值,并对返回值进行修改。

  23. 如果defer函数是nil时,在defer语句执行时不会panic而是在函数真正执行时panic。

  24. 循环体中的defer可能会有耗尽资源的风险。

  25. go的panic机制使得defer函数调用在释放栈之前。注意函数栈的顺序,如果panic发生在defer之前,defer不会生效。

  26. 当main function返回时,所有的goroutine会被直接终止,然后程序退出。

  27. copy 一个channel时或用于参数传递时,只是copy了一个reference。他们指向同一个数据结构。

  28. 对一个channel进行close操作,并不是进行垃圾回收。当一个channel 不会再访问时,会进行垃圾回收。无论它是否已经关闭。

  29. 发送方close一个channel是单方面的标记不会再发送任何值。通常只有发送方才会close,如果对对一个receive-only channel关闭会发生panic。发送方也无需关闭每一个channel。只有需要广播接收方时才需要。对一个已经close的channel,再次发送或者再次close会引发panic。

  30. 当一个channel close之后,接收方不会阻塞,(如果是带buffer的channel,接收完channel最后一个元素之后),并且会收到一个零值。接收方也可以通过显式的接受bool值,或者用range loop 来判断channel的发送是否结束。

  31. 从具体类型(包括指针)值赋到接口类型值产生隐式类型转换。并且接口值的动态值部分获得了一份具体类型值的拷贝。如果是*T则获得一份指针的拷贝(接口的细节需要在后续文章中展开讲)。