代码规范检查
代码规范检查,是根据 Go 语言的规范,对代码进行 静态扫描检查,这种检查和业务没有关系。
比如程序中定义了个常量,从未使用过,虽然代码运行没有什么影响,但是为了节省内存,我们可以删除它,这种情况可以通过代码规范检查检测出来。
golangci-lint
golangci-lint 是一个集成工具,它集成了很多静态代码分析工具(静态代码分析是不会运行代码的),我们通过配置这个工具,便可灵活启用需要的代码规范检查。
安装
golangci-lint 是 Go 语言编写的,可以从源代码安装它,在终端输入命令:
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.32.2
此处安装的是 v1.32.2 版本,安装完成后,检查是否安装成功,输入命令:
golangci-lint version //golangci-lint has version v1.32.2 复制代码
安装成功后,我们使用它来进行代码检查,比如我们有如下代码:
const name = "微客鸟窝" func main() { } 复制代码
终端输入命令:
golangci-lint run test/
表示检测目录 test 下的代码,运行结果:
test\test.go:3:7: `name` is unused (deadcode) const name = "微客鸟窝" ^ test\test.go:4:6: `main` is unused (deadcode) func main() { ^ 复制代码
可以看到,程序常量未使用的问题被检测出来了,后续我们就可以对代码进行完善。
golangci-lint 配置
golangci-lint 的配置可以自定义要启用哪些 linter。golangci-lint 默认启用的 linter 有:
deadcode - 死代码检查 errcheck - 返回错误是否使用检查 gosimple - 检查代码是否可以简化 govet - 代码可疑检查,比如格式化字符串和类型不一致 ineffassign - 检查是否有未使用的代码 staticcheck - 静态分析检查 structcheck - 查找未使用的结构体字段 typecheck - 类型检查 unused - 未使用代码检查 varcheck - 未使用的全局变量和常量检查 复制代码
更多的 linter 我们可以在终端输入命令: golangci-lint linters
, 来查看。
修改默认启用的 linter ,需要在项目根目录下新建一个名字为 .golangci.yml 的文件,这个就是 golangci-lint 的配置文件。在运行规范检查时,golangci-lint 会自动使用它。
不如我们在团队开发中,需要使用一个固定的 golangci-lint 版本,这样大家就可以基于同样的标准检查代码。需要在配置文件中添加如下代码:
service: golangci-lint-version: 1.32.2 # use the fixed version to not introduce new linters unexpectedly 复制代码
golangci-lint 的配置比较多,你可以根据自己需要来配置,可以参考官文档:golangci-lint.run/usage/confi… 。这里给一个常用的配置,供大家参考:
linters-settings: golint: min-confidence: 0 misspell: locale: US linters: disable-all: true enable: - typecheck - goimports - misspell - govet - golint - ineffassign - gosimple - deadcode - structcheck - unused - errcheck service: golangci-lint-version: 1.32.2 # use the fixed version to not introduce new linters unexpectedly 复制代码
集成 golangci-lint 到 CI
代码检查一定要集成到 CI 流程中,这样提交代码的时候,CI 就会自动检查代码,及时发现问题并进行修正。
我们可以通过 Makefile 的方式来运行 golangci-lint ,在项目根目录创建一个 Makefile 文件,代码为:
getdeps: @mkdir -p ${GOPATH}/bin @which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.32.2) lint: @echo "Running $@ check" @GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean @GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml verifiers: getdeps lint 复制代码
然后可以把如下命令添加到你的 CI 中了,它可以帮你自动安装 golangci-lint,并检查你的代码。make verifiers
堆分配还是栈
Go 语言有两部分内存空间:栈内存和堆内存。
- 栈内存由编译器自动分配和释放,开发者无法控制。栈内存一般存储函数中的局部变量、参数等,函数创建的时候,这些内存会被自动创建;函数返回的时候,这些内存会被自动释放。
- 堆内存的生命周期比栈内存要长,如果函数返回的值还会在其他地方使用,那么这个值就会被编译器自动分配到堆上。堆内存相比栈内存来说,不能自动被编译器释放,只能通过垃圾回收器才能释放,所以栈内存效率会很高。
逃逸分析
一个变量具体是分配到堆上还是栈上,需要进行逃逸分析来查看。
示例:
func newString() *string{ s := new(string) //通过 new 函数申请了一块内存,赋值给了指针变量 s *s = "微客鸟窝" return s //通过 return 关键字返回 } 复制代码
逃逸分析命令:
$ go build -gcflags="-m -l" ./test/test.go # command-line-arguments test\test.go:4:8: new(string) escapes to heap 复制代码
- -m 表示打印出逃逸分析信息
- -l 表示禁止内联,可以更好地观察逃逸
上面结果发现,发生了逃逸,表明指针作为函数返回值的时候,一定会发生逃逸。逃逸到堆内存的变量不能马上被回收,只能通过垃圾回收标记清除,增加了垃圾回收的压力,所以要尽可能地避免逃逸,让变量分配在栈内存上,这样函数返回时就可以回收资源,提升效率。
代码优化:
func newString() string { s := new(string) *s = "微客鸟窝" return *s } 复制代码
逃逸分析命令:
$ go build -gcflags="-m -l" ./test/test.go # command-line-arguments test\test.go:4:10: new(string) does not escape 复制代码
虽然还是声明了指针变量 s,但是函数返回的并不是指针,所以没有发生逃逸。
Go 语言中有 3 个比较特殊的类型,它们是 slice、map 和 chan,被这三种类型引用的指针也会发生逃逸:
func main() { m := map[int]*string{} s := "微客鸟窝" m[0] = &s } 复制代码
$ go build -gcflags="-m -l" ./test/test.go # command-line-arguments test\test.go:5:2: moved to heap: s test\test.go:4:22: map[int]*string{} does not escape 复制代码
逃逸分析结果发现,变量 m 没有逃逸,反而被变量 m 引用的变量 s 逃逸到了堆上。
- 被map、slice 和 chan 这三种类型引用的指针一定会发生逃逸的。
- 指针虽然可以减少内存的拷贝,但它同样会引起逃逸,所以要根据实际情况选择是否使用指针。