Go最佳实践

来自NSQ

nsq的官方文档的Dsign中提到一个PPThttps://speakerdeck.com/snakes/nsq-nyc-golang-meetup, 里面有这样一段话

总结一下.

  1. don’t be afraid of sync package
    sync包里有
  • sync.Mutex(互斥锁,一读一写)
  • sync.RWMutex(读写锁,可以多读一写)
  • sync.Pool(对象池, 合理利用可以减少内存分配, 降低GC压力, 稍后写一篇博客说说)
  • sync.Once(并发控制. 适用于开几个goroutines去执行一个只执行一次的任务, 比如单例模式)
  • sync.Cond(并发控制, cond.Wait()阻塞至其他goroutie运行到cond.Signal())
  • sync.WaitGroup(并发控制. 通常用法 wg.Add增加任务数量 goroutie完成任务后执行wg.Done,任务数量减1 wg.Wait等待wg任务数量为0)
  1. goroutines are cheap not free
    这句话在其他地方也看过, go func()简单好用, 创建开销也很小, 但也是有开销的. 很多情况下开固定数量worker, 用channel传递数据, 效果会更好.
    go-apns2中的example是个非常好的例子.https://github.com/sideshow/apns2/blob/master/_example/channel/main.go

注意一个问题, go里面一个goroutine panic了, 会导致进程退出, 所以go func()时第一行带上

go func(){
defer func(){
if err:=recover(); err!=nil{
}
}()
}()

是安全的做法, worker channel法时类似

package main

import (
"fmt"
"log"
"time"
)

func main() {
ch := make(chan int, 10)

for i := 0; i < 2; i++ {
go worker(ch, i)
}

for i := 0; i < 3; i++ {
ch <- i
ch <- -1
}

time.Sleep(time.Second * 5)
}

func worker(ch <-chan int, goId int) {

log.Printf("worker%d running", goId)
for data := range ch {
func() {
defer func() {
if err := recover(); err != nil {
log.Printf("worker%d recover error:%s", goId, err)
}
}()
log.Printf("worker%d received data:%d", goId, data)
if data == -1 {
panic(fmt.Errorf("worker%d panic", goId))
}
}()
}
}

fasthttp之所以快, 其中一个原因就是net/http是来一个连接就创建一个goroutie, 而fasthttp用了池复用了goroutines.

  1. watch your allocations (string() is costly, re-user buffers)
    go里面 []byte和string互转是会发生复制的, 开销明显, 如果代码里频繁互转, 考虑使用bytes.buffer 和 sync.Pool

  2. use anonymous structs for arbitrary JSON
    在写http api时, parse body这种事情, 如果只是纯粹取body里的json数据, 没必要单独定义结构体, 在函数里定义一个匿名结构体就好. var s struct { A int}

  3. no built-in per-request HTTP timeouts
    这是说要注意默认的httpClient没有超时

  4. synchronizing goroutine exit is hard - log each cleanup step in long-running goroutines
    同步化的goroutine一不小心就没有退出, 如果你写一个长期运行的服务, 用logger记录每一个goroutine的清理退出, 防止goroutine泄露

  5. select skips nil channels

select语句是会跳过nil的channels的. 因为在Go里往已经close掉的channel里发送数据是会panic的, 可以利用select语句.
附: channel操作导致panic的情况有: 关闭一个nil的channel, 关闭一个已经关闭的channel( j,ok:= <- ch, ok为false时代表ch已经关闭了), 往一个已经关闭的channel里发送数据(从已经关闭的channel里读数据是OK的, 如果这个channel是带缓冲的, 那么可以读到所有数据)

来自GO箴言

Python有import this的zen of Python, 想不到Go也有箴言
https://speakerdeck.com/ajstarks/go-proverbs

  1. 在go里, goroutines之间通信不要用共享内存的方式实现, 应该用channel来实现
  2. 并发不是并行
  3. channel是编排, mutexs是串行
  4. interface定义越多的方法, 抽象程度越低. Go提倡用接口组合的方式实现更大的接口
  5. 零值, 猜测这里说的是struct{}吧, struct{}是一个不占内存的空结构体, 在用map实现set, channel发送无额外意义的signal时能降低内存分配
  6. 提倡gofmt
  7. 一点点复制比一点点依赖好. 官方包里有时能见到一些复制的代码, 这是为了不互相依赖
  8. syscall每个平台实现不一样, 要加build tags
  9. cgo每个平台的lib不一样, 要加build tags
  10. Cgo不是go
  11. unsafe包不提供保障
  12. 简洁胜过高效
  13. error是值 可以用值的方式去处理错误: 传递, 比较
  14. 不用仅检查错误, 要优雅地处理
  15. 多花精力设计架构, 模块命名, 写详细的文档
  16. 写良好的文档给用户
  17. 对于普通错误, 应该用多值返回错误, 而不是手动panic

实践

  1. 写可重复使用的函数, 接收接口类型, 返回具体类型
  2. 写可扩展便于二次包装的函数, 接收接口类型, 返回接口类型. 如标准库的database/sql包

传值还是传指针

  1. 指针仅用于要修改值的场景和反射, 其他场景尽可能地用值传递, 能让变量尽可能分配在栈上, 减少GC压力, 提高性能. 还能减少nil 参考 https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/

Golang Github

https://github.com/golang/go/wiki/CodeReviewComments