0%

Golang笺言

本篇文章是学习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.Secondtime.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 main
type Status = int
type Format = int // remove `=` to have type safety

const A Status = 1
const B Format = 1

func main() {
println(A == B) // true, but a.type != b.type
}
  • 如果你想省略返回参数,你最好表示出来,_ = 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!")
}

// Output:
// [] 0 0
// 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{}))
// Output:
// false
// true
  • 不要将枚举类型与<,>,<=,>=进行比较,使用确定的值
1
2
3
4
5
value := reflect.ValueOf(object)
kind := value.Kind()
if kind >= reflect.Chan && kind <= reflect.Slice {
// ...
}
  • %+v来打印数据比较全的信息
  • 注意空结构struct{},看 issue: golang/go#23440 or 🔗
1
2
3
4
5
6
7
8
9
10
11
func f1() {
var a, b struct{}
print(&a, "\n", &b, "\n") // Prints same address
fmt.Println(&a == &b) // Comparison returns false
}

func f2() {
var a, b struct{}
fmt.Printf("%p\n%p\n", &a, &b) // Again, same address
fmt.Println(&a == &b) // ...but the comparison returns true
}
  • 包装错误🔗,如errors.Wrap(err, "additional message to a given error")
  • 在 go 里要小心使用range
    1. for i := range afor i,v := range &a,都不是a的副本
    2. for i, v := range a里面就是a的副本
    3. more 🔗
  • 从 map 里读取一个不存在的 key 不会 panic
1
2
3
4
value := map["no_key"]  // return 0

// well
value, ok := map["no_key"] // return 0, false and check ok
  • 不要使用原始参数进行文件操作
1
2
3
4
5
// bad
os.MkdirAll(path, 0700)

// good
os.MkdirAll(path, os.FileMode)
  • 要不忘记对iota指定一种类型🔗
1
2
3
4
5
6
7
8
9
10
const (
_ = iota
testvar // testvar 将是 int 类型
)

type myType int
const (
_ myType = iota
testvar // testvar 将是 myType 类型
)
  • 不要在你不拥有的结构上使用encoding/gob,在某些时候,结构可能会改变,而你可能会错过这一点。因此,这可能会导致很难找到 bug。
  • 不要依赖于计算顺序,特别是在return语句中
1
2
3
4
5
6
// BAD
return res, json.Unmarshal(b, &res)

// GOOD
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{} // to prevent unkeyed literals
}

func main(){
p1 := Point{X: 1, Y: 1}
p2 := Point{1, 1} // compile error
}
  • 防止结构比较,可以添加func类型的空字段
1
2
3
4
5
6
7
8
9
type Point struct {
_ [0]func() // unexported, zero-width non-comparable field
X, Y float64
}

func main(){
p1, p2 := Point(1,1), Point(1,1)
fmt.Println(p1 == p2) // 正常输出true,但添加func空字段后会编译报错
}
  • http.HandlerFunchttp.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)
}
}

// 为了帮助编译器删除绑定检查,参考`_ = b[7]`
  • time.Time有指针字段time.Location并且这对 go GC 不好,只有使用了大量的time.Time对性能才有意义,否则使用 timestamp 代替
  • regexp.MustCompileregexp.Compile更好,大多数情况下,正则表达式是不可变的,所以你最好在func init中初始化它
  • 请勿在你的热点代码中过度使用fmt.Sprintf,由于维护接口的缓冲池和动态调度,它是很昂贵的
1
2
3
4
5
6
7
8
res := fmt.Sprintf("%s%s", str1, str2)
// may be
res := str1 + str2

res = fmt.Sprintf("%x",var)
// may be
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
// noescape hides a pointer from escape analysis.  noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
//go:nosplit
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
// 重用map内存(但是也要注意m的回收)
for k := range m {
delete(m, k)
}

// 给m分配新的内存
m = make(map[int]int)

6. 模块

  • 如果你想在 CI 中测试go.modgo.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_testpackage要好
  • 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. 其它

  • dump goroutines 🔗
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)
  • len(nil) = 0 🔗
  • 匿名结构体很好用
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)]