本节课讲了如何写出更简洁清晰的代码,每种语言都有自己的特性,也有自己独特的代码规范,对于 Go 来说,有哪些性能优化的手段、趁手的工具,也都进行了介绍。
高质量代码需要具备正确可靠、简洁清晰的特性
提到编码规范就不得不提到代码格式化工具,推荐使用Go官方提供的格式化工具 gofmt
,Goland中内置了其功能,常见的IDE也都能方便的配置
js中也有类似的格式化工具
Prettier
,可以配合ESLint进行代码格式化。
好的注释需要
解释代码作用
解释复杂、不明显的逻辑
解释代码实现的原因(这些因素脱离上下文后很难理解)
解释代码什么情况会出错(解释一些限制条件)
解释公共符号的注释(包中声明的每个公共的符号:变量、常量、函数以及结构等)
Google Style 指南中有两条规则:
- 任何既不明显也不简短的 公共功能 必须予以注释。
- 无论长度或复杂程度如何,对 库 中的任何函数都必须进行注释
而需要避免的情况如下:
总而言之,代码是最好的注释
简洁胜于冗长
i
和 index
的作用范围,不需要 index
的额外冗长// Bad
for index := 0; index < len(s) ; index++ {
// do something
}
// Good
for i := 0; i < len(s); i++ {
// do something
}
缩略词全大写,但当其 位于变量开头且不需要导出 时,使用全小写
ServeHTTP
而不是 ServeHttp
XMLHTTPRequest
或 xmlHTTPRequest
变量名距离其被使用的地方越远,则越需要携带越多的上下文信息。
// Bad
func ( c *Client ) send( req *Request, t time.Time )
// Good
func ( c *Client ) send( req *Request, deadline time.Time )
函数名 不携带包名的上下文信息,因为包名和函数名总是成对出现的
Serve
> ServeHTTP
,因为调用时总是http.Serve
函数名 尽量简短
当名为 foo
的包某个函数返回类型 T
时(T
并不是 Foo
),可以在函数名中加入返回的类型信息
Foo
类型时,可以省略而不导致歧义schema
、 task
等sync
或者 strings
以下规则尽量满足,以标准库包名为例:bufio
而不是 buf
encoding
而不是 `encodings``fmt
在不破坏上下文的情况下比 format
更加简短总的来说,好的命名降低阅读理解代码的成本,可以能让人把关注点留在主流程上,清晰地理解程序的功能,而不是频繁切换到分支细节,并且必须解释它。
避免嵌套,保持正常流程清晰可读
// Bad
if foo {
return x
} else {
return nil
}
// Good
if foo {
return x
}
return nil
// Bad
func OneFunc() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil // normal case
}
return err
}
return err
}
// Good
func OneFunc() error {
if err := doSomething(); err != nil {
return err
}
if err := doSomething(); err != nil {
return err
}
return nil // normal case
}
总而言之,程序中流程这一块处理逻辑尽量走直线,避免复杂的嵌套分支,使正常流程代码沿着屏幕向下移动。提升代码可维护性和可读性,因为故障问题大多出现在复杂的条件语句和循环语句中
简单错误
errors.New
来创建匿名变量来直接表示简单错误fmt.Errorf
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
复杂错误:使用错误的 Wrap
和 Unwrap
Wrap
实际上是提供了一个 error
嵌套另一个 error
的能力,从而生成一个 error
的跟踪链fmt.Errorf
中使用 %w
关键字来将一个错误关联至错误链中errors.Is
判定错误是否为某特定错误,可判定错误链上的所有错误(go/wrap_test.go · golang/go)errors.As
在错误链上获取特定种类的错误,并将错误赋值给定义好的变量。(go/wrap_test.go · golang/go)在Go中,比错误更严重的就是 panic
,它的出现表示程序无法正常工作了
不建议在业务代码中使用panic
panic
发生后,会向上传播至调用栈顶recover
会造成整个程序崩溃。error
代替 panic
当程序启动阶段发生不可逆转的错误时,可以在 init
或 main
函数中使用 panic
(sarama/main.go · Shopify/sarama)
有painc
,自然就会提到 recover
,如果是引入其它库的bug
导致panic
,影响到自身的逻辑时,就需要recover
recover
只能在被 defer
的函数中使用,嵌套无法生效,只在当前goroutine 生效(github.com/golang/go/b…)error
要尽可能提供简明的上下文信息链,方便定位问题panic
用于真正异常的情况recover
生效范围,在当前 goroutine 的被 defer
的函数中生效针对 Go 语言特性,课上介绍了很多 Go 相关的性能优化建议:
使用make() 初始化切片时尽可能提供容量信息
func PreAlloc(size int) {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
这是由于切片本质是一个数组片段的描述,包括数组指针、片段的长度、片段的容量(不改变内存分配情况下的最大长度),
使用 strings.Builder
常见的字符串拼接方式
+
进行连接 (最慢)
strings.Builder
(最快)
bytes.Buffer
原理:字符串在 Go 语言中是不可变类型,占用内存大小是固定的
使用 + 拼接时,生成一个新的字符串,开辟一段新空间,新空间的大小是原来两个字符串的大小之和
strings.Builder
,bytes.Buffer
的内存是以倍数申请的
strings.Builder
和 bytes.Buffer
底层都是 []byte
数组
bytes.Buffer
转化为字符串时重新申请了一块空间存放生成的字符串变量strings.Builder
直接将底层的 []byte
转换成了字符串类型返回 func PreStrBuilder(n int, str string) string {
var builder strings.Builder
builder.Grow(n * len(str))
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
空结构体 struct
实例不占据任何的内存空间
可作为各种场景下的占位符使用
如实现Set时,利用map的键,而将值设为空结构体。(golang-set/threadunsafe…)
本节课介绍了Go乃至其他语言中常见的代码规范,提出了Go语言中相关的性能优化建议。后续还进行了性能优化的实战练习,使用pprof工具进行。
笔记内容来源于第三届青训营张雷老师的课程《高质量编程与性能调优实战》
课程资料:【Go 语言原理与实践学习资料(上)】第三届字节跳动青训营-后端专场