Go踩坑

go初学者常见错误

强烈建议 go 新手先过一遍下边这篇文章,go 写多了你一定会遇到很多里边总结的坑。

Go tricks

影子变量(Shadowed variables)

  1. // https://yourbasic.org/golang/gotcha-shadowing-variables/
  2. func main() {
  3. n := 0
  4. if true {
  5. n := 1
  6. n++
  7. }
  8. fmt.Println(n) // 0,注意 if 作用与里边使用 := 赋值隐藏了外部的 n,所以原来的 n 打印还是 0
  9. // 如果想要修改 n,直接用 n = 1
  10. }

golang cannot refer to unexported field or method

小写开头只能被本 package 访问

访问其它Go package中的私有函数(不推荐)

可以写一个公有函数暴露私有方法,不过一般私有方法都是希望隐藏的,就像 python 的下划线虽然不强制,但是不推荐使用。

Can’t load package

同一个 folder 不要存在不同 package,否则 can’t load package

package is folder. package name is folder name. package path is folder path.

如何编译期间检查是否实现接口?

  1. // for pointers to struct
  2. type MyType struct{}
  3. var _ MyInterface = (*MyType)(nil) // 很多源码中可以看到这种写法,但是一开始感觉有点奇怪
  4. // for struct literals
  5. var _ MyInterface = MyType{}
  6. // for other types - just create some instance
  7. type MyType string
  8. var _ MyInterface = MyType("doesn't matter")

如何通过反射判断是否实现接口?

  1. type CustomError struct{}
  2. func (*CustomError) Error() string {
  3. return ""
  4. }
  5. func testImplements() {
  6. // 判断某个类型是否实现了接口。
  7. // 获取接口类型 reflect.TypeOf((*<interface>)(nil)).Elem()
  8. typeOfError := reflect.TypeOf((*error)(nil)).Elem()
  9. customErrorPtr := reflect.TypeOf(&CustomError{})
  10. customError := reflect.TypeOf(CustomError{})
  11. fmt.Println(customErrorPtr.Implements(typeOfError)) // true
  12. fmt.Println(customError.Implements(typeOfError)) // false
  13. }

Go 运行单个测试文件报错 undefined?

执行命令时加入这个测试文件需要引用的源码文件,在命令行后方的文件都会被加载到command-line-arguments中进行编译

go test -v getinfo_test.go lib.go

Go 循环遍历 []struct 是值传递

注意循环遍历一个结构体切片是值传递,如果想要修改 struct 的值,请使用 slice 下标赋值或者用结构体指针。

  1. type Cat struct {
  2. name string
  3. }
  4. func testSliceAssign() {
  5. cats := []Cat{
  6. {name: "cat1"},
  7. {name: "cat2"},
  8. }
  9. for _, cat := range cats { // cat 这里是拷贝的值
  10. cat.name = "new cat" // NOTE: 注意这里 cat 是拷贝的值,所以你无法修改 cat。使用下标或者指针
  11. }
  12. fmt.Println(cats) // 无法修改 [{cat1} {cat2}]
  13. // 方式1:使用下标
  14. for i, _ := range cats {
  15. cats[i].name = "new cat"
  16. }
  17. fmt.Println(cats)
  18. // 方式2:使用struct 指针
  19. pcats := []*Cat{
  20. {name: "cat1"},
  21. {name: "cat2"},
  22. }
  23. for _, cat := range pcats {
  24. cat.name = "new cat"
  25. }
  26. for _, cat := range pcats {
  27. fmt.Println(cat)
  28. }
  29. }

Go 无法修改值为结构体的map

  1. func testChangeMapStruct() {
  2. type T struct{ Cnt int }
  3. m := map[string]T{"a": T{Cnt: 1}, "b": T{Cnt: 2}}
  4. for _, v := range m {
  5. v.Cnt = 100
  6. }
  7. fmt.Println(m)
  8. // 想要修改 map[string]T 的值,必须使用指针
  9. m2 := map[string]*T{"a": &T{Cnt: 1}, "b": &T{Cnt: 2}}
  10. for _, v := range m2 {
  11. v.Cnt = 100
  12. }
  13. fmt.Println(m2["a"])
  14. }
  15. func main() {
  16. testChangeMapStruct()
  17. }

不要并发读写map,可能导致程序崩溃

使用内置 map 注意几点:

  • 使用 make 初始化。直接声明然后赋值会 panic
  • 赋值是浅拷贝。深拷贝需要自己复制
  • 内置 map 不要并发写入或者删除,必须加锁。或者使用 sync.Map

如果多个 goroutine 并发对 map 进行读写,必须要同步,否则可能导致进程退出

  1. // https://blog.golang.org/go-maps-in-action
  2. var counter = struct{
  3. sync.RWMutex
  4. m map[string]int
  5. }{m: make(map[string]int)}
  6. counter.RLock() // locks for reading
  7. n := counter.m["some_key"]
  8. counter.RUnlock()
  9. fmt.Println("some_key:", n)
  10. counter.Lock() // locks for writing
  11. counter.m["some_key"]++
  12. counter.Unlock()

如何判断一个空结构体(empty struct)

  1. // 注意需要加上括号,否则 syntax error
  2. // https://stackoverflow.com/questions/28447297/how-to-check-for-an-empty-struct
  3. if (Session{}) == session {
  4. fmt.Println("is zero value")
  5. }

go 如何实现函数默认值(go本身没提供)

  1. // https://stackoverflow.com/questions/19612449/default-value-in-gos-method
  2. // 可以通过传递零值或者 nil 的方式来判断。
  3. // Both parameters are optional, use empty string for default value
  4. func Concat1(a string, b int) string {
  5. if a == "" {
  6. a = "default-a"
  7. }
  8. if b == 0 {
  9. b = 5
  10. }
  11. return fmt.Sprintf("%s%d", a, b)
  12. }

go 初始化 slice/map 的区别

直接看代码,注意 map 赋值之前需要先 make 创建,直接给一个 nil map 赋值会 panic,但是 slice 却可以直接声明然后 append。 如果是一个 struct 包含了 map,你应该在构造函数里进行 make 初始化,否则直接赋值也会 panic。

  1. // 初始化一个全局 map 可以用 make,防止第一次赋值 nil map 会 panic
  2. var globalMap = make(map[string]string) // 之后可以在 init() 函数初始化
  3. func main() {
  4. var intSlice []int // 注意可以直接声明一个 slice 然后 append
  5. fmt.Println(intSlice)
  6. intSlice = append(intSlice, 1)
  7. fmt.Println(intSlice)
  8. // 已知最大容量的情况下,建议 make 初始化,可以避免重新分配内存提升效率
  9. maxCap := 3
  10. intSlice2 := make([]int, 0, maxCap)
  11. fmt.Println(intSlice2)
  12. m2 := make(map[int]int) // 如果是 map 要先 make 才可以,否则 panic
  13. m2[1] = 1 // ok
  14. fmt.Println(m2)
  15. // 直接声明然后赋值就会 panic。有一些 struct 包含了 map 结构体成员,构造函数里需要注意初始化 map,否则直接赋值panic
  16. // https://stackoverflow.com/questions/27553399/golang-how-to-initialize-a-map-field-within-a-struct
  17. var m1 map[int]int
  18. m1[1] = 1 // NOTE: panic ! 注意这样会panic 啊!!!
  19. fmt.Println(m1)
  20. type T struct {
  21. m map[int][int]
  22. }
  23. func NewT() T {
  24. return T{m: make(map[int]int)}
  25. // return T{m: map[int]int{}}
  26. }
  27. }

go 没有内置的 set 结构

go 目前没有提供泛型,也没提供一个统一的 set 数据结构。可以使用 map[string]bool 来模拟 set(注意并发安全)。 或者使用第三方提供的 set 类型。

编译 tag 的作用

  1. // +build linux,386 darwin,!cgo

Application auto build versioning

给 build 的二进制文件加上版本号,注意如果命令中输出有空格,需要加上单引号。 这样我们可以每次运行二进制文件的时候打印构建时间,当前的版本等信息。

  1. // +build linux,386 darwin,!cgo
  2. package main
  3. import "fmt"
  4. var xyz string
  5. func main() {
  6. fmt.Println(xyz)
  7. }
  8. // $ go run -ldflags "-X main.xyz=abc" main.go
  9. // go build -ldflags "-X main.minversion=`date -u +.%Y%m%d.%H%M%S`" service.go
  10. // go build -ldflags "-X 'main.time=$(date -u --rfc-3339=seconds)' -X 'main.git=$(git log --pretty=format:"%h" -1)'" main.go

Go JSON 空值处理的一些坑,看示例

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. // https://www.sohamkamani.com/blog/golang/2018-07-19-golang-omitempty/
  7. // omitempty 对于0值和,nil,pointer 的处理需要注意下坑。
  8. func testNormal() {
  9. type Dog struct {
  10. Breed string
  11. WeightKg int
  12. }
  13. d := Dog{
  14. Breed: "dalmation",
  15. WeightKg: 45,
  16. }
  17. b, _ := json.Marshal(d)
  18. fmt.Println(string(b)) // {"Breed":"dalmation","WeightKg":45}
  19. }
  20. func testOmit() {
  21. type Dog struct {
  22. Breed string
  23. WeightKg int
  24. }
  25. d := Dog{
  26. Breed: "pug",
  27. }
  28. b, _ := json.Marshal(d)
  29. fmt.Println(string(b)) //{"Breed":"pug","WeightKg":0}
  30. // 注意没填的字段输出0,如果不想输出0呢?比如想输出 null 或者压根不输出这个字段
  31. }
  32. func testOmitEmpty() {
  33. type Dog struct {
  34. Breed string
  35. // The first comma below is to separate the name tag from the omitempty tag
  36. WeightKg int `json:",omitempty"`
  37. }
  38. d := Dog{
  39. Breed: "pug",
  40. }
  41. b, _ := json.Marshal(d)
  42. fmt.Println(string(b)) // {"Breed":"pug"}
  43. }
  44. func testValuesCannotBeOmitted() {
  45. type dimension struct {
  46. Height int
  47. Width int
  48. }
  49. type Dog struct {
  50. Breed string
  51. WeightKg int
  52. Size dimension `json:",omitempty"`
  53. }
  54. d := Dog{
  55. Breed: "pug",
  56. }
  57. b, _ := json.Marshal(d)
  58. fmt.Println(string(b)) //{"Breed":"pug","WeightKg":0,"Size":{"Height":0,"Width":0}}
  59. }
  60. func testValuesCannotBeOmittedButUsePointer() {
  61. type dimension struct {
  62. Height int
  63. Width int
  64. }
  65. type Dog struct {
  66. Breed string
  67. WeightKg int
  68. Size *dimension `json:",omitempty"` //和上一个不同在于这里使用指针
  69. }
  70. d := Dog{
  71. Breed: "pug",
  72. }
  73. b, _ := json.Marshal(d)
  74. fmt.Println(string(b)) // {"Breed":"pug","WeightKg":0}
  75. }
  76. // The difference between 0, "" and nil
  77. // One issue which particularly caused me a lot a trouble is the case where you want to differentiate between a default value, and a zero value.
  78. //
  79. // For example, if we have a struct describing a resteraunt, with the number of seated customers as an attribute:
  80. func testZeroWillOmit() {
  81. type Restaurant struct {
  82. NumberOfCustomers int `json:",omitempty"`
  83. }
  84. d := Restaurant{
  85. NumberOfCustomers: 0,
  86. }
  87. b, _ := json.Marshal(d)
  88. fmt.Println(string(b)) // {}
  89. // 输出 {}, 0被省略了
  90. }
  91. func testZeroPointer() {
  92. type Restaurant struct {
  93. NumberOfCustomers *int `json:",omitempty"`
  94. }
  95. d1 := Restaurant{}
  96. b, _ := json.Marshal(d1)
  97. fmt.Println(string(b)) //Prints: {}
  98. n := 0
  99. d2 := Restaurant{
  100. NumberOfCustomers: &n,
  101. }
  102. b, _ = json.Marshal(d2)
  103. fmt.Println(string(b)) //Prints: {"NumberOfCustomers":0} ,总结一下就是值为 0 的 pointer 也不会省略字段
  104. }
  105. func main() {
  106. // testOmit()
  107. // testOmitEmpty()
  108. // testValuesCannotBeOmitted()
  109. // testValuesCannotBeOmittedButUsePointer()
  110. testZeroWillOmit()
  111. }

Go int/int64/float 和 string 转换示例

  1. // 推荐一个更加强大的转换库:https://github.com/spf13/cast
  2. package main
  3. import (
  4. "fmt"
  5. "strconv"
  6. )
  7. func main() { // 测试 int 和 string(decimal) 互相转换的函数
  8. // https://yourbasic.org/golang/convert-int-to-string/
  9. // int -> string
  10. sint := strconv.Itoa(97)
  11. fmt.Println(sint, sint == "97")
  12. // byte -> string
  13. bytea := byte(1)
  14. bint := strconv.Itoa(int(bytea))
  15. fmt.Println(bint)
  16. // int64 -> string
  17. sint64 := strconv.FormatInt(int64(97), 10)
  18. fmt.Println(sint64, sint64 == "97")
  19. // int64 -> string (hex) ,十六进制
  20. sint64hex := strconv.FormatInt(int64(97), 16)
  21. fmt.Println(sint64hex, sint64hex == "61")
  22. // string -> int
  23. _int, _ := strconv.Atoi("97")
  24. fmt.Println(_int, _int == int(97))
  25. // string -> int64
  26. _int64, _ := strconv.ParseInt("97", 10, 64)
  27. fmt.Println(_int64, _int64 == int64(97))
  28. // https://stackoverflow.com/questions/30299649/parse-string-to-specific-type-of-int-int8-int16-int32-int64
  29. // string -> int32,注意 parseInt 始终返回的是 int64,所以还是需要 int32(n) 强转一下
  30. _int32, _ := strconv.ParseInt("97", 10, 32)
  31. fmt.Println(_int32, int32(_int32) == int32(97))
  32. // int32 -> string, https://stackoverflow.com/questions/39442167/convert-int32-to-string-in-golang
  33. strconv.FormatInt(int64(i), 10) // fast
  34. strconv.Itoa(int(i)) // fast
  35. fmt.Sprint(i) // slow
  36. // int -> int64 ,不会丢失精度
  37. var n int = 97
  38. fmt.Println(int64(n) == int64(97))
  39. // string -> float32/float64 https://yourbasic.org/golang/convert-string-to-float/
  40. f := "3.14159265"
  41. if s, err := strconv.ParseFloat(f, 32); err == nil {
  42. fmt.Println(s) // 3.1415927410125732
  43. }
  44. if s, err := strconv.ParseFloat(f, 64); err == nil {
  45. fmt.Println(s) // 3.14159265
  46. }
  47. // float -> string https://yourbasic.org/golang/convert-string-to-float/
  48. s := fmt.Sprintf("%f", 123.456)
  49. }

Go struct 如何设置默认值

Go 的结构体成员没法直接设置默认值,使用的是每个类型的默认值,可以 New 构造函数里设置。

  1. // https://stackoverflow.com/questions/37135193/how-to-set-default-values-in-go-structs
  2. //Something is the structure we work with
  3. type Something struct {
  4. Text string
  5. DefaultText string
  6. }
  7. // NewSomething create new instance of Something
  8. func NewSomething(text string) Something {
  9. something := Something{}
  10. something.Text = text
  11. something.DefaultText = "default text"
  12. return something
  13. }

Go 如何使用枚举值 Enum

Go没有提供内置的枚举类型,不过可以使用自定义类型和常量值来实现枚举类型。 并且还可以给自定义的类型定义方法。

  1. type Base int
  2. const (
  3. A Base = iota
  4. C
  5. T
  6. G
  7. )

Go 如何断判非空字符串/slice

标准库实际上 len(s) != 0s != "" 都有使用,我个人倾向于 s != "" 看起来更清楚,区分其他容器类型判断的方式。 比如如果使用 slice 可以使用 len(slice) == 0 判断是否为空。

Go 如何格式化参数

命名返回值

go 的返回值可以命名,使用命名返回值有几个用处:

  • 可以当成文档,直观的展示返回值的含义
  • 自动初始化为类型的零值
  • 返回的时候不用写很多参数名,直接用 return 就行
  • 如果想要在 defer 中修改返回值,只能使用命名参数。例子如下
  • 缺点:函数里很容易误用声明一个同名的参数就会被被覆盖了(shadow)
  • 函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。
  1. func namedReturn(i int) (ret int) {
  2. ret = i
  3. defer func() { ret++ }()
  4. return
  5. }
  6. func anonReturn(i int) int {
  7. ret := i
  8. defer func() { ret++ }() // 修改 ret 无效
  9. return ret
  10. }
  11. func main() {
  12. fmt.Println(namedReturn(0)) // 1
  13. fmt.Println(anonReturn(0)) // 0
  14. }

Go 如何复制map

注意 go 和其他很多编程语言一样,对于复合结构是浅拷贝,共享底层数据结构。几个变量指向同一个复合结构的时候注意修改一个对其他变量也是可见的。

  1. // https://stackoverflow.com/questions/23057785/how-to-copy-a-map
  2. func copyMap(src map[string]string) map[string]string {
  3. res := make(map[string]string)
  4. for k, v := range src {
  5. res[k] = v
  6. }
  7. return res
  8. }
  9. func testShareMap() {
  10. am := []map[string]string{
  11. map[string]string{"a1": "a1", "b1": "b1"},
  12. map[string]string{"a2": "a2", "b2": "b2"},
  13. }
  14. bm := am
  15. bm[0]["a1"] = "testbm" // NOTE 这里修改了b,a 里边的也会变。共享 map
  16. fmt.Println(am)
  17. var cm []map[string]string
  18. for _, m := range am {
  19. cm = append(cm, copyMap(m))
  20. }
  21. cm[0]["a1"] = "testcm" // will not modify am
  22. fmt.Println(am)
  23. }
  24. func main() {
  25. testShareMap()
  26. }

闭包问题

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. // 闭包问题
  7. func testClosure() {
  8. data := []string{"one", "two", "three"}
  9. for _, v := range data {
  10. go func() {
  11. fmt.Println(v)
  12. }()
  13. }
  14. time.Sleep(1 * time.Second) // not good, just for demo
  15. // three three three
  16. }
  17. // 两种方式解决:1.使用一个for 循环临时变量
  18. func testClosure1() {
  19. data := []string{"one", "two", "three"}
  20. for _, v := range data {
  21. vcopy := v
  22. go func() {
  23. fmt.Println(vcopy)
  24. }()
  25. }
  26. time.Sleep(1 * time.Second) // not good, just for demo
  27. // one two three (may wrong order)
  28. }
  29. // 方法2:使用传给匿名goroutine参数,推荐使用这种方式
  30. func testClosure2() {
  31. data := []string{"one", "two", "three"}
  32. for _, v := range data {
  33. go func(in string) {
  34. fmt.Println(in)
  35. }(v)
  36. }
  37. time.Sleep(1 * time.Second) // not good, just for demo
  38. // one two three (may wrong order)
  39. }
  40. type field struct {
  41. name string
  42. }
  43. func (p *field) print() {
  44. fmt.Println(p.name)
  45. }
  46. func testField() {
  47. data := []field{{"one"}, {"two"}, {"three"}}
  48. for _, v := range data {
  49. // v := v // NOTE:直接这样就可以解决,
  50. // 或者使用 struct 指针。 []*field 初始化
  51. go v.print() // print three three three
  52. }
  53. time.Sleep(1 * time.Second)
  54. }
  55. func main() {
  56. // testClosure()
  57. // testClosure1()
  58. // testClosure2()
  59. testField()
  60. }

Failed Type Assertions

  1. package main
  2. import "fmt"
  3. func main() {
  4. var data interface{} = "hehe"
  5. // NOTE: 这里不要用 同名的 data 变量,比如换成 dataInt
  6. if data, ok := data.(int); ok {
  7. fmt.Println("[is an int] value =>", data)
  8. } else {
  9. fmt.Println("[not an int] value =>", data)
  10. // NOTE :注意 data 已经被失败的 type assert 赋值成了0
  11. }
  12. }

An interface holding a nil value is not nil

An interface holding nil value is not nil. An interface equals nil only if both type and value are nil.

  1. package main
  2. import "fmt"
  3. func main() {
  4. var a interface{}
  5. fmt.Printf("a == nil is %t\n", a == nil) // a == nil is true
  6. var b interface{}
  7. var p *int = nil
  8. b = p
  9. fmt.Printf("b == nil is %t\n", b == nil) // b == nil is false
  10. }

逃逸分析

想要知道变量在 stask 还是 heap 分配使用 go run -gcflags -m app.go

报错:go test flag: flag provided but not defined

redio tricks

redis 连接超时

默认是没有超时时间的,注意设置超时时间(connect/read/write)。

redis 单测如何 mock

reids mock 可以用 miniredis,以下是一个示例代码

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "testing"
  6. "github.com/alicebob/miniredis"
  7. "github.com/go-redis/redis"
  8. "github.com/stretchr/testify/assert"
  9. )
  10. var followImpl *Follow
  11. func setupFollow() {
  12. fmt.Println("setup")
  13. mr, err := miniredis.Run()
  14. if err != nil {
  15. panic("init miniredis failed")
  16. }
  17. client := redis.NewClient(&redis.Options{
  18. Addr: mr.Addr(),
  19. })
  20. _ = client.Set("key", "wang", 0).Err()
  21. followImpl = &Follow{client: client}
  22. }
  23. func TestGet(t *testing.T) {
  24. val, err := followImpl.Get("key")
  25. followImpl.client.Set("key", "2", 0)
  26. fmt.Println(val, err)
  27. assert.Equal(t, val, "wang")
  28. }
  29. func TestPING(t *testing.T) {
  30. PING()
  31. }
  32. func TestMain(m *testing.M) {
  33. setupFollow()
  34. code := m.Run()
  35. os.Exit(code)
  36. }

网络相关

获取本机 ip

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net"
  6. "time"
  7. )
  8. var localIp string // 用一个全局变量或者缓存,防止高并发的时候重复频繁系统调用
  9. // GetIPAddr 获取 server IP
  10. func GetIPAddr() string {
  11. if localIp != "" {
  12. // fmt.Printf("use local ip %s\n", localIp)
  13. return localIp
  14. }
  15. addrs, err := net.InterfaceAddrs()
  16. if err != nil {
  17. return ""
  18. }
  19. for _, addr := range addrs {
  20. if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
  21. if ipnet.IP.To4() != nil {
  22. localIp = ipnet.IP.String()
  23. return localIp
  24. }
  25. }
  26. }
  27. return ""
  28. }
  29. func main() {
  30. fmt.Println(GetIPAddr())
  31. }

Go panic 场景

在《Go 编程之旅》中总结了一些 panic 场景,写 go 的时候留意下:

  • 数组/切片越界
  • 空指针调用
  • 过早关闭 HTTP 响应体
  • 除以 0
  • 向已经关闭的 channel 发送消息
  • 重复关闭 channel
  • 关闭未初始化的 channel
  • 未初始化 map
  • 跨协程的 panic 处理
  • sync 计数为负数。