Golang 新手可能会踩的 50 个坑 下
在多行 array、slice、map 语句中缺少 ,
号 1 2 3 4 5 6 7 8 9 func main () { x := []int { 1 , 2 } y := []int {1 ,2 ,} z := []int {1 ,2 } }
声明语句中 }
折叠到单行后,尾部的 ,
不是必需的。
log.Fatal
和 log.Panic
不只是 loglog 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()、Panic*() 时能做更多日志外的事,如中断程序的执行等:
1 2 3 4 func main () { log.Fatal("Fatal level log: log entry" ) log.Println("Nomal level log: log entry" ) }
对内建数据结构的操作并不是同步的 尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。
goroutine
和 channel
是进行原子操作的好方法,或使用 "sync"
包中的锁。
range 迭代 string 得到的值 range
得到的索引是字符值(Unicode point
/ rune
)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。
注意一个字符可能占多个 rune,比如法文单词 café
中的 é。操作特殊字符可使用norm 包。
for range
迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode
替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。
1 2 3 4 5 6 7 8 9 10 func main () { data := "A\xfe\x02\xff\x04" for _, v := range data { fmt.Printf("%#x " , v) } for _, v := range []byte (data) { fmt.Printf("%#x " , v) } }
range 迭代 map 如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。
Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:
1 2 3 4 5 6 func main () { m := map [string ]int {"one" : 1 , "two" : 2 , "three" : 3 , "four" : 4 } for k, v := range m { fmt.Println(k, v) } }
如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:
switch 中的 fallthrough 语句 switch
语句中的 case
代码块会默认带上 break
,但可以使用 fallthrough
来强制执行下一个 case
代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { isSpace := func (char byte ) bool { switch char { case ' ' : case '\t' : return true } return false } fmt.Println(isSpace('\t' )) fmt.Println(isSpace(' ' )) }
不过你可以在 case
代码块末尾使用 fallthrough
,强制执行下一个 case
代码块。
也可以改写 case
为多条件判断:
1 2 3 4 5 6 7 8 9 10 11 func main () { isSpace := func (char byte ) bool { switch char { case ' ' , '\t' : return true } return false } fmt.Println(isSpace('\t' )) fmt.Println(isSpace(' ' )) }
自增和自减运算 很多编程语言都自带前置后置的 ++
、--
运算。但 Go 特立独行,去掉了前置操作,同时 ++
、—
只作为运算符而非表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { data := []int {1 , 2 , 3 } i := 0 ++i fmt.Println(data[i++]) } func main () { data := []int {1 , 2 , 3 } i := 0 i++ fmt.Println(data[i]) }
按位取反 很多编程语言使用 ~
作为一元按位取反(NOT)操作符,Go 重用 ^
XOR 操作符来按位取反 :
1 2 3 4 5 6 7 8 9 10 11 func main () { fmt.Println(~2 ) } func main () { var d uint8 = 2 fmt.Printf("%08b\n" , d) fmt.Printf("%08b\n" , ^d) }
同时 ^
也是按位异或 (XOR)操作符。
一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02
,与二元的 XOR 操作 0x22 XOR 0xff
是一致的。
运算符的优先级 除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n" , 0x2 &0x2 +0x4 ) fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n" , 0x2 +0x2 <<0x1 ) fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n" , 0xf |0x2 ^0x2 ) }
1 2 3 4 5 6 Precedence Operator 5 * / % << >> & &^ 4 + - | ^ 3 == != < <= > >= 2 && 1 ||
不导出的 struct 字段无法被 encode 以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:
1 2 3 4 5 6 7 8 9 10 11 func main () { in := MyData{1 , "two" } fmt.Printf("%#v\n" , in) encoded, _ := json.Marshal(in) fmt.Println(string (encoded)) var out MyData json.Unmarshal(encoded, &out) fmt.Printf("%#v\n" , out) }
程序退出时还有 goroutine 在执行 程序默认不等所有 goroutine
都执行完才退出,这点需要特别注意:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { workerCount := 2 for i := 0 ; i < workerCount; i++ { go doIt(i) } time.Sleep(1 * time.Second) fmt.Println("all done!" ) } func doIt (workerID int ) { fmt.Printf("[%v] is running\n" , workerID) time.Sleep(3 * time.Second) fmt.Printf("[%v] is done\n" , workerID) }
常用解决办法:使用 "WaitGroup"
变量,它会让主程序等待所有 goroutine
执行完毕再退出。
如果你的 goroutine
要做消息的循环处理等耗时操作,可以向它们发送一条 kill
消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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) } for i := 0 ; i < workerCount; i++ { 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 } } }
向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回 只有在数据被 receiver
处理时,sender
才会阻塞。因运行环境而异,在 sender
发送完数据后,receiver
的 goroutine
可能没有足够的时间处理下一个数据。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 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" }
向已关闭的 channel 发送数据会造成 panic 从已关闭的 channel 接收数据是安全的:
接收状态值 ok
是 false
时表明 channel
中已没有数据可以接收了。类似的,从有缓冲的 channel
中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false
向已关闭的 channel
中发送数据会造成 panic
:
1 2 3 4 5 6 7 8 9 10 11 12 func main () { ch := make (chan int ) for i := 0 ; i < 3 ; i++ { go func (idx int ) { ch <- idx }(i) } fmt.Println(<-ch) close (ch) time.Sleep(2 * time.Second) }
针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done
的结果是 {}
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { ch := make (chan int ) done := make (chan struct {}) for i := 0 ; i < 3 ; i++ { go func (idx int ) { select { case ch <- (idx + 1 ) * 2 : fmt.Println(idx, "Send result" ) case <-done: fmt.Println(idx, "Exiting" ) } }(i) } fmt.Println("Result: " , <-ch) close (done) time.Sleep(3 * time.Second) }
使用了值为 nil 的 channel 在一个值为 nil
的 channel
上发送和接收数据将永久阻塞:
1 2 3 4 5 6 7 8 9 10 11 func main () { var ch chan int for i := 0 ; i < 3 ; i++ { go func (i int ) { ch <- i }(i) } fmt.Println("Result: " , <-ch) time.Sleep(2 * time.Second) }
runtime 死锁错误:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive (nil chan)]
利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func main () { inCh := make (chan int ) outCh := make (chan int ) go func () { var in <-chan int = inCh var out chan <- int var val int for { select { case out <- val: println ("--------" ) out = nil in = inCh case val = <-in: println ("++++++++++" ) out = outCh in = nil } } }() go func () { for r := range outCh { fmt.Println("Result: " , r) } }() time.Sleep(0 ) inCh <- 1 inCh <- 2 time.Sleep(3 * time.Second) }
若函数 receiver 传参是传值方式,则无法修改参数的原有值 方法 receiver
的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。
除非 receiver
参数是 map
或 slice
类型的变量,并且是以指针方式更新 map
中的字段、slice
中的元素的,才会更新原有值:
struct、array、slice 和 map 的值比较 可以使用相等运算符 ==
来比较结构体变量,前提是两个结构体的成员都是可比较的类型 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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) }
如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { v1 := data{} v2 := data{} fmt.Println("v1 == v2: " , reflect.DeepEqual(v1, v2)) m1 := map [string ]string {"one" : "a" , "two" : "b" } m2 := map [string ]string {"two" : "b" , "one" : "a" } fmt.Println("v1 == v2: " , reflect.DeepEqual(m1, m2)) s1 := []int {1 , 2 , 3 } s2 := []int {1 , 2 , 3 } fmt.Println("v1 == v2: " , reflect.DeepEqual(s1, s2)) }
注意: DeepEqual()
并不总适合于比较 slice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { var str = "one" var in interface {} = "one" fmt.Println("str == in: " , reflect.DeepEqual(str, in)) v1 := []string {"one" , "two" } v2 := []string {"two" , "one" } fmt.Println("v1 == v2: " , reflect.DeepEqual(v1, v2)) data := map [string ]interface {}{ "code" : 200 , "value" : []string {"one" , "two" }, } encoded, _ := json.Marshal(data) var decoded map [string ]interface {} json.Unmarshal(encoded, &decoded) fmt.Println("data == decoded: " , reflect.DeepEqual(data, decoded)) }
reflect.DeepEqual()
认为空 slice
与 nil slice
并不相等,但注意 byte.Equal()
会认为二者相等:
1 2 3 4 5 6 7 8 func main () { var b1 []byte = nil b2 := []byte {} fmt.Println("b1 == b2: " , bytes.Equal(b1, b2)) }
从 panic 中恢复 在一个 defer 延迟执行的函数中调用 recover()
,它便能捕捉 / 中断 panic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { recover () panic ("not good" ) recover () println ("ok" ) } func main () { defer func () { fmt.Println("recovered: " , recover ()) }() panic ("not good" ) }
从上边可以看出,recover()
仅在 defer 执行的函数中调用才会生效。
1 2 3 4 5 6 7 8 9 10 11 func main () { defer func () { doRecover() }() panic ("not good" ) } func doRecover () { fmt.Println("recobered: " , recover ()) }
recobered: panic: not good
在 range 迭代 slice、array、map 时通过更新引用来更新元素 在 range
迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:
1 2 3 4 5 6 7 func main () { data := []int {1 , 2 , 3 } for _, v := range data { v *= 10 } fmt.Println("data: " , data) }
如果要修改原有元素的值,应该使用索引直接访问:
1 2 3 4 5 6 7 func main () { data := []int {1 , 2 , 3 } for i, v := range data { data[i] = v * 10 } fmt.Println("data: " , data) }
如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:
1 2 3 4 5 6 7 func main () { data := []*struct { num int }{{1 }, {2 }, {3 },} for _, v := range data { v.num *= 10 } fmt.Println(data[0 ], data[1 ], data[2 ]) }
slice 中隐藏的数据 从 slice
中重新切出新 slice
时,新 slice
会引用原 slice
的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice
来指向原底层数组的部分数据,将导致难以预料的内存使用。
1 2 3 4 5 6 7 8 9 10 func get () []byte { raw := make ([]byte , 10000 ) fmt.Println(len (raw), cap (raw), &raw[0 ]) return raw[:3 ] } func main () { data := get() fmt.Println(len (data), cap (data), &data[0 ]) }
可以通过拷贝临时 slice 的数据,而不是重新切片来解决:
1 2 3 4 5 6 7 8 9 10 11 12 func get () (res []byte ) { raw := make ([]byte , 10000 ) fmt.Println(len (raw), cap (raw), &raw[0 ]) res = make ([]byte , 3 ) copy (res, raw[:3 ]) return } func main () { data := get() fmt.Println(len (data), cap (data), &data[0 ]) }
旧 slice 当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 “旧”(stale) slice 问题。
某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { s1 := []int {1 , 2 , 3 } fmt.Println(len (s1), cap (s1), s1) s2 := s1[1 :] fmt.Println(len (s2), cap (s2), s2) for i := range s2 { s2[i] += 20 } fmt.Println(s1) fmt.Println(s2) s2 = append (s2, 4 ) for i := range s2 { s2[i] += 10 } fmt.Println(s1) fmt.Println(s2) }
for 语句中的迭代变量与闭包函数 for 语句中的迭代变量在每次迭代中都会重用,即 for
中创建的闭包函数接收到的参数始终是同一个变量 ,在 goroutine
开始执行时都会得到同一个迭代值:
1 2 3 4 5 6 7 8 9 10 11 12 func main () { data := []string {"one" , "two" , "three" } for _, v := range data { go func () { fmt.Println(v) }() } time.Sleep(3 * time.Second) }
最简单的解决方法:无需修改 goroutine
函数,在 for
内部使用局部变量保存迭代值,再传参:
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { data := []string {"one" , "two" , "three" } for _, v := range data { vCopy := v go func () { fmt.Println(vCopy) }() } time.Sleep(3 * time.Second) }
另一个解决方法:直接将当前的迭代值以参数形式传递给匿名函数:
1 2 3 4 5 6 7 8 9 10 11 12 func main () { data := []string {"one" , "two" , "three" } for _, v := range data { go func (in string ) { fmt.Println(in) }(v) } time.Sleep(3 * time.Second) }
defer 函数的参数值 对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值:
1 2 3 4 5 6 func main () { var i = 1 defer fmt.Println("result: " , func () int { return i * 2 }()) i++ }
result: 2
defer 函数的执行时机 对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。
比如在一个长时间执行的函数里,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就会出现问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func main () { if len (os.Args) != 2 { os.Exit(1 ) } dir := os.Args[1 ] start, err := os.Stat(dir) if err != nil || !start.IsDir() { os.Exit(2 ) } var targets []string filepath.Walk(dir, func (fPath string , fInfo os.FileInfo, err error ) error { if err != nil { return err } if !fInfo.Mode().IsRegular() { return nil } targets = append (targets, fPath) return nil }) for _, target := range targets { f, err := os.Open(target) if err != nil { fmt.Println("bad target:" , target, "error:" , err) break } defer f.Close() } }
解决办法:defer 延迟执行的函数写入匿名函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { for _, target := range targets { func () { f, err := os.Open(target) if err != nil { fmt.Println("bad target:" , target, "error:" , err) return } defer f.Close() }() } }
当然你也可以去掉 defer
,在文件资源使用完毕后,直接调用 f.Close()
来关闭。
失败的类型断言 在类型断言语句中,断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { var data interface {} = "great" if data, ok := data.(int ); ok { fmt.Println("[is an int], data: " , data) } else { fmt.Println("[not an int], data: " , data) } } func main () { var data interface {} = "great" if res, ok := data.(int ); ok { fmt.Println("[is an int], data: " , res) } else { fmt.Println("[not an int], data: " , data) } }
堆栈变量 你并不总是清楚你的变量是分配到了堆还是栈。
在 C++
中使用 new
创建的变量总是分配到堆内存上的,但在 Go 中即使使用 new()
、make()
来创建变量,变量为内存分配位置依旧归 Go
编译器管。
Go 编译器会根据变量的大小及其 "escape analysis"
的结果来决定变量的存储位置,故能准确返回本地变量的地址,这在 C/C++ 中是不行的。
在 go build
或 go run
时,加入 -m
参数,能准确分析程序的变量分配位置: