这是我参与「第五届青训营」伴学笔记创作活动的第 3 天
go高质量编程
编程原则
简单性
- 消除多余的复杂性,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
生产力
- 团队整体工作效率非常重要
编码规范
代码格式
可以使用 gofmt 和 goimports 工具自动格式化代码。对于 GoLand,可以在 Settings - Tools - Actions on Save 中打开 Reformat code 和 Optimize imports(不出意外的话,它们应当是默认启用的)。
注释
- 注释应该解释代码作用
- 注释应该解释代码如何做的
- 注释应该解释代码实现的原因
- 注释应该解释代码什么情况会出错
- 公共符号始终要注释
命名规范
变量(variable)
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
例如:
for index := 0; index < len(s); index++ {
// do sth.
}
// Good
for i := 0; i < len(s); i++ {
// do sth.
}
i和index的作用范围仅限于for循环内部时,index的额外冗长几乎没有增加对程序的理解
func (c *Client) send(req *Request, deadline time.Time)
// Bad
func (c *Client) send(req *Request, t time.Time)
将 deadline 换成 t 降低了变量名的信息量。前者特指截止时间,而后者仅是代表某个时间,故替换不合适
函数(function)
- 函数名不应携带包名的上下文信息,因为两者总是成对出现;
- 函数名应当尽量简短;
- 当名为
foo包的某个函数返回类型为Foo时,可以省略类型信息而不导致歧义; - 当名为
foo包的某个函数返回类型为T而不是Foo时,应该在函数名中加入类型信息。
举例如下:
// Bad
func ServeHTTP(I net.Listener, handler Handler) error
// Good
func Serve(I net.Listener, handler Handler) error
在调用http包的Server方法时,代码是http.Server,携带有包名,故不需额外添加包名信息
包(package)
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用 sync 或者 strings
总体来说,命名的大多数规范核心在于考虑上下文,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本。
编码规范
控制流程
- 避免嵌套,保持正常流程清晰
- 如果两个分支中都包含 return 语句,则可以去除冗余的 else
- 尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性 举例如下:
if foo {
return x
} else {
return nil
}
// Good
if foo {
return x
}
return nil
错误和异常处理
- 优先使用 errors.New 来创建匿名变量来直接表示该错误。有格式化需求时使用 fmt.Errorf
- fmt.Errorf 中使用 %w 关键字来将一个错误 wrap 至其错误链中
- 使用 errors.Is 可以判定错误链上的所有错误是否含有特定的错误
- 在错误链上获取特定种类的错误,使用 errors.As
- 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
- recover 只能在被 defer 的函数中使用,嵌套无法生效,只在当前 goroutine 生效
性能优化
- 使用 Benchmark 进行基准测试
- 在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时,即尽可能为 slice 和 map 预分配内存
- 多个字符串拼接时,使用
strings.Builder比直接使用+或使用bytes.Buffer更快 - 当需要占位符时,可使用空结构体(
struct{})代替 - 使用
atomic包代替锁修改变量