Go语言编码规范和性能调优
编码规范
注释
Good code has lots of comments,bad code requires lots of comments.
不必要的注释
场景一
如上图所示,第一个Open函数应该解释代码作用,而第二个函数这样的作用解释则毫无必要,因为它的函数名就已经解释了。
场景二
第一个函数的逻辑较为复杂,很多情况没法看懂,需要注释,而第二个则完全没必要。
需要的注释
公共符号始终要注释
这里的公共符号包括全局可见的函数和变量,而方法则不包含在内。
小结
- 代码是最好的注释。
- 注释应提供代码未表达出的上下文信息。
变量命名
- 简洁
- 缩略词都大写,比如HTTP不要Http
- 变量定义的位置距离使用的地方越远,命名需要越详细,特别是全局变量,有时需要注释
函数内
例如for循环时:
for i:=0; i<size;i++{ //good
...
}
for index:=0;index<size;index++{//bad
...
}
而作为函数参数时:
//good
func (c *Client) send(req *Request, deadline time.Time)
//bad
func (c *Client) send(req *Request, t time.Time)
由于第一个变量仅在for循环这个作用域内,它的作用也很清晰,如果命名的更详细,反而会影响阅读。
第二个变量作为函数的参数,作用域很大,且需要作为提供给使用者的名字,这个需要带有详细信息描述。
包内
- 函数名应该不携带报名的上下文信息。
例如在http包中有个Serve方法和ServeHTTP方法这两个命名,我们应该选择Serve去命名而不是ServeHTTP。因为我们使用的时候会携带包名。类比于C++的命名空间,Java的包名。
包命名
- 只有小写字母组成(不包含下划线等字母
- 简短并包含一定的信息
- 不要和标准库的包冲突,标准库的很多包名喜欢用复数,我们应该避免使用复数形式为包名,比如strings是标准库的一个包名。
控制流程
这里我用之前在其他地方学到的两个优化代码的方式来讲。
嵌套条件校验链
嵌套两层以上的if
if a>10 {
if b>10 {
if c>10 {
...
}
}
}
优化如下:
for {
if !(a>10) {
break
}
if !(b>10) {
break
}
if !(c>10) {
break
}
...
}
互斥条件表驱动
比如有以下并列的if嵌套逻辑:
func CalculateByCmd(cmd string,a,b int)(int,error){
if strings.EqualFold(cmd,"add"){
return a+b,nil
}
if strings.EqualFold(cmd,"sub"){
return a-b,nil
}
if strings.EqualFold(cmd,"mul"){
return a*b,nil
}
return 0,errors.New("cmd not exist")
}
这段代码是根据给出的字符串命令,得出对应的计算结果。但是我们发现,这个代码的可读性虽然还行,但由于最终的计算和这个函数的耦合性太强,实现功能拓展有点拖后腿。
我们通过表驱动做出以下优化:
var mapCalculate = map[string]func(a,b int) int{
"add": func(a, b int) int {
return a+b
},
"sub": func(a, b int) int {
return a-b
},
"mul": func(a, b int) int {
return a*b
},
}
func CalculateByCmd(cmd string,a,b int)(int,error){
if v,ok := mapCalculate[cmd];ok{
return v(a,b),nil
}
return 0,errors.New("cmd not exist")
}
错误处理
error相关的函数
errors.New()
return errors.New("需要的错误信息描述")
errors.Is()
用于判断错误断言,不同于简单的==,它能够判断错误链中是否包含它。
errors.As()
从错误链中提取想要的错误。
panic和recover和defer
这几样东西,语法就那样,真要理解原理可以看看下面这些视频链接。
性能调优
benchmark测试
这个benchmark,之前在(二)里面讲了如何去使用,这里直接贴图看如何看懂测试结果。
slice的阴暗面
对于之前使用C++的同学,这里slice的预分配应该不用多讲。主要就是学会避坑。
最大坑点
- 更新slice后持有的底层数组相同。 具体而言:有时我们只是想要底层数组的一小部分,结果因为简单切个片,然后就和他共用了同一片底层数组。go的垃圾回收机制在某种程度上和C++智能指针(引用计数)很相似,如果此时大的数组实际上已经没用了,而有用的只有小数组,而它们是共用同一片底层数组,此时这整个底层数组的空间会得不到释放,因为引用计数不为零!算是意外延长了生命周期。
- 更新slice后持有的底层数组不同。 具体而言:如果一个map映射的值是slice类型,那么我们每次更新这个slice里的元素时,我们还得更新map映射的这个slice,这是为了防止底层数组发生了变化。所以一般slice作为值最好是使用slice指针。
关于string可变与不可变的优化
不可变string的优化
go语言和Java等等语言都把string设置为了不可变,我觉得这样设置是非常合理的,毕竟字符串在使用过程中,多数情况下都是传递,而且可以利用不可变做很多优化,比如内存池之类的。
string不可变的缺陷
一旦string不可变,就意味着,每次得到一个新的字符串就需要申请一片新的内存,那么这样的话,多次字符串拼接的过程中将会有严重的性能损失!
如下,每次 +=
右边的值都会引起内存的分配。
s := "aa"
for i:=0;i<size;i++{
s += "ccc"
}
解决方案
- 使用strings.Builder。
var s strings.Builder
for i:=0;i<size;i++{
s.WriteString("cc")
}
s.String()
- 使用bytes.Builder。
var s bytes.Buffer
for i:=0;i<10;i++{
s.WriteString("cc")
}
s.String()
第一种解决方案比第二种要快。
因为在最后的String()阶段bytes包的处理方式是再进行一次切片处理。
而strings包则是直接指针强转。
可变string的优化
可变string的代表便是C++。
C++的string是可变的,而且因为实现了重载=号实现的深拷贝,导致很多情况下string的使用是需要进行拷贝的,但为了解决这个问题C++可以使用 const&
来引用字符串以防止拷贝,但这就有一个问题:由于string是可变的万一我在使用的过程中string被外界改变了怎么办?这是可变string需要面临的最大问题,所以可变string一般都是会实现深拷贝,但使用起来大多数人还是会通过引用传递,毕竟拷贝太耗时了!但这时使用起来就得遵守编码规范唯唯诺诺了。
回到正题,即便是使用了 const&
进行字符串的传递,从实践来看,它至少有以下几方面问题:
- 字符串字面值、字符数组、字符串指针的传递仍要数据拷贝 这三类低级数据类型与
string
类型不同,传入时,编译器需要做隐式转换,即需要拷贝这些数据生成string
临时对象。const string&
指向的实际上是这个临时对象。通常字符串字面值较小,性能损耗可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,会严重影响程序的性能。 substr
O(n)复杂度 这是一个特别常用的函数,好在std::string
提供了这个函数,美中不足的是其每次都返回一个新生成的子串,很容易引起性能热点。实际上我们本意并不是要改变原字符串,为什么不在原字符串基础上返回呢?
说说可变字符串类型的好处,内存的开辟问题不会太多,因为虽然在拷贝,但基本上都是复用的同一片内存。
解决方案
方案一:SSO优化
SSO,全称为:小字符串优化。
这个优化简单粗暴,就是根据字符串的长度来决定内存的开辟情况。
比如字符串长度如果小于128字节,那么内存就开辟在栈上面,众所周知,栈内存开辟比堆内存开辟的代价小很多!
方案二:string_view(C++17引入)
通过提供一个新的类型,这个类型和不可变的字符串的类型类似,它是不可变的,只能看,不然怎么叫view🤭
每次string赋值给它,代价都很小,不是直接拷贝字符串,而是指针的赋值而已。
而且string_view重写了substr,这个方法返回string_view而且你会发现通过它再构建string性能会比string调用substr快很多。
string_view虽然解决了拷贝问题,但是依旧没有解决C++的内存安全问题,string_view内部是原始指针,不会意外延长生命周期,所以要非常注意它所观察的字符串内存是否被释放了,如果被释放string_view将失效,将会产生严重的内存安全问题。
关于string_view,可以看看我的这篇博客string_view。
空结构体的使用
空结构体,不占内存,仅作为占位符,所以可以作为map实现set的理想工具。
atomic包
用atomic保护变量的并发安全,用sync.Mutex保护一段代码逻辑的并发安全。
对于非数值变量,可以使用atomic.Value来承载一个空接口。
性能调优实战
pprof工具的使用
这个东西暂时用不来,感觉暂时这个阶段也很难用上。用上了再研究(主要是感觉很多东西都还完全不会分析,只会工具操作很麻木的感觉