青训营笔记之高质量编程及编码规范 | 豆包MarsCode AI 刷题

45 阅读5分钟

如何编写高质量的 Go 代码

编码规范

代码格式

① gofmt,能过自动格式化 Go 语言代码为官方统一版本,
② goimports,实际等于 gofmt + 依赖包管理,可以自动增删依赖的包引用,将依赖包按字母序排序并分类

注释

好的注释要求:①解释代码作用,适合注释公共符号,
②解释代码如何做的,适合注释实现过程,
③解释代码实现的原因,适合提供额外上下文,解释代码的外部因素,
④解释代码什么情况会出错,适合解释代码的限制条件
官方手册给的建议:①包中声明的每个公共符号、变量、常量以及结构体都需注释,②任何不明显也不简短的公共功能需要注释,③库中的任何函数都需注释

命名规范

①变量命名 varaible,缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写,变量具体其被使用的地方越远,则需要携带更多的上下文信息
②函数命名 function,函数名不需要携带包名的上下文信息,因为包名和函数名都是成对出现的,函数名尽量简短
例子:当名为 foo 的包某个函数返回类型为 Foo 时,可以省略类型信息而不导致歧义,而返回类型为 T 时,可以在函数名中加入类型信息
③包命名 package,只用小写字符组成,不包含大写字母以及下划线等字符,简短但尽量包含上下文信息,不要和标准库同名,不使用常用变量作为包名

控制流程

①避免嵌套,保持正常流程清晰,②尽量保持正常代码路径为最小缩进,比如优先处理错误/异常情况,尽早返回或继续循环来减少嵌套

错误和异常处理

①简单错误,即指仅出现一次的错误,且在其他地方不需要捕获该错误,优先使用 error.New 来创建匿名变量直接表示错误,如果有格式要求就用 fmt.Errorf
②复杂错误,注意 warp 和 unwarp,前者是提供 error 嵌套 error 的能力,从而产生 error 的跟踪链,后者就是从跟踪链中解析出来具体的某种 error,比如常用API,errors.Is(错误判定)、errors.As(获取错误链上的特定种类的错误)、errors.Unwrap,以及 fmt.Errorf 的 %w 关键字关联错误到错误链中
③panic,问题比较大程序崩溃,不建议在业务代码中使用,尽量用 error 代替(但也可以在程序启动阶段发生不可逆转错误时,在 init 或者 main 函数中使用)
④recover,出现 panic 时进行恢复的作用,只能在 defer 的函数中使用且不能嵌套,只在当前 goroutine 生效(注:如果出现多个 defer,这些 defer 按照后进先出的顺序去判断 ),如果需要上下文信息,可以在 recover 后再 log 中记录当前的调用栈

性能优化

性能测试工具,Go 语言提供支持基准测试的工具,Benchmark,使用例子如下:

package main
import (
    "testing"
)
// 假设我们要测试一个简单的函数
func add(a, b int) int {
    return a + b
}
// 基准测试函数,其命名必须以 Benchmark 开头,并且接受一个类型为 *testing.B 的参数
// *testing.B 提供了一些方法和字段,用于控制测试的执行和记录结果
func BenchmarkAdd(b *testing.B) {
    // b.N 是一个由测试框架自动设置的变量,表示测试函数应该执行的次数
    // 每次运行基准测试时,b.N 的值可能会不同,以确保测试结果的准确性和可靠性
    for i := 0; i < b.N; i++ {
        add(1, 2)
    }
}
// go test -bench=. 执行,可能出现以下结果
// BenchmarkAdd-8          1000000000               0.50 ns/op
// PASS
// ok      your/package/name  0.566s

// BenchmarkAdd-8:基准测试函数名,后面的 -8 表示测试是在 8 个逻辑 CPU 上运行的
// 1000000000:测试函数执行的次数
// 0.50 ns/op:每次操作的平均时间(纳秒)
// PASS:测试通过
// 0.566s:整个基准测试的总耗时

// 如果要要看到内存分配情况,执行时加上 -benchmem
Slice 优化

①尽量在使用 make 初始化切片时提供容量信息
因为切片本质是一个数组片段的描述,包括数组指针、片段长度、片段容量,使用切片操作时并不复制切片指向的元素,而创建一个新的切片会复用原来切片的底层数组
②避免大内存得不到释放,使用 copy 代替 Slice
具体表现在在已有大切片的基础上创建小切片,因为没有创建新的底层数组,所以底层是相同数组,而原底层数组会因为在内存中存在引用,得不到释放,所以可以用 copy 代替 re-slice

Map 优化

也需尽量预分配内存,因为不断向 map 中添加元素的操作会导致 map 扩容,提前分配好空间可以减少内存拷贝和 Rehash 的消耗

String 优化

常见的字符串拼接方式,直接使用 + 拼接性能最差,使用 strings.Builder,bytes.Buffer 性能相近,但是 strings.Builder 速度更快
原因在于字符串在 Go 语言中是不可变类型,占用内存大小是固定的,使用 + 来进行字符串拼接时每次都会分配内存,而 strings.Builder,bytes.Buffer 底层都是 []byte 数组,使用的是内存扩容策略,不需要每次拼接重新分配内存

Struct 优化

使用空结构体节约内存,空结构体 struct{} 不占用任何内存空间,可以节省资源作为占位符使用
比如使用 map 来实现 set,只需要 map 的 key 值,对于 value 值可以用空结构体作为占位符,以此节省空间

Atomic 优化

使用 atomic 包来保证一些变量的原子性操作,这样的方式比使用 lock 会更高效,因为 atomic 是通过硬件实现的,而锁本质是通过操作系统的系统调用来实现的,而且使用 sync.Mutex + Lock 的方式是为了保护一段逻辑,而用于保护一个变量太过浪费

性能分析工具 pprof

(待补充)