高质量编程
代码格式
使用gofmt和goimports等工具自动格式化代码,让项目整体代码风格保持统一
注释
- 解释代码作用
- 注释需要描述代码片段所使用的公共符号(变量、常量、函数、结构)的含义和基本的使用说明
- 多处使用的公共功能
- 对所有函数
- 解释代码如何做的:注释需要描述代码片段实现过程
- 解释代码实现原因:主要是提供额外的上下文解释影响代码实现的原因和思路
- 解释代码什么情况下会出错:描述代码的边界情况和限制条件
命名规范
- 在保持命名语义的前提下尽可能简洁
fmt - 缩略词全大写
ServerHTTP,位于变量开头且不需要导出时全小写xmlHTTPRequest - 距离被使用的地方越远命名时需要提供越多的上下文信息,特别是全局变量
- 对于函数名命名
- 不用携带包名的上下文信息,因为使用时一般为
package.function - 在保持命名语义的前提下尽可能简洁
- 返回值类型和包名相同时命名可以省略类型信息
- 返回值类型和包名不同时在函数名中加入类型信息
- 不用携带包名的上下文信息,因为使用时一般为
- 对于包名命名
- 只包含小写字母(不包含大写字母和下划线)
- 简洁但包含一定上下文信息
- 不能与标准库同名
- 尽量满足
- 不使用常用变量名,如
buf作为包名 - 使用单数
- 谨慎使用缩写
- 不使用常用变量名,如
控制流程
- 在条件判断分支较多时,需要先进入异常逻辑判断和处理,保证主流程的代码路径为最小缩进
- 避免多层嵌套,保证代码流程清晰
错误和异常处理
- 简单错误
- 是指只在当前位置出现,不需要在其他地方捕获
- 使用
errors.New("")创建匿名变量 - 有格式化要求使用
fmt.Errorf
- 错误链
- 在
fmt.Errorf中使用%w将一个错误关联至错误链中 - 使用
errors.Is()来比较一个错误是否是指定错误,这种方式比直接字符串比较要好,可以判定错误链上的所有错误是否含有特定的错误 - 使用
errors.As()在错误链上获取指定种类的错误 - 使用
errors.Unwrap检索错误链中的下一个错误,可用于遍历错误链package main import ( "errors" "fmt" ) func main() { err := foo() for err != nil { fmt.Println(err) // foo: bar: baz // bar: baz // baz err = errors.Unwrap(err) } } func foo() error { return fmt.Errorf("foo: %w", bar()) } func bar() error { return fmt.Errorf("bar: %w", baz()) } func baz() error { return errors.New("baz") }
- 在
- panic
- 调用函数不包含
recover时调用panic()会中断程序运行 - 如果错误可以跳过或者屏蔽,应尽量使用
error代替panic - 在程序的启动阶段发生不可逆转的错误时,在
init或main中使用panic
- 调用函数不包含
- recover
recover只能在被defer的函数中使用- 嵌套无法生效
- 只在当前的
goroutine生效 defer语句是后进先出
性能优化
Benchmark
测试命令go test -bench=. -benchmem
测试结果
BenchmarkFib10-8 1855870 602.5 ns/op 0 B/op 0 allocs/op
- 测试函数名(-8表示GOMAXPROCS,默认为CPU核数)
- 总共执行测试次数,为
b.N的值 - 每次执行花费时间
- 每次执行申请内存大小
- 每次执行申请内存次数
优化建议
- 在使用
make()初始化切片时提供容量信息make([]int, 0, size) - 不要在已有大切片上新建小切片,这种方式不会创建新的底层数组,实际上是对元切片的引用,原切片得不到释放
- 使用
copy得到新切片copy(new_s, origin[len(origin)-2:]) - 在使用
make()初始化map时提供容量信息make(map[int]int, size) - 针对字符串处理,使用
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() } strings.Builder也能提前指定容量var builder strings.Builder builder.Grow(n * len(str))- 使用空结构体可以节省内存
- 空结构体
struct{}实例不占据任何的内存空间 - 可作为各种场景下的占位符使用,不需要任何值,仅作为占位符,比如使用
map实现set时,只需要键,不需要值,可以使用空结构体占位
- 空结构体
- 针对单个变量的锁保护使用
atomic包代替sync.Mutextype atomicCounter struct { i int32 } func AtomicAddOne(c *atomicCounter) { atomic.AddInt32(&c.i, 1) }-
锁的实现通过操作系统,属于系统调用
-
atomic操作是通过硬件实现,效率比锁高 -
sync.Mutex应该用于保护一段逻辑,而不是单个变量 -
对于非数值操作,可以使用
atomic.Value,能承载一个interface{}
-
pprof工具调优实践
pprof工具调优过程
使用go tool pprof "http://localhost:6060/debug/pprof/profile? seconds=10"收拾数据
CPU指标优化
-
使用
topN命令查看占用资源最多的函数flat:当前函数本身的执行耗时flat%:flat占CPU总时间的比例sum%:上面每一行的flat%的总和cum:当前函数本身加上其调用函数的总耗时cum%:cum占CPU总时间的比例
-
使用
list xxx根据指定正则表达式查找代码行 -
使用
web实现调用关系可视化
Heap(堆内存)指标优化
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"打开分析网页- 通过
VIEW-Graph标签查看调用关系图 - 通过
VIEW-Source标签查看源码视图 - 采样条件,一般查看
alloc,因为程序可能释放,需要查看累计值alloc_objects:程序累计申请的对象数alloc_space:程序累计申请的内存大小inuse_objects:程序当前持有的对象数inuse_space:程序当前占用的内存大小
goroutine协程、mutex锁、block阻塞
- `go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
- `go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
- `go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
- 一般是
view->source的查看顺序