这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记
1. 编程原则
实际场景千变万化,各种语言各不相同,但是高质量编程遵循的原则是相通的。
- 简单性
- 消除“多余的复杂性”,以简单清晰的逻辑写代码
- 不理解的代码无法修复改进
- 可读性
- 代码是写给人看的而不是机器看的
- 编写可维护的代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
2. 命名规范
2.1 变量的命名
- 缩略词全部大写,但当其位于变量开头而且不需要导出时,需要全小写
- 例如ServeHTTP而不是用ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
2.2 函数/方法的命名
- 函数名尽量不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名要尽量简短
- 当名为foo的包的某个函数返回类型foo时,可以省略类型信息而不导致歧义
- 当名为foo的包的某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息。
2.3 包的命名
只由小写字母组成。不包含大写字母和下划线等字符- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用 sync 或者 strings
3. 编码规范-控制流程
- 线性原理,处理逻辑尽量走直线,避免复杂的分支嵌套
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环嵌套中
4. 编码规范-错误捕获与处理
4.1 简单错误处理
- 简单的错误指的是仅出现一次的错误,而且在别的地方可以不用捕获该错误
- 优先使用errors.new("error info")这样的匿名消息来处理简单错误
- 如果有格式化的要求则使用fmt.Errorf 如:
fmt.Errorf("正在处理错误%d", 20036)
4.2 复杂错误处理
- 错误的warp实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链。如何理解跟踪链呢,也就是类似于JAVA里面异常处理的机制,当发生异常时,异常会一层层地向上抛,直到它被捕获并且处理。
- 在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中。
- 错误判定,判定一个错误是否为特定错误,使用Error.Is,不同于使用== ,使用该方法可以判定错误链上的所有错误是否含有特定的错误
- 特定错误获取,在错误链上获取特定种类的错误,使用errors.As
panic,不建议在业务代码中使用panic,调用函数不包含recover会造成程序崩溃,若问题可以被屏蔽解决,建议使用error代替panic- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
recover机制:只能在被defer的函数中使用,嵌套无法生效,只在当前的goroutine生效.defer语句:defer是后进先出的,我们来看一个例子
func test() {
if true {
defer fmt.Println("1")
} else {
defer fmt.Println("2")
}
defer fmt.Println("3")
}
//输出的结果是:
//3
//1
//代码解析:首先首先是先进到了1所对应的defer中,然后最后进到了3所对应的defer中,根据后进先出原则,先执行3,再执行1
5. 性能优化
5.1 性能优化帮助工具-Benchmark
go语言提供了支持基准性能测试的benchmark工具
在终端中执行go test -bench=. -benchmen即可启动
5.2 性能优化建议-slice使用
- 尽量在make slice时提供合适的容量信息以适应底层。
- 关于slice:其本质是数组片段的描述
- 包括数组指针
- 片段的长度
- 片段的容量(不改变内存分配情况下的最大片段长度),一旦长度超过该容量就会需要重新为切片分配内存
- 切片操作并不复制切片所指向的元素,而是在创建新切片时
复用原来的切片。- 陷阱:大内存未释放,在已有切片的基础上创建切片,不会创建新的底层数组
- 实例:创建了一个100MB的大切片,此时引用一个它的3MB的小切片,这时候内存中依然有对大切片的内存引用,大切片得不到释放。
- 策略:使用copy来代替re-slice
5.3 性能优化建议-map的使用
- 对于map的定义,我们有两种方式,一种是提前预设好map的容量,另一种是不提前预设它的容量
data := make(map[int]int)
data := make(map[int]int,size)
- 在向map中填充数据的时候,map会不断扩容,如果我们提前对map中的内存进行预分配,可以减少内存拷贝和rehash的消耗。
5.4 性能优化建议-stringbuilder的使用
程序中对于处理拼接字符串有以下三种策略:
- 直接使用
+进行拼接
func addStr_fir(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}
- 使用stringbuilder
func addStr_sec(n int, str string) string {
builder := strings.Builder{}
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
- 使用byte[]数组
func addStr_thr(n int, str string) string {
buf := new(bytes.Buffer)
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.String()
}
经过测试,性能最佳的使用stringBuilder的方法,最差的为直接使用加号拼接的,原因分析:
- 字符串在GO中是不可变类型占用内存的大小是固定,这一点和JAVA中的字符串管理是相同的,因此在使用+后,每次内存都会重新分配
- StringBuilder和StringBuffer其底层都是[]byte数组
- 内存扩容策略:不需要每次拼接都重新分配内存
- 为什么使用StringBuffer比StringBuilder要慢?
在拼接字符串时,其原理是一致的,差别在于最后,将底层的[]byte数组转化为string时存在差异,
- stringBuilder是直接将[]byte元素转化为字符串后返回的
- byte.Buffer是新开辟了一块内存空间,把这个字符存到这里面去,再返回的
- 支持内存预分配:如果已知未来拼接后的字符串的长度,可以通过
Grow()方法来为builder或者buffer预分配内存空间,从而进一步提高性能
5.5 性能优化建议-空结构体的使用
可以使用空结构体来节省内存
- 空结构体struct实例不占据任何的内存空间
- 可作为任何场景下的占位符使用
- 节省资源
- 不占据任何的内存空间,同时也具有很强的语义,仅作为占位符
5.6 性能优化建议-atomic的使用
对于传统的加锁互斥来保证并发安全,也可以通过调用atomic包内方法来保证并发安全,而且效率更高
type atomicCounter struct {
i int32
}
func atomicFunc(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
- 锁的实现是通过操作系统来实现的,属于系统调用
- atomic是通过硬件来实现的,效率比锁要搞
- sync.Mutex应该用来保护一段逻辑,不仅仅用来保护一个变量
- 对于非数值操作,可以用atomic.Value来操作,可以承载一个interface{}
6. 性能分析工具-pprof介绍与使用
6.1 分析指标
- 当flat == cum时,该函数没有调用其他函数
- 当flat = 0,该函数只调用了其他函数
- 使用
list 关键字可以来定位对应的代码行 - 使用
web可以来使用可视化对刚才的过程进行图解分析
6.2 火焰图的使用
- 火焰图:从上到下表示调用顺序
- 每一块代表一个函数,越长代表占用CPU的时间更长
- 火焰图是动态的,支持点击块进行分析