高质量编程
简介
什么是高质量?
答:编写的代码能够达到正确可靠、简洁清晰的
目标可称之为高质量代码,考虑以下三点:
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
实际应用场景千变万化 , 各种语言的特性和语法各不相同
但是高质量编程遵循的原则是相通的
- 简单性
- 消除 “ 多余的复杂性 " , 以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
- 可读性
- 代码是写给人看的 , 而不是机器
- 编写可维护代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
编码规范
如何编写高质量的 Go代码
- 代码格式
- gofmt 推荐使用 gofmt 自动格式化代码, Go 语言官方提供的工具 , 能自动格式化 Go 语言代码为官方统一风格
- goimports 也是 Go 语言官方提供的工具,实际等于 goftt 加上依赖包管理,自动增删依赖的包引用,将依赖包按字母序排序并分类
- 注释
注释应该做的:
- 注释应该解释代码作用:对公共符号做出解释以表明作用,这样别人直接调用就可以不需要了解实现
- 注释应该解释代码如何做的:对一些晦涩的代码解释实现过程方便别人查看?
- 注释应该解释代码实现的原因:为非常规的代码提供xontext上下文信息以解释为什么怎么做
- 注释应该解释代码什么情况会出错:描述代码的用例,方便测试,解释代码的鲁棒性
Good code has a lots of comments, bad code requires a lot of comments 好的代码有很多注释 , 坏代码需要很多注释
Dave Thomas and Andrew Hunt
公共符号始终要注释:
- 包中声明的 每个公共的符号:变量、常量、函数以及结构都需要添加注释
- 可既不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
(实现接口的方法不需要注释,接口应当能详尽地描述方法的功能)
还有一点是,如果代码块联系比较密切,注释可以共同写在最上层区域,比如一个类的方法一般写在结构体附近,文档就可以一起写在结构体上方,个人理解
"代码就是最好的注释,但是你还是需要注释来提供一些context"
- 命名规范
变量
- 简洁胜于冗长
- 缩略词全大写 , 但当其位于变量开头且不需要导出时 , 使用全小写
- 例如使用 ServeHTTP 而不是 ServeHttp
- 使用 XMLHTTPRequest 或者 xmlHTTPRequest
- 变量距离其被使用的地方越远 , 则需要携带越多的上下文信息(比如,全局变量在其名字中需要更多的上下文信息 , 使得在不同地方可以轻易辨认出其含义)
函数
- 函数名不携带包名的上下文信息 , 因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为 foo 的包某个函数返回类型 Foo 时 , 可以省略类型信息而不导致歧义
- 当名为 foo 的包某个函数返回类型 T 时 (T 并不是 Foo) , 可以在函数名中加入类型信息
包
- 只由小写字母组成 。 不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息 。 例如 schema 、 task 等
- 不要与标准库同名 。 例如不要使用 sync 或者 strings
以下规则尽量满足 , 以标准库包名为例
- 不使用常用变量名作为包名 。 例如使用 bufio 而不是 buf
- 使用单数而不是复数 。 例如使用 encoding 而不是 encodings
- 谨慎地使用缩写 。 例如使用 fmt 在不破坏上下文的情况下比 format 更加简短
总之, 命名的核心目标是,降低阅读理解代码的成本,需要重点考虑上下文信息,设计简洁清晰的名称
又是一句quote:
Good naming is like a good joke. If you have to explain it, it's not funny
by Dave Cheney
- 控制流程
- 避免嵌套,保持正常流程清晰,比如 if-else两个分支都包含 return语句,则可以去除冗余的 else
- 尽量保持正常代码路径为最小缩进,意思是 return nil 直接写在函数作用域最后表示流程无异常,不要和判断异常的语句混在一堆嵌套里
原则:
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
- 错误和异常处理
- 简单错误
简单的错误指的是仅出现一次的错误 , 且在其他地方不需要捕获该错误,优先使用 errors.New 来创建匿名变量来直接表示简单错误,如果有格式化的需求 , 使用 fmt.Errorf eg
func defaultCheckRedirect(req *Request,via []*Request) error {
if len(via)>=10 {
return errors.New("Opps!stopped after 10 redirects")
}
return nil
}
- 输出错误链
错误的 wrap 实际上是提供了一个 error 嵌套另一个error 的能力 , 从而生成一个 error 的跟踪链,在 fmt.Errorf 中使用 %w 关键字来将一个错误关联至错误链中 eg
list, _, err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles lsit : %w",err)
}
- 错误判定
判定一个错误是否是特定错误,使用 errors.Is , 不同于使用 == ,使用该方法可以判定错误链上的所有错误是否含有特定的错误;在错误链上获取特定种类的错误,使用 errors.As
注意点:
- error 尽可能提供简明的上下文信息链 , 方便定位问题
- panic 用于真正异常的情况
- recover 生效范围 , 在当前 goroutine 的被 defer 的函数中生效
性能优化建议
简介
- 性能优化的前提是满足正确可靠 、 简洁清晰等质量因素
- 性能优化是综合评估 , 有时候时间效率和空间效率可能对立
- 针对 Go 语言特性 , 介绍 Go 相关的性能优化建议
使用 go提供的 benchmark做基准性能测试
优化建议
- 尽可能在使用 make() 初始化切片时提供容量信息
- map也要提供size信息预分配内存,都是为了尽可能避免扩容操作
- 使用 string.Builder或者 string.Buffer 来进行长字符串的拼接,而不是加号
- 使用空结构体节省内存 (空结构体实例不占据任何内存空间,可作为各种场景下的占位符使用,比如使用 map实现 set.......)
- 使用 atomic包 (对变量的原子操作)
总结:
- 避免常见的性能陷阱可以保证大部分程序的性能
- 普通应用代码 , 不要一味地追求程序的性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠 、 简洁清晰的质量要求的前提下提高程序性能