青训营笔记(四)
这是我参与「第五届青训营 」笔记创作活动的第4天。本文将介绍一些高质量编程的注意事项以及代码性能优化的一些建议。
1.高质量编程
每个程序员都希望自己编写的代码能被称为高质量,那什么是高质量呢?高质量实际上是一个偏主观的概念,这里认为高质量是指代码能够达到正确可靠、简洁清晰的目标。
- 正确性是指各种边界条件是否考虑完备,错误调用能否处理;
- 可靠性是指异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够正确处理
- 简洁就是逻辑是否简单,后续调整功能或新增功能是否能快速支持
- 清晰是指其他人在阅读代码时能否清楚明白,重构或者修改功能会不会出现无法预料的问题
下面将从具体实践中编写代码时的几个方面介绍Go语言的编码规范
1.1代码格式
go语言中推荐使用gofmt自动格式化代码,保证所有Go代码与官方推荐代码格式一致,而且可以很方便的配置,在Goland中就内置了相关功能,在保存文档时编辑器就会对文件中的代码自动格式化。
此外还可以考虑goimports,会对依赖包进行管理,自动增删依赖的包引用并按字母顺序排列分类。
1.2注释
在编写代码时,注释也是很有必要的,合适的注释既能利于团队中的其他人能快速读懂你写的代码,有时也能帮助自己快速理清逻辑。一个好的注释应该能做到以下几点:
- 能解释代码的作用;
- 能解释代码如何做的;
- 解释代码实现的原因;
- 解释代码什么情况会出错。
公共符号始终要注释;包括变量、常量、函数以及结构都要注释,任何既不明显也不简短的公共功能必须要注释;无论长度或复杂度如何,对库中的任何函数都要注释。
1.3 命名规范
在编写代码中,最让程序员头痛的除了注释就是命名了。命名一般包括变量的命名和函数的命名,下面将分别介绍两种命名的注意事项。
变量命名:
- 对于变量命名要简洁胜于冗长。
- 缩略词要全大写,但其位于变量名开头且不需要导出时,使用全小写。如使用
SeverHTTP而不是SeverHttp;使用xmlHTTPRequest。 - 变量距离其被使用的地方越远,则需要携带越多的上下文信息;全局变量需要更多的上下文信息,使得在不同地方能轻易辨别。
函数命名:
- 函数名不携带包名的上下文信息,因为包名和函数名是成对出现的
- 函数名尽量简短。
- 当名为foo包中的某个函数的返回类型为Foo时,可以省略类型信息而不导致歧义
- 当名为foo包中的某个函数的返回类型为T时,可以在函数名中加入类型信息
包命名:
- 只由小写字母组成。不包含数字和下划线等字符。
- 简短并包含一定的上下文信息。如
schema、task等。 - 不要与标准库重名。如
fmt、sync等。
1.4 控制流程
在能够给变量和函数选择合适名称后,就要考虑具体的功能实现,我们常常会遇到if else语句,如下面这个例子,如果在if else中都包含return语句,那么可以将else省略。
//bad
if foo {
return x
}
else{
return nil
}
//good
if foo {
return x
}
return nil
此外,在控制流程中还应该保证正常代码的路径为最小缩进,即优先处理错误情况,能尽早返回或继续循环来减少嵌套。如下面的bad例子,函数中的流程被嵌套在两个if条件内,无论是否满足条件都要进入到两个if循环中,最后才返回错误。如果后续正常流程增加一步操作,调用新的函数又会增加一层嵌套,这无疑增加的时间的浪费。对该程序改进后就会先判断错误的条件,只有不属于错误就会返回正常流程的返回值。
// bad
func OnceFunc() error {
err := doSomthing()
if err == nil {
err := doAotherThing()
if err == nil {
return nil //normal case
}
return err
}
return err
}
// Good
func OnceFunc() error {
if err := doSomthing(); err != nil {
return err
}
if err := doAotherThing(); err != nil {
return err
}
return nil //normal case
}
1.5 错误和异常处理
简单错误
简单错误是指仅出现一次的错误,且在其他地方不需要捕获该错误,可以使用errors.New来创建匿名变量来直接表示错误。如下就是使用了errors.New来描述了一个简单错误的失败原因。
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
而对于复杂错误,有时不能简单的描述,errors包中提供了warp,warp可以将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.Is,若要获取特定种类的错误,可以使用errors.As。
在Go中,比错误更严重的的就是panic,它的出现表示程序无法正常工作。因此不建议在业务代码中使用painc。如果调用的的函数不包含recover会造成程序崩溃。如果问题可以被解决,建议使用error。当程序启动阶段发生不可逆转的错误,可以在init或main函数中使用painc。
2.性能优化
高质量的代码虽然能实现较好的功能,但在实际大规模程序部署场景,仅仅功能正常是不够的还需要尽可能提高性能,节约成本。评价性能主要从时间效率和空间效率评估,而时间和空间往往是对立的,此时需要分析侧重方向,做出适当的折中,才能有效地提高程序性能。
2.1 slice性能优化
slice是Go语言中常见的数据结构,与数组相类似,但可以随时扩展容量。对于使用slice时有以下几点建议:
- 在创建
slice时尽可能提供容量信息;创建新的切片会复用原来切片的底层数组,在切片追加元素时,如果追加元素数大于容量,会将原切片中元素拷贝到一片更大的区域中来扩容,这无疑产生了额外的内存分配。因此,最好预先设置切片的容量,避免产生额外的内存分配。 - 原切片由大量元素构成,占用较大内存,而使用时只切片一小段,但在底层数组内存中仍占用了大量内存。这里可以使用
copy来代替切片操作。
同样的,与切片类似的map也可以通过预分配内存来避免内存的浪费。
2.2 string优化
再编程中,字符串处理也是高频操作。在go语言中可以使用+来拼接两个字符串,但使用+会每次重新分配内存,造成内存浪费。这里可以使用strings包中的strings.Builder,bytes.Buffer,strings.Buffer来拼接字符串。
使用+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快。strings.Builder,bytes.Buffer其底层都是[]byte数组。同样的在创建字符串时可以通过预分配内存来提高性能。
2.3 空结构体
以上都是提高时间的优化方法,对于提高空间效率我们可以使用空结构体。空结构体占用内存更少,在元素多的情况最明显。
空结构体struct{}实例不占据任何内存空间,可以作为各种场景下的占位符使用。为了实现set,我们可以考虑使用map,但对于set我们只用到了键,没有用到value。这样就可以用将value设为空结构体
2.4 atomic包
对于多线程场景,如何保证线程安全是一个很重要的问题。可以使用锁来解决并发安全问题,但这里锁是通过操作系统来实现的,属于系统调用。而atmoic操作是通过硬件实现的,效率较高。对于非数值操作,可以使用atomic.Value,能承载一个interfac{}