go编码规范&性能调优指南 | 青训营笔记

93 阅读6分钟

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.Builderbytes.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{}