这是我参与「第五届青训营」伴学笔记创作活动的第 4 天
重点概览
- 高质量编程简介
- 编码规范
- 性能优化建议
- 性能调优简介
详细介绍
高质量编程
高质量的定义:
编写的代码能够达到正确可靠、简洁清晰的目标。
- 正确性:是否考虑各种边界条件,错误的调用是否能够处理
- 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
- 简洁:逻辑是否简单,后续调整功能或新增功能是否能够快速支持
- 清晰:其他人在阅读代码的时候是否能清楚明白,重构或修改功能是否不用担心出现无法预料的问题
高质量的代码并不仅仅局限于哪一门语言或者哪一个工程,而应当是作为一个coder的基本素养。
- 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码,因为不好理解的代码无法修复改进
- 可读性:代码是写给人看的,而不是机器,编写可维护代码的第一步是确保代码可读
- 生产力:团队整体的工作效率非常重要,为了减低新员工上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式。
编码规范
如何编写高质量的Go代码?需要注意:
-
工具:
- gofmt,Go官方提供的工具,自动格式化代码
- goimports也是Go官方提供的工具,实际等于gofmt加上依赖包管理 自动增删依赖的包引用、将依赖包按字母序排序并分类
-
注释:Good code has lots of comments, bad code requires lots of comments.
-
注释应该解释代码作用
-
注释应该解释代码如何做的
-
注释应该解释代码实现的原因
-
注释应该解释代码什么情况会出错
-
公共符号始终要注释
- 包,中声明的每个公共的符号: 变量、常量、函数以及结构 都需要添加注释
- 任何既不明显也不简短的公 共功能必须予以注释
- 无论长度或复杂程度如何 对库中的任何函数都必须进 行注释
- 有一个例外,不需要注释实现接口的方法
-
-
命名规范:
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 使用
ServeHTTP而不是ServeHttp - 使用
XMLHTTPRequest而不是xmlHTTPRequest
- 使用
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息(要更具体
-
函数名不携带包名的上下文信息且尽量简短
-
package名只由小写字母组成
-
-
控制流程:
- 避免嵌套,保持正常流程清晰,例如去掉不必要的
else - 尽量保持正常代码路径为最小缩进,能对称就对称
- 故障问题的大多出现在复杂的条件语句和循环语句中,尽量化简
- 避免嵌套,保持正常流程清晰,例如去掉不必要的
-
错误和异常处理:
-
简单错误:指仅出现一次的错误,且在其他地方不需要捕获该错误
-
优先使用
errors.New来创建匿名变量来直接表示简单错误 -
如果有格式化需求,请使用
fmt.Errorf -
func defaultCheckRedirect(req *Request, via []*Request) error { if len(via) >= 10 { // 使用errors.New return errors.New("stopped after 10 redirects.") } return nil // 去掉不必要的else } 复制代码 -
错误的
Wrap与Unnwrap:- 错误的
Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error跟踪链 - 在
fmt.Errorf中使用%w关键字来将一个错误关联至错误链中 -
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles")) if err != nil { return fmt.Errorf("reading srcfiles list: %w", err) } 复制代码
- 错误的
-
错误判定:
-
判定一个错误是否为特定错误,用
errors.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误data, err = lockedfile.Read(targ) if errors.Is(err, fs.ErrNotExist) { return []byte{}, nil } return data, err -
在错误链上获取特定种类的错误,使用
errors.Asif _, err := os.Open("non-existing"); err != nil { var pathError *fs.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } } 复制代码
-
-
panic:比错误更严重,表示程序无法正常工作,在业务代码中不建议使用,故不展开介绍。
-
recover:与panic对应,如果需要更多的上下文信息可以在recover后在log中记录当前的调用栈
生效条件:
- 只能在被defer的函数中使
- 嵌套无法生效
- 只在当前goroutine生效
- 注意defer是一个栈
-
内容补充:一个
defer语句就是一个普通的函数或方法调用。延迟调用函数,f()函数返回时才会调用。defer语句保证了不论是在正常情况下 (return 返回), 还是非正常情况下 (发生错误, 程序终止),函数或方法都能够执行。如果一个函数中注册了多个
defer函数,这些函数会按照后进先出的顺序执行 (和栈的出栈顺序一致)。也就是最后注册的 defer 函数会第一个执行,而第一个注册的defer函数会最后执行。
性能优化建议
简介:
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立,所谓时间换空间空间换时间
Benchmark
性能表现需要实际数量来衡量,Go语言提供了支持基准性能测试的benchmark工具
// 一个例子
// from fib.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n - 1) + Fib(n - 2)
}
// from fib_test.go
func BenchmarkFib10(b *testing.B) {
// run the Fib funciton b.N times
for n := 0; n < b.N; n++ {
Fib(10
}
}
复制代码
通过go test -bench=. -benchmen来进行测试
运行结果说明:
Slice预分配内存
尽可能在使用make()初始化切片的时候就提供容量信息,执行时间会差很多
究其原因是因为
- 切片本质是一个数组片段的描述包括数字指针、片段的长度以及片段的容量
- 切片操作并不复制切片指向的元素
- 创建一个新的切片会复用原来切片的底层数组
另一个陷阱:大内存未释放
有一种情况,原切片由大量元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量的空间,得不到释放。
这个时候我们可以用copy代替re-slice
Map预分配内存
与Slice相似地,如果初始化size也可以很大程度上优化性能,分析如下:
- 不断向map中添加元素会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
strings.Builder
在字符串拼接的过程中,使用strings.Builder往往比直接+要快,分析如下:
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用
+每次都会重新分配内存 strings.Builder, bytes.Buffer底层都是[]byte数组- 内存扩容策略,不需要每次拼接重新分配内存
// 一个strings.Builder例子
func StrBuilder(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
复制代码
空结构体
使用空结构体节省内存,分析如下:
-
空结构体
struct{}实例不占据任何的内存空间 -
可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,不需要任何值,仅作为占位符
// 一个例子
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
func BoolMap(n int){
m := make(map[int]bool)
for i := 0; i < n; i++ {
m[i] = false
}
}
// 比较性能即可
复制代码
atomic包
即原子变量与原子操作;
- atomic提供的原子操作能够确保任意时刻只有一个goroutine对变量进行操作
- 善用atomic能够避免程序中出现大量的锁操作
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率显然高
atomic的常见操作:
- 增减
- 载入 read
- cas
- 交换
- 存储 write
// 一个例子
var x int32 = 100
// atomic内部是一个compare ans swap, 简称cas, 会在加减操作之前先比较old new两个值再进行操作
// 而sync.Mutex应该用于保护一段逻辑,而不是仅仅一个变量
func f_add() {
atomic.AddInt32(&x, 1)
}
func f_sub() {
atomic.AddInt32(&x, -1)
}
func main() {
for i := 0; i < 100; i++ {
f_add()
f_sub()
}
fmt.Printf("x: %v\n", x)
}
// 对于非数值操作,可以使用atomic.Value,能承载一个interface{}