跳转至

Go Mistakes

  • 变量作用域问题:变量隐藏(if 分支里使用 :=,不会覆盖外部定义的变量);
  • init函数的误用:不要依赖默认的顺序关系(包内按文件字母序加载) ;
  • 发现抽象而不是创建抽象:接口定义放在消费端(使用的包),而不是在生产端(实现的包);
  • 生产端定义接口:C++ / Java 的用法,Go 的实现不需要继承;
  • 消费端定义接口:客户端可以根据需要定义最准确的抽象;
  • 决策:标准库中使用生产者端接口(方法尽量少);不能证明抽象现在有效,就不要创建抽象。
  • 针对客户端抽象:生产端函数返回结构体而不是接口,客户端接受接口而不是结构体;
  • 不要使用 any,使用泛型;(~IntInt的区别,~Int 可以接受任意底层是 Int 的结构体)
  • 内嵌类型的使用:内嵌类型的所有方法,都可以直接使用;类似于组合,但是添加函数的转发;
  • nil slice 和 empty slice:两者 len 方法都返回0,nil slice 不分配内存;
  • map 只能扩容,不能缩容:删除元素不会导致map底层的bucket数组为空;
  • 使用 reflect.DeepEquals 对复杂的数据结构判等,或者使用第三方库 go-cmp等;
  • for 循环中元素是拷贝的副本,修改元素不会影响原先的数据 ;
  • map 遍历不保证顺序,且每次的顺序都可能不一致;
  • 打印 s[i] 不会打印第 i 个字符,它打印索引 i 处字节的 UTF-8 表示;
  • bytes 包提供了与 strings 包相同的操作,这有助于避免额外的字节/字符串转换。
  • 使用值接收器还是指针接收器:一般用指针
  • 方法需要修改接收器的值,那么应该使用 指针接收器
  • 值接收器会创建原始值的副本,在方法内部对其进行的修改 不会 影响原始值。
  • nil receiver 不等于 nil interface;(类型转换时,nil 只是将接口的值为 nil,但类型不是)
  • interface 类型包含两部分信息——值信息和类型信息,只有 interface的值和类型都为nil时,interface才为nil
  • nil receiver 调用方法时,不会有其他语言的空指针问题;
  • defer 函数中,参数立即被计算:使用闭包(内部引用外部变量),或者参数使用指针;
  • 错误类型的判断不要使用等号,使用 errors.As/Is 来比较错误的类型;
  • 大多数情况下,一个错误应该只处理一次:在日志记录或返回错误之间做出选择;
  • defer 函数中的错误,也需要进行处理;
  • goroutine 的创建开销 << 计算的开销,性能才会有效果;
  • 并行 goroutine 之间的同步应该通过互斥来实现,并发 goroutine 必须协调和编排
  • 在传播上下文时,理解上下文可以被取消的条件应该很重要;
  • goroutine 是一种资源,就像任何其他资源一样,最终必须关闭以释放内存或其他资源;
  • 在多通道中使用 select 时,如果有多个选项, 是随机选择;
  • 不需要特定的值来传递一些信息时,使用空结构的通道:chan struct{}
  • 允许从 nil 通道接收消息或向 nil 通道发送消息,会永远阻塞;
  • 使用 nil 通道,以某种方式从 select 语句中删除一个 case
  • 大部分情况,为缓冲通道使用的默认值是它的最小值:1;确定准确的队列大小并不是一个容易的问题;
  • 标准库的 sync.Mutex 不支持重入(Reentrancy)
  • 字符串格式可能的副作用,例如在已经获取锁的函数里调用Format,进一步调用 String 方法导致死锁;
  • 在并发应用程序中对共享切片使用 append 可能(取决于是否后扩容)会导致数据竞争;
  • 使用 sync.Cond 向多个 goroutine 发送重复的通知;
  • 同步一组 goroutine,并使用 errgroup 包处理错误和上下文;
  • sync类型不应该被复制;
  • 按大小降序重新组织结构的字段可以导致更紧凑的结构(更少的内存分配和潜在的更好的空间局部性);
  • 在容器中,GOMAXPROCS 不会自动设置为被限制的cpu配额,通过 uber-go/automaxprocs 自动设置;

参考文献

  1. 100 Go Mistakes and How to Avoid Them.