本篇文章是学习go-advice 时所写,包含了大部分写 go 需要注意的点
1. 谏言
不要通过共享内存进行通信,通过通信共享内存
并发不是并行 ✔
管道用于协调;互斥量(锁)用于同步 ✔
接口越大,抽象越弱 ✔
利用好零值
空结构interface{}
即any
没有任何类型约束 ✔
gofmt 的风格不是人们最喜欢的,但 gofmt 是每个人的最爱
一点点重复比引入一点点依赖更好
始终使用构建标记保护系统调用和 cgo
cgo 不是 go
使用unsafe
不能保证能如期运行
清晰比聪明更好
反射永远不清晰
错误是值
不要只检查错误,还要优雅的处理它们
设计架构,命名组件,文档记录细节
文档是供用户使用的
不要(在生产环境)使用panic()
2. 目标
每个 package 实现单一的目的
显示处理错误
尽早返回,而不是使用深嵌套
让调用者处理并发(带来的问题)
在启动一个 goroutine 时,需要知道它何时会停止
避免 package 级别的状态
简单很重要
编写测试以锁定 package API 的行为
如果你觉得很慢,先编写 benchmark 来证明
适度是一种美德
可维护性
3. 代码
使用go fmt
格式化
让团队一起使用官方的 Go 格式化工具,不要重新发明轮子,尝试减少代码复杂度,使代码更易于阅读
多个 if 语句折叠成 switch
使用chan struct{}
来传递信号,chan bool
表达不够清楚
30 * time.Second
比time.Duration(30) * time.Second
更好
用time.Duration
替代int64
+变量名
按类型分组const
声明,按逻辑或类型分组var
每个阻塞 或者IO 函数 操作应该是可取消或者至少是可超时的
为整型常量值实现Stringer
🔗 接口
检查defer
中的错误
1 2 3 4 5 6 defer func () { err := body.Clost() if err != nil { return err } }
不要在checkErr
函数中使用panic()
或os.Exit()
仅仅在很特殊的情况下才使用panic()
,你必须要去处理error
不要给枚举使用别名,因为这样打破了类型安全🔗
1 2 3 4 5 6 7 8 9 10 package maintype Status = int type Format = int const A Status = 1 const B Format = 1 func main () { println (A == B) }
如果你想省略返回参数,你最好表示出来,_ = f()
比f()
更好
我们用a := []T{}
简单初始化 slice
用 range 循环来进行数组或 slice 的迭代
1 2 3 4 5 for _, c := range a[3 :7 ] { ... }for i := 3 ; i < 7 ; i++ { ...}
1 func f (a int , _ string ) {}
如果你要比较时间戳,请使用time.Before()
或time.After()
,不要使用time.Sub()
来获得 duration,然后检查他的值
带有上下文的函数第一个参数名为ctx
,形如func foo(ctx Context, ...)
几个类型相同的参数定义可以用简短的方式来定义,func f(a int, b, c string)
一个 slice 的零值是 nil🔗
1 2 3 4 5 6 7 8 9 var s []int fmt.Println(s, len (s), cap (s)) if s == nil { fmt.Println("nil!" ) }
🔗
1 2 3 4 5 6 7 8 var a []string b := []string {} fmt.Println(reflect.DeepEqual(a, []string {})) fmt.Println(reflect.DeepEqual(b, []string {}))
不要将枚举类型与<
,>
,<=
,>=
进行比较,使用确定的值
1 2 3 4 5 value := reflect.ValueOf(object) kind := value.Kind() if kind >= reflect.Chan && kind <= reflect.Slice {}
1 2 3 4 5 6 7 8 9 10 11 func f1 () { var a, b struct {} print (&a, "\n" , &b, "\n" ) fmt.Println(&a == &b) } func f2 () { var a, b struct {} fmt.Printf("%p\n%p\n" , &a, &b) fmt.Println(&a == &b) }
包装错误🔗 ,如errors.Wrap(err, "additional message to a given error")
在 go 里要小心使用range
for i := range a
和for i,v := range &a
,都不是a
的副本
for i, v := range a
里面就是a
的副本
more 🔗
从 map 里读取一个不存在的 key 不会 panic
1 2 3 4 value := map ["no_key" ] value, ok := map ["no_key" ]
1 2 3 4 5 os.MkdirAll(path, 0700 ) os.MkdirAll(path, os.FileMode)
1 2 3 4 5 6 7 8 9 10 const ( _ = iota testvar ) type myType int const ( _ myType = iota testvar )
不要在你不拥有的结构上使用encoding/gob
,在某些时候,结构可能会改变,而你可能会错过这一点。因此,这可能会导致很难找到 bug。
不要依赖于计算顺序,特别是在return
语句中
1 2 3 4 5 6 return res, json.Unmarshal(b, &res)err := json.Unmarshal(b, &res) return res, err
防止结构体字段用纯值方式初始化,添加_ struct{}
字段
1 2 3 4 5 6 7 8 9 type Point struct { X, Y float64 _ struct {} } func main () { p1 := Point{X: 1 , Y: 1 } p2 := Point{1 , 1 } }
1 2 3 4 5 6 7 8 9 type Point struct { _ [0 ]func () X, Y float64 } func main () { p1, p2 := Point(1 ,1 ), Point(1 ,1 ) fmt.Println(p1 == p2) }
http.HandlerFunc
比http.Handler
更好,http.HandlerFunc
只需要一个 func,http.Handler
需要一个类型
defer
移动至顶部,提高代码可靠性,并明确函数结束调用了什么
JS 解析整数为浮点数可能会导致 int64 溢出,使用json:"id,string"
1 2 3 type Request struct { ID int64 `json:"id,string"` }
4. 并发
以线程安全的方式创建单例(只创建一次)的最好选择是sync.Once
,不要使用 flags、mutexes、channels or atomics
永远不要使用select{}
,省略通道,等待信号 ❓
不要关闭一个发送(写入)通道,应该由创建者关闭(⚠ 往一个关闭的 channel 里写数据会 panic)
math/rand
中的func NewSource(seed int64) Source
不是并发安全的,默认的lockedSource
是并发安全的🔗 or 🔗
当你需要一个自定义类型的 aotmic 值时,你可以使用atomic.Value
5. 性能
不要省略defer
,大多数情况下 200ns 加速可以忽略不计
总是关闭 http body,defer r.Body.Close()
,除非你需要泄漏 goroutine
过滤不需要分配新内存
1 2 3 4 5 6 7 8 b := a[:0 ] for _, x := range a { if condition { b = append (b, x) } }
time.Time
有指针字段time.Location
并且这对 go GC 不好,只有使用了大量的time.Time
对性能才有意义,否则使用 timestamp 代替
regexp.MustCompile
比regexp.Compile
更好,大多数情况下,正则表达式是不可变的,所以你最好在func init
中初始化它
请勿在你的热点代码中过度使用fmt.Sprintf
,由于维护接口的缓冲池和动态调度,它是很昂贵的
1 2 3 4 5 6 7 8 res := fmt.Sprintf("%s%s" , str1, str2) res := str1 + str2 res = fmt.Sprintf("%x" ,var ) res = strconv.FormatInt(var , 16 )
如果你不需要它,可以考虑丢弃它,例如io.Copy(ioutil.Discard, resp.Body)
,HTTP 客户端的传输不会重新连接,直到 body 被读完或关闭
1 2 3 res, _ := client.Do(req) io.Copy(ioutil.Discard, res.Body) defer res.Body.Close()
不要在循环里使用 defer,否则会导致内存泄漏,因为这些 defer 会不断地填满你的栈(内存)
不要忘记停止 ticker,除非你需要泄漏 channel
1 2 ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()
用自定义的 marshaler 去加速 marshal 过程,但是在使用它之前要进行定制!🔗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (entry Entry) MarshalJSON() ([]byte , error ) { buffer := bytes.NewBufferString("{" ) first := true for key, value := range entry { jsonValue, err := json.Marshal(value) if err != nil { return nil , err } if !first { buffer.WriteString("," ) } first = false buffer.WriteString(key + ":" + string (jsonValue)) } buffer.WriteString("}" ) return buffer.Bytes(), nil }
sync.Map
是万能的,没有过硬的理由就要不使用它 🔗
在sync.Pool
中分配内存存储非指针数据 🔗
为了隐藏逃生分析的指针,你可以小心使用这个函数 🔗
1 2 3 4 5 6 7 8 9 func noescape (p unsafe.Pointer) unsafe.Pointer { x := uintptr (p) return unsafe.Pointer(x ^ 0 ) }
对于最快的原子交换,你可以使用m := (*map[int]int)(atomic.LoadPointer(&ptr))
❓
如果执行许多顺序读取或写入操作,请使用缓冲 I/O,以减少系统调用次数
两种清空一个 map 的方法
1 2 3 4 5 6 7 for k := range m { delete (m, k) } m = make (map [int ]int )
6. 模块
如果你想在 CI 中测试go.mod
和go.sum
是否是最新🔗
7. 构建
用go build -ldflags="-s -w" ...
去掉你的二进制文件
拆分构建不同版本的简单方法,用// + build integration
并且运行他们go test -v --tags integration
构建最小的 Go Docker 镜像🔗
1 CGO_ENABLED=0 go build -ldflags="-s -w" app.go && tar C app | docker import - myimage:latest
run go format on CI and compare diff,确保一切都是生成的和承诺的
用最新的 Go 运行 Travis-CI,用travis 1
🔗
检查代码格式是否有错误diff -u <(echo -n) <(gofmt -d .)
8. 测试
测试名称package_test
比package
要好
go test -short
允许减少要运行的测试数
1 2 3 4 5 func TestSomething (t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode." ) } }
1 2 3 if runtime.GOARM == "arm" { t.Skip("this doesn't work under ARM" ) }
用testing.AllocPerRun
跟踪你的内存分配🔗
多次运行你的基准测试可以避免噪音go test -test.bench=. -count=20
9. 工具
快速替换gofmt -w -l -r "panic(err) -> log.Error(err)"
.
go list
允许找到所有直接和传递的依赖关系
go list -f '{{ .Imports }}' package
列出 package 中引入的包
go list -f '{{ .Deps }}' package
列出 package 中所有包的信息
对于快速基准比较,benchstat
go-critic
linter,从这个文件中强制执行几条建议
go mod why -m <module>
告诉我们为什么特定的模块在go.mod
文件中。
GOGC=off go build ...
应该会加快构建速度🔗
内存分析器每 512KB 记录一次分配。你能通过GODEBUG
环境变量增加比例,来查看你的文件的更多详细信息🔗
10. 其它
1 2 3 4 5 6 7 8 9 10 go func () { sigs := make (chan os.Signal, 1 ) signal.Notify(sigs, syscall.SIGQUIT) buf := make ([]byte , 1 <<20 ) for { <-sigs stacklen := runtime.Stack(buf, true ) log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n" , buf[:stacklen]) } }()
1 var _ io.Reader = (*MyFastReader)(nil )
1 2 3 4 5 6 7 var hits struct { sync.Mutex n int } hits.Lock() hits.n++ hits.Unlock()
httputil.DumpRequest
是非常有用的东西,不要自己创建🔗
获得调用堆栈,可以使用runtime.Caller
🔗
要 marshal 未知的 JSON,你可以 marshal 为map[string]interface{}
从一个 slice 生成简单的随机元素
1 []string {"one" , "two" , "three" }[rand.Intn(3 )]