第50章:Go细节101



Go细节101

索引:

一个包可以在一个源文件里被引入多次。

一个Go源文件可以多次引入同一个包。但是每次的引入名称必须不同。这些相同的包引入引用着同一个包实例。

示例:

  1. package main
  2. import "fmt"
  3. import "io"
  4. import inout "io"
  5. func main() {
  6. fmt.Println(&inout.EOF == &io.EOF) // true
  7. }

switchselect流程控制代码块中,default分支可以放在所有的case分支之前或者所有的case分支之后,也可以放在case分支之间。

示例:

  1. switch n := rand.Intn(3); n {
  2. case 0: fmt.Println("n == 0")
  3. case 1: fmt.Println("n == 1")
  4. default: fmt.Println("n == 2")
  5. }
  6. switch n := rand.Intn(3); n {
  7. default: fmt.Println("n == 2")
  8. case 0: fmt.Println("n == 0")
  9. case 1: fmt.Println("n == 1")
  10. }
  11. switch n := rand.Intn(3); n {
  12. case 0: fmt.Println("n == 0")
  13. default: fmt.Println("n == 2")
  14. case 1: fmt.Println("n == 1")
  15. }
  16. var x, y chan int
  17. select {
  18. case <-x:
  19. case y <- 1:
  20. default:
  21. }
  22. select {
  23. case <-x:
  24. default:
  25. case y <- 1:
  26. }
  27. select {
  28. default:
  29. case <-x:
  30. case y <- 1:
  31. }

switch流程控制代码块中的数字常量case表达式不能重复,但是布尔常量case表达式可以重复。

例如,下面的代码在编译时会失败。

  1. package main
  2. func main() {
  3. switch 123 {
  4. case 123:
  5. case 123: // error: duplicate case
  6. }
  7. }

但是下面的代码在编译时是没问题的。

  1. package main
  2. func main() {
  3. switch false {
  4. case false:
  5. case false:
  6. }
  7. }

关于原因,请阅读这个issue。 此行为依赖于编译器。事实上,标准编译器同样不允许重复的字符串case表达式,但是gccgo编译器却允许。

switch流程控制代码块里的switch表达式总是被估值为类型确定值。

例如,在下列switch代码块中的switch表达式123被视为一个int值,而不是一个类型不确定的整数。

  1. package main
  2. func main() {
  3. switch 123 {
  4. case int64(123): // error: 类型不匹配
  5. case uint32(789): // error: 类型不匹配
  6. }
  7. }

switch流程控制代码块中的switch表达式的缺省默认值为类型确定值true(其类型为预声明类型bool)。

例如,下列程序会打印出true

  1. package main
  2. import "fmt"
  3. func main() {
  4. switch { // <=> switch true {
  5. case true: fmt.Println("true")
  6. case false: fmt.Println("false")
  7. }
  8. }

有时候,显式代码块的开括号{可以放在下一行。

例如:

  1. package main
  2. func main() {
  3. var i = 0
  4. Outer:
  5. for
  6. { // 在这里断行是没问题的
  7. switch
  8. { // 在这里断行是没问题的
  9. case i == 5:
  10. break Outer
  11. default:
  12. i++
  13. }
  14. }
  15. }

下面程序的结果会打印什么?true还是false? 答案是true。 关于原因请阅读Go中的代码断行规则(第28章)一文。

  1. package main
  2. import "fmt"
  3. func False() bool {
  4. return false
  5. }
  6. func main() {
  7. switch False()
  8. {
  9. case true: fmt.Println("true")
  10. case false: fmt.Println("false")
  11. }
  12. }

有些case分支代码块必须是显式的。

例如,下面的程序会在编译时将失败。

  1. func demo(n, m int) (r int) {
  2. switch n {
  3. case 123:
  4. if m > 0 {
  5. goto End
  6. }
  7. r++
  8. End: // syntax error: 标签后缺少语句
  9. default:
  10. r = 1
  11. }
  12. return
  13. }

为了编译通过,case分支代码块必须改成显式的:

  1. func demo(n, m int) (r int) {
  2. switch n {
  3. case 123: {
  4. if m > 0 {
  5. goto End
  6. }
  7. r++
  8. End:
  9. }
  10. default:
  11. r = 1
  12. }
  13. return
  14. }

另外,我们可以在标签End:之后加一个分号,如下所示:

  1. func demo(n, m int) (r int) {
  2. switch n {
  3. case 123:
  4. if m > 0 {
  5. goto End
  6. }
  7. r++
  8. End:;
  9. default:
  10. r = 1
  11. }
  12. return
  13. }

关于原因,请阅读Go的代码断行规则(第28章)一文。

嵌套的延迟函数调用可以修改外层函数的返回结果。

例如:

  1. package main
  2. import "fmt"
  3. func F() (r int) {
  4. defer func() {
  5. r = 789
  6. }()
  7. return 123 // <=> r = 123; return
  8. }
  9. func main() {
  10. fmt.Println(F()) // 789
  11. }

某些recover函数调用是空操作。

我们需要在正确的地方调用recover函数。 关于细节,请阅读 在正确的位置调用内置函数recover(第31章)一文。

我们可以使用os.Exit函数调用退出一个程序和使用runtime.Goexit函数调用退出一个协程。

我们可以通过调用os.Exit函数从任何函数里退出一个程序。 os.Exit函数调用接受一个int代码值做为参数并将此代码返回给操作系统。

示例:

  1. // exit-example.go
  2. package main
  3. import "os"
  4. import "time"
  5. func main() {
  6. go func() {
  7. time.Sleep(time.Second)
  8. os.Exit(1)
  9. }()
  10. select{}
  11. }

运行:

  1. $ go run a.go
  2. exit status 1
  3. $ echo $?
  4. 1

我们可以通过调用runtime.Goexit函数退出一个goroutine。 runtime.Goexit函数没有参数。

在下面的示例中,文字Java将不会被打印出来。

  1. package main
  2. import "fmt"
  3. import "runtime"
  4. func main() {
  5. c := make(chan int)
  6. go func() {
  7. defer func() {c <- 1}()
  8. defer fmt.Println("Go")
  9. func() {
  10. defer fmt.Println("C")
  11. runtime.Goexit()
  12. }()
  13. fmt.Println("Java")
  14. }()
  15. <-c
  16. }

递增运算符++和递减运算符--的优先级低于解引用运算符*和取地址运算符&,解引用运算符和取地址运算符的优先级低于选择器.中的属性选择操作符。

例如:

  1. package main
  2. import "fmt"
  3. type T struct {
  4. x int
  5. y *int
  6. }
  7. func main() {
  8. var t T
  9. p := &t.x // <=> p := &(t.x)
  10. fmt.Printf("%T\n", p) // *int
  11. *p++ // <=> (*p)++
  12. *p-- // <=> (*p)--
  13. t.y = p
  14. a := *t.y // <=> *(t.y)
  15. fmt.Printf("%T\n", a) // int
  16. }

移位运算中的左类型不确定操作数的类型推断规则取决于右操作数是否是常量。

  1. package main
  2. func main() {
  3. }
  4. const M = 2
  5. var _ = 1.0 << M // 编译没问题。1.0将被推断为一个int值。
  6. var N = 2
  7. var _ = 1.0 << N // 编译失败。1.0将被推断为一个float64值。

关于原因请阅读运算操作符(第8章)一文。

如果两个指针的类型具有不同的底层类型但是它们的基类型却共享相同的底层类型,则这两个指针值可以间接相互转换为对方的类型。

例如:

  1. package main
  2. type MyInt int64
  3. type Ta *int64
  4. type Tb *MyInt
  5. func main() {
  6. var a Ta
  7. var b Tb
  8. //a = Ta(b) // error: 直接转换是不允许的。
  9. // 但是间接转换是允许的。
  10. y := (*MyInt)(b)
  11. x := (*int64)(y)
  12. a = x // 等价于下一行
  13. a = (*int64)(y) // 等价于下一行
  14. a = (*int64)((*MyInt)(b))
  15. _ = a
  16. }

两个零尺寸值的地址可能相等,也可能不相等。

两个零尺寸值的地址是否相等时依赖于具体编译器实现以及具体编译器版本。

  1. package main
  2. import "fmt"
  3. func main() {
  4. a := struct{}{}
  5. b := struct{}{}
  6. x := struct{}{}
  7. y := struct{}{}
  8. m := [10]struct{}{}
  9. n := [10]struct{}{}
  10. o := [10]struct{}{}
  11. p := [10]struct{}{}
  12. fmt.Println(&x, &y, &o, &p)
  13. // 对于标准编译器1.22版本,x、y、o和p将
  14. // 逃逸到堆上,但是a、b、m和n则开辟在栈上。
  15. fmt.Println(&a == &b) // false
  16. fmt.Println(&x == &y) // true
  17. fmt.Println(&a == &x) // false
  18. fmt.Println(&m == &n) // false
  19. fmt.Println(&o == &p) // true
  20. fmt.Println(&n == &p) // false
  21. }

上面代码中所示的输出是针对标准编译器1.22版本的。

一个指针类型的基类型可以为此指针类型自身。

例如:

  1. package main
  2. func main() {
  3. type P *P
  4. var p P
  5. p = &p
  6. p = **************p
  7. }

类似的,

  • 一个切片类型的元素类型可以是此切片类型自身,
  • 一个映射类型的元素类型可以是此映射类型自身,
  • 一个通道类型的元素类型可以是此通道类型自身,
  • 一个函数类型的输入参数和返回结果值类型可以是此函数类型自身。
  1. package main
  2. func main() {
  3. type S []S
  4. type M map[string]M
  5. type C chan C
  6. type F func(F) F
  7. s := S{0:nil}
  8. s[0] = s
  9. m := M{"Go": nil}
  10. m["Go"] = m
  11. c := make(C, 3)
  12. c <- c; c <- c; c <- c
  13. var f F
  14. f = func(F)F {return f}
  15. _ = s[0][0][0][0][0][0][0][0]
  16. _ = m["Go"]["Go"]["Go"]["Go"]
  17. <-<-<-c
  18. f(f(f(f(f))))
  19. }

有关选择器缩写形式的细节。

无论一个指针值的类型是具名的还是无名的,如果它的(指针)类型的基类型为一个结构体类型,则我们可以使用此指针值来选择它所引用着的结构体中的字段。 但是,如果此指针的类型为一个具名类型,则我们不能使用此指针值来选择它所引用着的结构体中的方法。

我们总是不能使用二级以上指针来选择结构体字段和方法。

  1. package main
  2. type T struct {
  3. x int
  4. }
  5. func (T) m(){} // T有一个方法m。
  6. type P *T // P为一个具名一级指针类型。
  7. type PP *P // PP为一个具名二级指针类型。
  8. func main() {
  9. var t T
  10. var tp = &t
  11. var tpp = &tp
  12. var p P = tp
  13. var pp PP = &p
  14. tp.x = 12 // 没问题
  15. p.x = 34 // 没问题
  16. pp.x = 56 // error: 类型PP没有名为x的字段或者方法。
  17. tpp.x = 78 // error: 类型**T没有名为x的字段或者方法。
  18. tp.m() // 没问题,因为类型*T也有一个m方法。
  19. p.m() // error: 类型P没有名为m的字段或者方法。
  20. pp.m() // error: 类型PP没有名为m的字段或者方法。
  21. tpp.m() // error: 类型**T没有名为m的字段或者方法。
  22. }

有时候,嵌套组合字面量可以被简化。

关于细节,请阅读内嵌组合字面量可以被简化(第18章)这一章节。

在某些情形下,我们可以将数组指针当作数组来用。

关于细节,请阅读把数组指针当做数组来使用(第18章)这一章节。

从nil映射中读取元素不会导致崩溃,读取结果是一个零元素值。

例如,函数Foo1Foo2是等价的,但是函数Foo2比函数Foo1简洁得多。

  1. func Foo1(m map[string]int) int {
  2. if m != nil {
  3. return m["foo"]
  4. }
  5. return 0
  6. }
  7. func Foo2(m map[string]int) int {
  8. return m["foo"]
  9. }

从一个nil映射中删除一个条目不会导致崩溃,这是一个空操作。

例如,下面这个程序不会因为恐慌而崩溃。

  1. package main
  2. func main() {
  3. var m map[string]int // nil
  4. delete(m, "foo")
  5. }

append函数调用的结果可能会与原始切片共享一些元素,也可能不共享任何元素。

关于细节,请阅读添加和删除容器元素(第18章)这一章节。

从一个基础切片派生出的子切片的长度可能大于基础切片的长度。

例如:

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := make([]int, 3, 9)
  5. fmt.Println(len(s)) // 3
  6. s2 := s[2:7]
  7. fmt.Println(len(s2)) // 5
  8. }

关于细节,请阅读从数组或者切片派生切片(第18章)这一章节。

从一个nil切片中派生子切片是允许的,只要子切片表达式中使用的所有索引都为零,则不会有恐慌产生,结果子切片同样是一个nil切片。

例如,下面的程序在运行时刻不会产生恐慌。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var x []int // nil
  5. a := x[:]
  6. b := x[0:0]
  7. c := x[:0:0]
  8. // 下一行将打印出三个true。
  9. fmt.Println(a == nil, b == nil, c == nil)
  10. }

关于细节,请阅读从数组或者切片派生切片(第18章)这一章节。

range遍历nil映射或者nil切片是没问题的,这属于空操作。

例如,下面的程序可以编译是没问题的。

  1. package main
  2. func main() {
  3. var s []int // nil
  4. for range s {
  5. }
  6. var m map[string]int // nil
  7. for range m {
  8. }
  9. }

range遍历nil数组指针时,如果忽略或省略第二个迭代变量,则此遍历是没问题的。遍历中的循环步数为相应数组类型的长度。

例如,下面的程序会输出01234

  1. package main
  2. import "fmt"
  3. func main() {
  4. var a *[5]int // nil
  5. for i, _ := range a {
  6. fmt.Print(i)
  7. }
  8. }

切片的长度和容量可以被单独修改。

我们可以通过反射途径单独修改一个切片的长度或者容量。 关于细节,请阅读单独修改一个切片的长度或者容量(第18章)这一章节。

切片和数组组合字面量中的索引必须是非负常量。

例如,下面的程序将编译失败。

  1. var k = 1
  2. var x = [2]int{k: 1} // error: 索引必须为一个常量
  3. var y = []int{k: 1} // error: 索引必须为一个常量

注意,映射组合字面量中的键值不必为常量。

切片/数组/映射组合字面量的常量索引和键值不能重复。

例如,下面的程序将编译失败。

  1. // error: 重复的索引:1
  2. var a = []bool{0: false, 1: true, 1: true}
  3. // error: 重复的索引:0
  4. var b = [...]string{0: "foo", 1: "bar", 0: "foo"}
  5. // error: 重复的键值:"foo"
  6. var c = map[string]int{"foo": 1, "foo": 2}

这个特性可以用于在编译时刻断言某些条件(第52章)。

不可寻址的数组的元素依旧是不可寻址的,但是不可寻址的切片的元素总是可寻址的。

原因是一个数组值的元素和此数组存储在同一个内存块中。 但是切片的情况大不相同(第51章)。

一个例子:

  1. package main
  2. func main() {
  3. // 组合字面量是不可寻址的。
  4. /* 取容器元素的地址。 */
  5. // 取不可寻址的切片的元素的地址是没问题的
  6. _ = &[]int{1}[0]
  7. // error: 不能取不可寻址的数组的元素的地址
  8. _ = &[5]int{}[0]
  9. /* 修改元素值。 */
  10. // 修改不可寻址的切片的元素是没问题的
  11. []int{1,2,3}[1] = 9
  12. // error: 不能修改不可寻址的数组的元素
  13. [3]int{1,2,3}[1] = 9
  14. }

可以从不可寻址的切片派生子切片,但是不能从不可寻址的数组派生子切片。

原因和上一个细节是一样的。

例如:

  1. package main
  2. func main() {
  3. // 映射元素是不可寻址的。
  4. // 下面几行编译没问题。
  5. _ = []int{6, 7, 8, 9}[1:3]
  6. var ms = map[string][]int{"abc": {0, 1, 2, 3}}
  7. _ = ms["abc"][1:3]
  8. // 下面几行将编译失败,因为不可从不可寻址的数组派生切片。
  9. /*
  10. _ = [...]int{6, 7, 8, 9}[1:3] // error
  11. var ma = map[string][4]int{"abc": {0, 1, 2, 3}}
  12. _ = ma["abc"][1:3] // error
  13. */
  14. }

把以NaN做为键值的条目放如映射就宛如把条目放入黑洞一样。

原因是下面的另一个细节中提到的NaN != NaN。 但是,在Go1.12之前,以NaN作为键值的元素只能在for-range循环中被找到; 从Go1.12开始,以NaN作为键值的元素也可以通过类似fmt.Print的函数打印出来。

  1. package main
  2. import "fmt"
  3. import "math"
  4. func main() {
  5. var a = math.NaN()
  6. fmt.Println(a) // NaN
  7. var m = map[float64]int{}
  8. m[a] = 123
  9. v, present := m[a]
  10. fmt.Println(v, present) // 0 false
  11. m[a] = 789
  12. v, present = m[a]
  13. fmt.Println(v, present) // 0 false
  14. fmt.Println(m) // map[NaN:789 NaN:123]
  15. delete(m, a) // no-op
  16. fmt.Println(m) // map[NaN:789 NaN:123]
  17. for k, v := range m {
  18. fmt.Println(k, v)
  19. }
  20. // the above loop outputs:
  21. // NaN 123
  22. // NaN 789
  23. }

注意:在Go1.12之前,两个fmt.Println(m)调用均打印出map[NaN:<nil> NaN:<nil>]

字符串转换为byte切片或rune切片后的结果切片的容量可能会大于长度。

我们不应该假设结果切片的长度和容量总是相等的。

在下面的例子中,如果最后一个fmt.Println行被删除,在其前面的两行会打印相同的值5;否则,一个打印5,一个打印8(对于标准编译器1.22版本来说)。

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := "abcde"
  5. x := []byte(s) // len(s) == 1
  6. fmt.Println(cap([]byte(s))) // 32
  7. fmt.Println(cap(x)) // 8
  8. fmt.Println(x)
  9. }

如果我们假设结果切片的长度和容量总是相等,就可能写出一些有bug的代码

对于切片s,循环for i = range s {...}并不等价于循环for i = 0; i < len(s); i++ {...}

对于这两个循环,迭代变量i的最终值可能是不同的。

  1. package main
  2. import "fmt"
  3. var i int
  4. func fa(s []int, n int) int {
  5. i = n
  6. for i = 0; i < len(s); i++ {}
  7. return i
  8. }
  9. func fb(s []int, n int) int {
  10. i = n
  11. for i = range s {}
  12. return i
  13. }
  14. func main() {
  15. s := []int{2, 3, 5, 7, 11, 13}
  16. fmt.Println(fa(s, -1), fb(s, -1)) // 6 5
  17. s = nil
  18. fmt.Println(fa(s, -1), fb(s, -1)) // 0 -1
  19. }

一个映射中的条目的遍历次序在两次遍历中可能并不相同。我们可以认为映射中的条目的遍历次序是随机的。

比如下面这个例子不会无穷尽地循环下去(注意每次退出前的循环次数可能不同):

  1. package main
  2. import "fmt"
  3. func f(m map[byte]byte) string {
  4. bs := make([]byte, 0, 2*len(m))
  5. for k, v := range m {
  6. bs = append(bs, k, v)
  7. }
  8. return string(bs)
  9. }
  10. func main() {
  11. m := map[byte]byte{'a':'A', 'b':'B', 'c':'C'}
  12. s0 := f(m)
  13. for i := 1; ; i++{
  14. if s := f(m); s != s0 {
  15. fmt.Println(s0)
  16. fmt.Println(s)
  17. fmt.Println(i)
  18. return
  19. }
  20. }
  21. }

注意:对映射进行JSON格式化输出中的映射条目是按照它们的键值排序的。 另外,从Go 1.12开始,使用fmt标准库包中的打印函数打印映射时,输出的映射条目也是按照它们的键值排序的; 而在Go 1.12之前,这些打印输出时乱序的。

在对一个映射进行条目遍历期间,在此映射中创建的新条目可能会在当前遍历中被遍历出来,也可能不会。

有例为证:

  1. package main
  2. import "fmt"
  3. func main() {
  4. m := map[int]int{0: 0, 1: 100, 2: 200}
  5. r, n, i:= len(m), len(m), 0
  6. for range m {
  7. m[n] = n*100
  8. n++
  9. i++
  10. }
  11. fmt.Printf("新增了%d个条目,其中%d个被遍历出来,%d个没有。\n",
  12. i, i - r, n - i,
  13. )
  14. }

感谢Valentin Deleplace提出了上面两条细节建议

一个多返回值函数调用表达式不能和其它表达式混用在一个赋值语句的右侧或者另一个函数调用的实参列表中。

关于细节,请阅读有返回值的函数的调用是一种表达式(第20章)这一章节。

某些函数调用是在编译时刻被估值的。

关于细节,请阅读哪些函数调用将在编译时刻被估值?(第46章)这一总结。

每一个方法都对应着一个隐式声明的函数。

关于细节,请阅读每个方法对应着一个隐式声明的函数(第22章)这一章节。

如果两个接口值具有相同的动态类型并且此动态类型不支持比较,则比较这两个接口值将导致一个恐慌。

例如:

  1. package main
  2. func main() {
  3. var x interface{} = []int{}
  4. _ = x == x // panic
  5. }

类型断言可以用于将一个接口值转换为另一个接口类型,即使此接口值的类型并未实现另一个接口类型。

例如:

  1. package main
  2. type Foo interface {
  3. foo()
  4. }
  5. type T int
  6. func (T) foo() {}
  7. func main() {
  8. var x interface{} = T(123)
  9. // 下面这两行将编译失败。
  10. /*
  11. var _ Foo = x // error: interface{}类型没有实现Foo类型
  12. var _ = Foo(x) // error: interface{}类型没有实现Foo类型
  13. */
  14. // 但是下面这行可以编译通过。
  15. var _ = x.(Foo) // okay
  16. }

一个失败的类型断言的可选的第二个结果是否被舍弃将影响此类型断言的行为。

如果第二个可选结果出现在失败的类型断言中,那么此类型断言不会导致恐慌。否则,恐慌将产生。 例如:

  1. package main
  2. func main() {
  3. var x interface{} = true
  4. _, _ = x.(int) // 断言失败,但不会导致恐慌。
  5. _ = x.(int) // 断言失败,并导致一个恐慌。
  6. }

关于在编译时刻即可确定总是失败的目标类型为接口类型的断言。

在编译时刻,编译可以发现某些目标类型为接口类型的断言是不可能成功的。比如下面这个程序中的断言:

  1. package main
  2. type Ia interface {
  3. m()
  4. }
  5. type Ib interface {
  6. m() int
  7. }
  8. type T struct{}
  9. func (T) m() {}
  10. func main() {
  11. var x Ia = T{}
  12. _ = x.(Ib) // panic: main.T is not main.Ib
  13. }

这样的断言并不会导致编译失败(但编译后的程序将在运行时刻产生恐慌)。 从Go官方工具链1.15开始,go vet会对对这样的断言做出警告。

以相同实参调用两次errors.New函数返回的两个error值是不相等的。

原因是errors.New函数会复制输入的字符串实参至一个局部变量并取此局部变量的指针作为返回error值的动态值。 两次调用会产生两个不同的指针。

  1. package main
  2. import "fmt"
  3. import "errors"
  4. func main() {
  5. notfound := "not found"
  6. a, b := errors.New(notfound), errors.New(notfound)
  7. fmt.Println(a == b) // false
  8. }

单向接收通道无法被关闭。

例如,下面的代码会在编译时候失败。

  1. package main
  2. func main() {
  3. }
  4. func foo(c <-chan int) {
  5. close(c) // error: 不能关闭单向接收通道
  6. }

发送一个值到一个已关闭的通道被视为一个非阻塞操作,该操作会导致恐慌。

例如,在下面的程序里,如果第二个case分支会被选中,则在运行时刻将产生一个恐慌。

  1. package main
  2. func main() {
  3. var c = make(chan bool)
  4. close(c)
  5. select {
  6. case <-c:
  7. case c <- true: // panic: 向已关闭的通道发送数据
  8. default:
  9. }
  10. }

类型可以在声明函数体内。

类型可以声明在函数体内。例如,

  1. package main
  2. func main() {
  3. type T struct{}
  4. type S = []int
  5. }

对于标准编译器,结构体中的某些零尺寸字段的尺寸有可能会被视为一个字节。

关于细节,请阅读这个FAQ条目(第51章)。

NaN != NaN,Inf == Inf。

此规则遵循IEEE-754标准,并与大多数其它语言是一致的。

  1. package main
  2. import "fmt"
  3. import "math"
  4. func main() {
  5. var a = math.Sqrt(-1.0)
  6. fmt.Println(a) // NaN
  7. fmt.Println(a == a) // false
  8. var x = 0.0
  9. var y = 1.0 / x
  10. var z = 2.0 * y
  11. fmt.Println(y, z, y == z) // +Inf +Inf true
  12. }

不同代码包中的两个非导出方法名和结构体字段名总是被视为不同的名称。

例如,在包foo中声明了如下的类型:

  1. package foo
  2. type I = interface {
  3. about() string
  4. }
  5. type S struct {
  6. a string
  7. }
  8. func (s S) about() string {
  9. return s.a
  10. }

在包bar中声明了如下的类型:

  1. package bar
  2. type I = interface {
  3. about() string
  4. }
  5. type S struct {
  6. a string
  7. }
  8. func (s S) about() string {
  9. return s.a
  10. }

那么,

  • 两个包中的两个类型S的值不能相互转换。
  • 两个包中的两个接口类型指定了两个不同的方法集。
  • 类型foo.S没有实现接口类型 bar.I
  • 类型bar.S没有实现接口类型foo.I
  1. package main
  2. import "包2/foo"
  3. import "包2/bar"
  4. func main() {
  5. var x foo.S
  6. var y bar.S
  7. var _ foo.I = x
  8. var _ bar.I = y
  9. // 下面这些行将编译失败。
  10. x = foo.S(y)
  11. y = bar.S(x)
  12. var _ foo.I = y
  13. var _ bar.I = x
  14. }

在结构体值的比较中,名为空标识符的字段将被忽略。

比如,下面这个程序将打印出true

  1. package main
  2. import "fmt"
  3. type T struct {
  4. _ int
  5. _ bool
  6. }
  7. func main() {
  8. var t1 = T{123, true}
  9. var t2 = T{789, false}
  10. fmt.Println(t1 == t2) // true
  11. }

在某些很少见的场景中,圆括号是必需的。

例如:

  1. package main
  2. type T struct{x, y int}
  3. func main() {
  4. // 因为{}的烦扰,下面这三行均编译失败。
  5. /*
  6. if T{} == T{123, 789} {}
  7. if T{} == (T{123, 789}) {}
  8. if (T{}) == T{123, 789} {}
  9. var _ = func()(nil) // nil被认为是一个类型
  10. */
  11. // 必须加上一对小括号()才能编译通过。
  12. if (T{} == T{123, 789}) {}
  13. if (T{}) == (T{123, 789}) {}
  14. var _ = (func())(nil) // nil被认为是一个值
  15. }

栈溢出不可被挽救,它将使程序崩溃。

在目前的主流Go编译器实现中,栈溢出是致命错误。一旦栈溢出发生,程序将不可恢复地崩溃。

  1. package main
  2. func f() {
  3. f()
  4. }
  5. func main() {
  6. defer func() {
  7. recover() // 无法防止程序崩溃
  8. }()
  9. f()
  10. }

运行结果:

  1. runtime: goroutine stack exceeds 1000000000-byte limit
  2. fatal error: stack overflow
  3. runtime stack:
  4. ...

关于更多不可恢复的致命错误,请参考此篇维基文章

某些表达式的估值顺序取决于具体编译器实现。

关于细节,请阅读表达式估值顺序规则(第33章)一文。

reflect.DeepEqual(x, y)x == y的结果可能会不同。

如果表达式xy的类型不相同,则函数调用DeepEqual(x, y)的结果总为false,但x == y的估值结果有可能为true

如果xy为(同类型的)两个引用着不同其它值的指针值,则x == y的估值结果总为false,但函数调用DeepEqual(x, y)的结果可能为true,因为函数reflect.DeepEqual将比较xy所引用的两个值。

第三个区别是当xy均处于某个循环引用链中时,为了防止死循环,DeepEqual调用的结果可能为true

第四个区别是一个DeepEqual(x, y)调用无论如何不应该产生一个恐慌,但是如果xy是两个动态类型相同的接口值并且它们的动态类型是不可比较类型的时候,x == y将产生一个恐慌。

一个展示了这些不同的例子:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. type Book struct {page int}
  8. x := struct {page int}{123}
  9. y := Book{123}
  10. fmt.Println(reflect.DeepEqual(x, y)) // false
  11. fmt.Println(x == y) // true
  12. z := Book{123}
  13. fmt.Println(reflect.DeepEqual(&z, &y)) // true
  14. fmt.Println(&z == &y) // false
  15. type Node struct{peer *Node}
  16. var q, r, s Node
  17. q.peer = &q // 形成一个循环引用链
  18. r.peer = &s // 形成一个循环引用链
  19. s.peer = &r
  20. println(reflect.DeepEqual(&q, &r)) // true
  21. fmt.Println(q == r) // false
  22. var f1, f2 func() = nil, func(){}
  23. fmt.Println(reflect.DeepEqual(f1, f1)) // true
  24. fmt.Println(reflect.DeepEqual(f2, f2)) // false
  25. var a, b interface{} = []int{1, 2}, []int{1, 2}
  26. fmt.Println(reflect.DeepEqual(a, b)) // true
  27. fmt.Println(a == b) // 产生恐慌
  28. }

注意:如果传递给一个DeepEqual调用的两个实参均为函数类型值,则此调用只有在这两个实参都为nil并且它们的类型相同的情况下才返回true。 比较元素中含有函数值的容器值或者比较字段中含有函数值的结构体值也是类似的。 另外要注意:如果两个同类型切片共享相同的元素序列(即它们的长度相同并且它们的各对相应元素的地址也相同),则使用DeepEqual比较它们时返回的结果总是为true,即使它们的元素中含有函数值。 一个例子:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. a := [1]func(){func(){}}
  8. b := a
  9. fmt.Println(reflect.DeepEqual(a, a)) // false
  10. fmt.Println(reflect.DeepEqual(a[:], a[:])) // true
  11. fmt.Println(reflect.DeepEqual(a[:], b[:])) // false
  12. a[0], b[0] = nil, nil
  13. fmt.Println(reflect.DeepEqual(a[:], b[:])) // true
  14. }

reflect.Value.Bytes()方法返回一个[]byte值,它的元素类型byte可能并非属主参数代表的Go切片值的元素类型。

假设一个自定义类型MyByte的底层类型为内置类型byte,我们知道Go类型系统禁止切片类型[]MyByte的值转换为类型[]byte。 但是,当前的reflect.Value类型的Bytes方法的实现可以帮我们绕过这个限制。 此实现应该是违反了Go类型系统的规则。

例子:

  1. package main
  2. import "bytes"
  3. import "fmt"
  4. import "reflect"
  5. type MyByte byte
  6. func main() {
  7. var mybs = []MyByte{'a', 'b', 'c'}
  8. var bs []byte
  9. // bs = []byte(mybs) // this line fails to compile
  10. v := reflect.ValueOf(mybs)
  11. bs = v.Bytes() // okay. Violating Go type system.
  12. fmt.Println(bytes.HasPrefix(bs, []byte{'a', 'b'})) // true
  13. bs[1], bs[2] = 'r', 't'
  14. fmt.Printf("%s \n", mybs) // art
  15. }

虽然这违反了Go类型系统的规则,但是貌似此违反并没有什么害处,相反,它带来了一些好处。 比如,我们可以将bytes标准库包中提供的函数(间接)应用到[]MyByte值上,如上例所示。

注意:reflect.Value.Bytes()方法以后可能会被移除

我们应该使用os.IsNotExist(err)而不是err == os.ErrNotExist来检查文件是否存在。

使用err == os.ErrNotExist可能漏掉一些错误。

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. _, err := os.Stat("a-nonexistent-file.abcxyz")
  8. fmt.Println(os.IsNotExist(err)) // true
  9. fmt.Println(err == os.ErrNotExist) // false
  10. }

如果你的项目只支持Go 1.13+,则更推荐使用errors.Is(err, os.ErrNotExist)来检查文件是否存在。

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. )
  7. func main() {
  8. _, err := os.Stat("a-nonexistent-file.abcxyz")
  9. fmt.Println(errors.Is(err, os.ErrNotExist)) // true
  10. }

flag标准库包对待布尔命令选项不同于数值和字符串选项。

传递程序选项有三种形式。

  1. -flag:仅适用于布尔选项。
  2. -flag=x:用于任何类型的选项。.
  3. -flag x:仅用于非布尔选项。

请注意,使用第一种形式的布尔选项将被视为最后一个选项,其后面的所有项都被视为参数。

  1. package main
  2. import "fmt"
  3. import "flag"
  4. var b = flag.Bool("b", true, "一个布尔选项")
  5. var i = flag.Int("i", 123, "一个整数选项")
  6. var s = flag.String("s", "hi", "一个字符串选项")
  7. func main() {
  8. flag.Parse()
  9. fmt.Print("b=", *b, ", i=", *i, ", s=", *s, "\n")
  10. fmt.Println("arguments:", flag.Args())
  11. }

如果我们用下面显示的标志和参数运行此程序

  1. ./exampleProgram -b false -i 789 -s bye arg0 arg1

输出结果会是:

  1. b=true, i=123, s=hi
  2. arguments: [false -i 789 -s bye arg0 arg1]

这个输出显然不是我们所期望的。

我们应该像这样传递选项和参数:

  1. ./exampleProgram -b=false -i 789 -s bye arg0 arg1

或者

  1. ./exampleProgram -i 789 -s bye -b arg0 arg1

以获取我们期望的输出:

  1. b=true, i=789, s=bye
  2. arguments: [arg0 arg1]

[Sp|Fp|P]rintf函数支持位置参数。

下面的程序会打印coco

  1. package main
  2. import "fmt"
  3. func main() {
  4. // The next line prints: coco
  5. fmt.Printf("%[2]v%[1]v%[2]v%[1]v", "o", "c")
  6. }

本书由老貘历时三年写成。目前本书仍在不断改进和增容中。你的赞赏是本书和Go101.org网站不断增容和维护的动力。

(请搜索关注微信公众号“Go 101”或者访问github.com/golang101/golang101获取本书最新版)