Go 基础编码规范与性能优化

Go 编码规范

风格

自动格式化代码工具:

  1. gofmt
  2. goimports

GoLand 内置了这些工具。

注释

注释内容:

  1. 代码作用
  2. 代码如何如何做的
  3. 代码实现的原因
  4. 代码什么情况下会出错(代码的限制条件)

公共符号始终要注释;

注释应该提供代码未表达出的上下文信息。

命名规范

降低阅读代码成本,重点考虑上下文信息,设计简介清晰的名称。

Good naming is like a good joke. If you have to explain it, it’s not funny

—— Dave Cheney

variable

缩略词使用全大写 ServeHTTP 而非 ServerHttp。若不需要导出,则使用全小写

变量距离被使用的地方越远,则命名中需要携带越多的上下文信息。`

1
2
3
4
// Good 使用"deadline"能够在变量名中携带更多信息量
func send(req *Request, deadline time.time)
// bad t的意义不明
func send(req *Reuqest, t time.time)

function

  • 函数名不携带包名的上下文信息

  • 函数名应该尽量简短

  • 当名为 foo 的包的某个函数返回 Foo 类型时,可以在函数名省略其类型信息

    1
    2
    3
    4
    5
    package http
    // good
    func Serve(...)
    // bad
    func ServeHTTP(...)

    在调用时:http.Serve(...)

  • 当 foo 包的某个函数返回类型 T 时,可以在函数名中加入类型信息

    1
    2
    3
    4
    5
    6
    package time

    // bad
    func Parse(s String) (Duration, error)
    // good
    func ParseDuration(s string) (Duration, error)

    在调用时:duration := time.ParseDuration(...)

package

必须满足:

  • 只由小写字母组成
  • 简短、包含一定的上下文信息
  • 不要于标准库同名

尽量满足:

  • 不是用常用变量名作为包名
  • 使用单数而非复数
  • 谨慎地使用缩写

控制流程

  • 避免嵌套。

  • 优先处理错误情况,尽早返回循环。若 if 有 return 则省略 else

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // bad
    if A {
    return
    } else {
    if B {
    return
    } else {
    if C {
    return
    } else {
    ...
    }
    }
    }


    // good
    if A {
    return
    }
    if B {
    return
    }
    if C {
    return
    }
    ...

错误与异常处理

  • 简单的错误(仅出现一次,且其他地方不需要捕获)

    1
    errors.New("Simple Error")
  • 复杂错误

    1
    2
    3
    4
    5
    6
    7
    // err 错误由其他代码抛出,在此处进行关联 补充错误上下文信息
    fmt.Errorf("reading srcfiles list: %w", err)

    // 判断错误链是否有指定错误
    errors.Is(err1, err2)
    // 取出特定错误
    errors.As(err3, &err)
  • panic 程序遇到无法解决的异常

    • 不建议在业务代码中使用
    • 调用函数不包含 recorver 会造成程序崩溃
    • 若错误可以屏蔽或解决,建议使用 error
    • 当程序启动阶段发生不可逆转的错误时,可以在 initmain 中使用 panic
  • recover

    只能在 defer 中使用

    1
    2
    3
    4
    5
    defer func() {
    if e:= recover() && e != nil {
    err = fmt.Errorf("error: %v \n %s", err, debug.Stack())
    }
    }

Go 性能优化

Benchmark

1
go test -bench=. -benchmem

技巧

slice

  • 尽可能在初始化时提供容量信息

    1
    make([]int, capSize)
  • 使用 copy 代替 re-slice 以解决大内存未释放的问题

    由于类似于 origin[1:100] 这样的用法虽然创建了一个新的 Slice,但是还是复用着原 Slice 底层的数组,而如果原 Slice 不再使用的话,底层数组也会跟随新的 Slice 一直存在。使用 copy 函数可以避免此问题。

    1
    copy(result, origin[1:100])

map

预分配内存

1
make(map[string]int, capSize)

减少扩容带来的 Rehash 和内存分配的耗时

拼接字符串

字符串在 Go 中时不可变类型,占用内存大小的固定的,每次 + 都会重新分配内存。strings.Builder 等底层时 []byte,并根据内存扩容策略,无需每次拼接都重新分配切片。

  • 使用 + 性能最差
  • strings.Builder, bytes.Buffer 相近,bytes.Buffer 转化为字符串时重新申请空间,而 strings.Builder 使用原有的空间
  • strings.Buffer 最快

空结构体

空结构体不占据任何内存空间,可作为占位符使用。

atomic

通过硬件实现,效率比锁高

sync.Mutex 应用来保护一段逻辑而非只是一个变量

对于非数值操作,可以使用 atomic.Value,可以承载一个interface{}

测试工具

pprof