这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
01 本堂课重点内容
-
高质量编程
- 编码规范
- 常用Go语言程序优化手段
02 详细知识点介绍
2.1 高质量编程
-
写出更简洁清晰的代码
- 简洁:尽可能简单的逻辑——便于后续调整、新增功能
- 清晰:其余团队成员可以放心的重构、优化、新增功能
- 在工作中,编程是团队合作的工程,好的代码让其他人更容易在你的基础上开发,同时出问题的概率更低,大家更乐于与你合作,也让团队更高效
- 另外在面试的时候,也有编码环节,能不能用代码清晰的表达出你的思路,让面试官额外加分
-
写出正确可靠的代码
- 各种边界条件是否考虑完备
- 异常情况处理、稳定性保证
- 易读易维护
2.1.1 Dave Cheney的编程原则
-
简单性
- 消除”多余的复杂性“,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
-
可读性
-
代码是写给人看的,而不是机器
-
编写可维护代码的第一步是确保代码可读
-
在项目不断迭代的过程中,大部分工作是对已有功能的完善或扩展,很少会完全下线某个功能,对应的功能代码实际会生存很长时间。
- 已上线的代码在其生命周期内会被不同的人阅读几十上百次,听课时老师经常说的在课堂上不遵守纪律影响全班同学的时间,难以理解的代码会占用后续每一个程序员的时间
-
-
生产力
-
团队整体工作效率非常重要
-
为了降低新成员上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式
-
编码在整个项目开发链路中的一个节点,遵循规范,避免常见缺陷的代码能够降低后续联调、测试、验证、上线等各个节点的出现问题的概率,就算出现问题也能快速排查定位
-
-
2.1.2 实践编码规范
-
代码格式
- 使用gofmt自动格式化代码
- goimports = gofmt + 依赖包管理
-
注释
-
任何既不明显也不简短的公共功能(常量、方法)必须注释
-
无论长度或复杂度如何,库中的任何函数都必须进行注释
-
有一个例外,不需要注释实现接口的方法
-
注释应该做的
-
解释代码作用
-
代码实现的过程
-
代码实现的原因
- 外部因素
- 上下文
-
代码什么时候会出错
- 解释代码的限制条件
-
-
-
命名规范
-
变量
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头并且不需要导出的时候,全小写
- e.g. 使用ServerHTTP而不是ServerHttp,使用 XMLHTTPRequest 或 xmlHTTPRequest
-
变量距离被使用的地方越远,需要携带更多的context info
- e.g. 全局变量——以便在不同的地方可以认出含义
-
-
func
- 函数名不携带包名的上下文信息,因为包名和函数总是成对出现的
- 函数名尽量简短
- 当名为foo的包某个函数返回类型为Foo时,可以省略类型信息不导致歧义
- 当名为foo的包某个函返回数类型为T(T并不是Foo)时,可以在函数名中加入类型信息
-
package
- 由小写字母组成,不包括下划线
- 简短并包含一定上下文信息:e.g. schema、task
- 不要与标准库同名
- 不使用常用变量名作为包名:e.g. buffo而不是buf
- 使用单数而不是复数
-
-
控制流程
- 避免嵌套,保证流程清晰(线性原理)
- e.g. 如果两个分支中都包含return语句,则可以去除冗余的else
- e.g. 如果两个分支中都包含return语句,则可以去除冗余的else
- 优先处理错误/特殊情况
- 以尽早返回或继续循环来减少嵌套
- e.g.
- 避免嵌套,保证流程清晰(线性原理)
-
错误和异常处理
- 简单错误
- 仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用errors.New来创建匿名变量来直接表示简单错误
- 如果有格式化的需求,使用fmt.Errorf
- 错误的Wrap和Unwrap
- 错误的包装和解包
- 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
- fmt.Errorf中使用
%w来将一个错误关联至错误链中
- 错误判定
- 判定一个错误是否为特定错误:
errors.Is - 判定错误链上的所有错误是否含有特定的错误:
== - 在错误链上获取特定种类的错误:
errors.As
- 判定一个错误是否为特定错误:
- panic
panic用于主动抛出错误,recover用来捕获panic抛出的错误- 引发
panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出 - 不建议在业务代码中使用
panic - 如果问题可以被屏蔽或者解决,建议使用
error替代panic - 当程序启动阶段发生不可逆转的错误时,可以在
init或main函数中使用panic
- recover
- 只能在被
defer的函数中使用 - 只在当前
goroutine生效- 嵌套无法生效
defer是先进先出的
- 只能在被
- 简单错误
2.1.3 性能优化建议
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- 此时应当分析哪个更重要,作出适当的折衷。例如多花费一些内存来提高性能
- Benchmark
- 性能表现需要实际数据衡量
go test -bench==. benchmem
- Slice
- 预分配内存(容量),减少分配内存次数
- 切片操作并不复制切片指向的元素
- 创建一个 新的切片会复用原来切片 的底层数组
- 大内存未释放
- 在已有切片基础上创建切片,不会创建新的底层数组
- 场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
- 原切片较大,代码在原切片基础上新建小切片
- 预分配内存(容量),减少分配内存次数
- Map预分配内存
- 建议根据实际需求提前预估好需要的空间
- 字符串拼接
- 使用
+拼接性能最差,strings.Builder,bytes. Buffer相近,strings. Buffer更快bytes. Buffer转化为字符串时重新申请了一块空间strings. Builder直接将底层的[]byte转换成了字符串类型返回
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用
+每次都会重新分配内存 strings.Builder,bytes. Buffer底层都是[]byte 数组- 内存扩容策略,不需要每次拼接重新分配内存
- 使用
- 使用空结构体节省内存
- 作为占位符
- 使用map实现set,只会用到map的key,用不到value,value使用bool代替也有1个Byte
type Set map[string]struct{}
func (s Set) Has(key string) bool {
_, ok := s[key]
return ok
}
func (s Set) Add(key string) {
s[key] = struct{}{}
}
func (s Set) Delete(key string) {
delete(s, key)
}
func main() {
s := make(Set)
s.Add("Tom")
s.Add("Sam")
fmt.Println(s.Has("Tom"))
fmt.Println(s.Has("Jack"))
}
- atomic包
- 在工作中迟早会遇到多线程编程的场景,比如实现一个多线程共用的计数器,如何保证计数准确,线程安全
- atomic包比加锁的方式开销更小
- 锁的实现是通过操作系统来实现,属于系统调用
atomic操作是通过硬件实现,效率比锁高sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量- 对于非数值操作,可以使用
atomic.Value,能承载一个interface{}