介绍
代码风格是代码的一种约定。用风格这个词可能有点不恰当,因为这些约定涉及到的远比源码文件格式工具 gofmt 所能处理的更多。
本指南的目标是通过详细描述 Uber 在编写 Go 代码时的取舍来管理代码的这种复杂性。这些规则的存在是为了保持代码库的可管理性,同时也允许工程师更高效地使用 go 语言特性。
本指南最初由 Prashant Varanasi 和 Simon Newton 为了让同事们更便捷地使用 go 语言而编写。多年来根据其他人的反馈进行了一些修改。
本文记录了 uber 在使用 go 代码中的一些习惯用法。许多都是 go 语言常见的指南,而其他的则延伸到了一些外部资料:
Effective Go
The Go common mistakes guide
所用的代码在运行 golint 和 go vet 之后不会有报错。建议将编辑器设置为:
保存时运行 goimports
运行 golint 和 go vet 来检查错误
你可以在下面的链接找到 Go tools 对一些编辑器的支持:[https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins]
指南
接口的指针
你几乎不需要指向接口的指针,应该把接口当作值传递,它的底层数据仍然可以当成一个指针。
一个接口是两个字段:
- 指向特定类型信息的指针。你可以认为这是 “type.”。
- 如果存储的数据是指针,则直接存储。如果数据存储的是值,则存储指向此值的指针。
如果你希望接口方法修改底层数据,则必须使用指针。
接收者和接口
具有值接收者的方法可以被指针和值调用。
例如,
package main
import "fmt"
func main() {
s1Val := S1{} //值
s1Ptr := S1{} //指针
s2Val := S2{} //值
s2Ptr := &S2{}//指针
var i F
i = s1Val
i = s1Ptr
// 以下不能被编译,因为 s2Val 是一个值,并且 f 没有值接收者
//i = s2Val
i = s2Ptr
fmt.Print(i)
}
//(s S1)可以包容Value和Pointer传递
//(s *S2)只能接收Pointer传递
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
零值 Mutexes 是有效的
零值的 sync.Mutex 和 sync.RWMutex 是有效的,所以你几乎不需要指向 mutex 的指针
var mu sync.Mutex
mu.Lock
defer mu.Unlock()
如果你使用一个指针指向的结构体,mutex 可以作为一个非指针字段,或者,最好是直接嵌入这个结构体。
复制 Slice 和 Map
slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心
接收 Slice 和 Map 作为入参
需要留意的是,如果你保存了作为参数接收的 map 或 slice 的引用,可以通过引用修改它。
返回 Slice 和 Map
类似的,当心 map 或者 slice 暴露的内部状态是可以被修改的。
Defer 的使用
使用 defer 去关闭文件句柄和释放锁等类似的这些资源。
defer 的开销非常小,只有在你觉得你的函数执行需要在纳秒级别的情况下才需要考虑避免使用。使用 defer 换取的可读性是值得的。这尤其适用于具有比简单内存访问更复杂的大型方法,这时其他的计算比 defer 更重要。
channel 的大小是 1 或者 None
channel 的大小通常应该是 1 或者是无缓冲的。默认情况下,channel 是无缓冲的且大小为 0。任何其他的大小都必须经过仔细检查。应该考虑如何确定缓冲的大小,哪些因素可以防止 channel 在负载时填满和阻塞写入,以及当这种情况发生时会造成什么样的影响。
枚举值从 1 开始
在 Go 中引入枚举的标准方法是声明一个自定义类型和一个带 iota 的 const 组。由于变量的默认值为 0,因此通常应该以非零值开始枚举。
在某些情况下,使用零值是有意义的,例如零值是想要的默认值。
type LogOutPut int
consg (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
//LogToStdout=0 LogToFile=1,LogToRemote=2
Error 类型
声明 error 有多种选项:
- [ errors.New] 声明简单静态的字符串
- [ fmt.Errorf] 声明格式化的字符串
- 实现了 Error() 方法的自定义类型
- 使用 [ “pkg/errors”.Wrap] 包装 error
返回 error 时,可以考虑以下因素以确定最佳选择: - 不需要额外信息的一个简单的 error? 那么 [ errors.New] 就够了
- 客户端需要检查并处理这个 error?那么应该使用实现了 Error() 方法的自定义类型
- 是否需要传递下游函数返回的 error?那么请看 section on error wrapping
- 否则, 可以使用 [ fmt.Errorf]
如果客户端需要检查这个 error,你需要使用 [ errors.New] 和 var 来创建一个简单的 error。
如果你有一个 error 可能需要客户端去检查,并且你想增加更多的信息(例如,它不是一个简单的静态字符串),这时候你需要使用自定义类型。
在直接导出自定义 error 类型的时候需要小心,因为它已经是包的公共 API。最好暴露一个 matcher 函数(译者注:以下示例的 IsNotFoundError 函数)去检查 error。
// package foo
type errNotFound struct{
file string
}
func (e errNotFound)Error() string{
return fmt.Sprintf("file %q not found",e.file)
}
func IsNotFoundError(err error)bool{
_,ok :=err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err :=foo.Open("foo");err !=nil{
if foo.IsNotFoundError(err){
// handle
}else{
panic("unknown error")
}
}
Error 包装
如果调用失败,有三个主要选项用于 error 传递:
- 如果没有额外增加的上下文并且你想维持原始 error 类型,那么返回原始 error
- 使用 [ “pkg/errors”.Wrap] 增加上下文,以至于 error 信息提供更多的上下文,并且 [ “pkg/errors”.Cause] 可以用来提取原始 error
- 如果调用者不需要检查或者处理具体的 error 例子,那么使用 [ fmt.Errorf]
推荐去增加上下文信息取代描述模糊的 error,例如 “connection refused”,应该返回例如 “failed to
call service foo: connection refused” 这样更有用的 error。
请参考 Don’t just check errors, handle them gracefully.
处理类型断言失败
简单的返回值形式的类型断言在断言不正确的类型时将会 panic。因此,需要使用 “, ok” 的常用方式。
避免 Panic
生产环境跑的代码必须避免 panic。它是导致 级联故障 的主要原因。如果一个 error 产生了,函数必须返回 error 并且允许调用者决定是否处理它。
panic/recover 不是 error 处理策略。程序在发生不可恢复的时候会产生 panic,例如对 nil 进行解引用。一个例外是在程序初始化的时候:在程序启动时那些可能终止程序的问题会造成 panic。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
甚至在测试用例中,更偏向于使用 t.Fatal 或者 t.FailNow 解决 panic 确保这个测试被标记为失败。
使用 go.uber.org/atomic
性能
strconv 优于 fmt
避免 string 到 byte 的转换
代码样式
聚合相似的声明
包的分组导入的顺序
包命名
函数命名
别名导入
函数分组和顺序
减少嵌套
不必要的 else
顶层变量的声明
在不可导出的全局变量前面加上 _
结构体的嵌入
使用字段名去初始化结构体
局部变量声明
nil 是一个有效的 slice
减少变量的作用域
避免裸参数
使用原生字符串格式来避免转义
初始化结构体
在 Printf 之外格式化字符串
Printf-style 函数的命名
设计模式
表格驱动测试
函数参数可选化
《未完待续,此文章为转载》