go编码规范&性能调优指南 | 青训营笔记
这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
编码规范
推荐使用go fmt自动格式化代码,goimports能够自动增删依赖的包引用、将依赖包按字母序排序并分类。GoLand都有哈哈。
注释
-
注释应该解释代码作用
- 说明公共常量,变量,对外提供的函数等。清晰的描述功能和用途。
-
注释应该解释代码如何做的
- 注释实现过程
-
注释应该解释代码实现的原因
- 解释代码的外部因素
- 提供额外上下文
-
注释应该解释代码什么情况会出错
公共符号始终要注释
命名规范
变量
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 使用
ServeHTTP而非ServeHttp - 使用
XMLHTTPRequest或者xmlHTTPRequest
- 使用
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息(全局变量)
函数
- 函数名不携带包的上下文信息,因为包名和函数名总是成对出现的
- 尽量简短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 到名为foo的包某个函数返回类型T时(T不时Foo),可以在函数名中加入类型信息
包
- 只由小写字母构成
- 简短并包含一定的上下文信息。例如schema、task等
- 不要与标准库同名。例如不要使用sync或者strings
以下规则尽量满足,以标准库报名为例
- 不使用常用变量名作为报名。例如使用bufio而不是buf
- 使用单数而不是复数
- 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短
控制流程
-
尽量保持正常代码路径为最小缩进
- 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
错误和异常处理
简单错误
简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误。
- 优先使用
erros.New来创建匿名变量直接表示简单错误 - 如果有格式化的需求,使用
fmt.Errorf
嵌套错误
使用 fmt.Errorf 加上 %w 格式符来生成一个嵌套的 error,它并没有像 pkg/errors 那样使用一个 Wrap 函数来嵌套 error,非常简洁。
func Unwrap(err error) error
将嵌套的 error 解析出来,多层嵌套需要调用 Unwrap 函数多次,才能获取最里层的 error。
源码如下:
func Unwrap(err error) error {
// 判断是否实现了 Unwrap 方法
u, ok := err.(interface {
Unwrap() error
})
// 如果不是,返回 nil
if !ok {
return nil
}
// 调用 Unwrap 方法返回被嵌套的 error
return u.Unwrap()
}
对 err 进行断言,看它是否实现了 Unwrap 方法,如果是,调用它的 Unwrap 方法。否则,返回 nil。
特定错误
判定一个错误是否为特定错误,使用error-Is。
不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误。
func Is(err, target error) bool
data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
return []byte{}, nil
}
从 err 错误链里找到和 target 相等的并且设置 target 所指向的变量,使用error-As。它和is的区别在于as会提取出调用链中指定类型的错误,并将错误赋值给定义好的变量,方便后续处理。
func As(err error, target interface{}) bool
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
panic
在Go中,比错误更严重的就是panic,它的出现表示程序无法正常工作了。
不建议在业务代码中使用panic。因为panic发生后,会向上传播至调用栈顶,如果当前goroutine中所有deferred函数都不包含recover就会导致整个程序崩溃。若问题可以被屏蔽或解决,建议使用error代替panic。
特殊地,当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic。
recover
我们并不能控制所有的代码,避免不了引入其他库,如果是引入库的bug导致panic,影响到自身的逻辑该如何处理?
简单来讲:go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。
- recover只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer的语句时后进先出
注意:利用 recover 处理 panic 指令,defer 必须在 panic 之前声明,否则当 panic 时,recover 无法捕获到 panic。
func main() {
fmt.Println("c")
defer func() { // 必须要先声明defer,否则不能捕获到panic异常
fmt.Println("d")
if err := recover(); err != nil {
fmt.Println(err) // 这里的err其实就是panic传入的内容
}
fmt.Println("e")
}()
f() //开始调用f
fmt.Println("f") //这里开始下面代码不会再执行
}
func f() {
fmt.Println("a")
panic("异常信息")
fmt.Println("b") //这里开始下面代码不会再执行
}
-------output-------
c
a
d
异常信息
e
性能优化指南
性能优化是综合评估,有时候时间效率和空间效率可能对立。
工具
Go语言提供了支持基准性能测试的benchmark工具。
go test -bench=. -benchmem
示例代码:
// from fib.go
func Fib(n int) int {
if n < 2 {
return 1
} else {
return Fib(n-1) + Fib(n-2)
}
}
// from fib_test.go
func BenchmarkFib(b *testing.B) {
for i := 0; i < b.N; i++ {
Fib(10)
}
}
slice
- 尽可能在使用
make()初始化切片时提供容量信息 - 在已有切片基础上创建切片,不会创建新的底层数组,使用copy替代re-slice
func GetLastBySlice(origin []int) []int {
return origin[len(origin) - 2:]
}
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin) - 2:])
return result
}
map
- 预分配内存
data := make(map[int]int, size)
字符串处理
- 使用+拼接性能最差,
strings.Builder,bytes.Buffer相近,strings.Builder更快。
func preStrPlus(n int, str string) string {
ans := ""
for i := 0; i < n; i++ {
ans += str
}
return ans
}
func preStrBuilder(n int, str string) string {
var builder strings.Builder
builder.Grow(n * len(str)) // 预分配内存
for i := 0; i < 10; i++ {
builder.WriteString(str)
}
return builder.String()
}
空结构体
空结构体struct{}实例不占据任何的内存空间,可作为各种场景下的占位符使用。
比如map中的键值对,若某个不需要的话,就可以用空结构体来占位。
atomic包
处理与锁相关的问题时,可以用atomic包。
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率比锁高
sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量- 对于非数值操作,可以使用
atomic.Value,能承载一个interface{}