一. Golang编码规范
使用go mod
go env -w GO111MODULE=on #开启 MODULE
命名规则
文件名
整个应用或包的主入口文件应当是 main.go,或与应用名称简写相同。
文件命名一律采用小写,不用驼峰式,尽量见名思义,看见文件名就可以知道这个文件下的大概内容。
包名
包名与目录名一致如果一个目录下同时出现多个 package,则编译失败: | 1 | found packages pkg (a.go) and pb (b.go) in XXX | | —- | —- |
全部小写,支持下划线,不适用驼峰。错误示例MyPackage、myPackage
- 不用复数。例如net/url,而不是net/urls
尽量不用信息量不足的名字。错误示例common、lib、util
导入包
如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名 | 1 2 3 4 | import (
client “example.com/client-go”
trace “example.com/trace/v2”
) | | —- | —- |在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名 | 1 2 3 4 | import (
“net/http/pprof”
gpprof “github.com/google/pprof”
) | | —- | —- |如遇重名,请保留标准包而别名自定义
禁止使用相对路径导入(./subpackage),所有导入路径必须符合 go get 标准
URL
URL 命名全部小写
- 用正斜杠 / 表明层级关系
- 使用连字符 - 来提高长路径中名称的可读性
- 不得在 URL 中使用下划线 _
- URL 结尾不应包含正斜杠 /
- 文件扩展名不应包含在 URL 中
| Bad | Good |
| —- | —- |
| /GetUserInfo
/photos_path
/My-Folder/my-doc/
/user/user-list | /user/list
/user/operator-logs |
驼峰式命名
常量、变量、类型、结构体、接口、函数、方法、属性等,全部使用驼峰法 MixedCaps 或 mixedCaps。
常量
- 在相对简单的环境(对象数量少、针对性强)中,可以将一些名称由完整单词简写为单个字母
- user 可以简写为 u
- userId 可以简写 uid
- 若变量类型为 bool 类型,则名称应以 Has、Is、Can 或 Allow 开头
| 1 2 3 4 | var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool | | —- | —- |
Error
Error 类型的命名以 Error 结尾 | 1 2 3 | type ParseError struct {
Line, Col int
} | | —- | —- |Error 类型的变量,以 Err开头 | 1 | var ErrBadAction = errors.New(“somepkg: a bad action was performed”) | | —- | —- |
处理错误
不要将error赋值给匿名变量 _(因为你不可以使用匿名变量,当把error赋值给匿名变量后,相当于抛弃了这个error)。
如果一个函数返回error,一定要检查它是否为空,判断函数调用是否成功。如果不为空,说明发生了错误,一定要处理它。
处理断言失败
类型断言将会在检测到不正确的类型时,以单一返回值形式返回 panic。 因此,请始终使用“逗号 ok”检查。
Bad | Good |
---|---|
t := i.(string) | t, ok := i.(string) if !ok { // 处理错误 } |
Imports
当import多个包时,应该对包进行分组。同一组的包之间不需要有空行,不同组之间的包需要一个空行。标准库的包应该放在第一组。
goimports这个工具能直接帮你修正import包的规范。
以下是一个不错的import示例:
| 1
2
3
4
5
6
7
8
9
10
11
12
13 | package main
import (
“fmt”
“hash/adler32”
“os”
"appengine/foo"<br /> "appengine/user"
"code.google.com/p/x/y"<br /> "github.com/foo/bar"<br />) |
| —- | —- |
代码行长度
在Golang中,没有严格限制代码行长度,但我们应该尽量避免一行内写过长的代码,以及将长代码进行断行。
每行不超过80个字符。
对于未导出的顶层常量和变量,使用_作为前缀
在未导出的顶级vars和consts, 前面加上前缀_,以使它们在使用时明确表示它们是全局符号。
Bad | Good |
---|---|
// foo.go const ( defaultPort = 8080 defaultUser = “user” ) // bar.go func Bar() { defaultPort := 9090 … fmt.Println(“Default port”, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted. } | // foo.go const ( _defaultPort = 8080 _defaultUser = “user” ) |
使用字段名初始化结构体
初始化结构体时,应该指定字段名称。
Bad | Good |
---|---|
k := User{“John”, “Doe”, true} | k := User{ FirstName: “John”, LastName: “Doe”, Admin: true, } |
日志格式
日志接入统一参考文档:日志GO SDK接入文档
单元测试
- 单元测试文件名必须以 xxx_test.go 命名
- 方法必须是 TestXxx 开头,建议风格保持一致(驼峰或者下划线)
- 方法参数必须 t *testing.T
- 测试文件和被测试文件必须在一个包中
- 单元测试覆盖率 60%
- 推荐使用 GoConvey , testify 编写测试用例
项目代码仓库路径
详细请参考Gitlab代码仓库路径规范
项目目录
├── api ├── cmd │ └── your_app ├── configs ├── docs ├── examples ├── internal │ ├── app │ │ └── your_app │ └── pkg │ └── your_private_lib ├── pkg │ └── your_public_lib ├── web │ ├── app │ ├── static │ └── template ├── .gitignore ├── build.sh
├── .gitlab-ci.yml
├── Makefile ├── README.md
├── Dockerfile
└── go.mod
cmd
当前项目的可执行文件。cmd 目录下的每一个子目录名称都应该匹配可执行文件。比如果我们的项目是一个 server 服务,在 /cmd/server/main.go 中就包含了启动服务进程的代码,编译后生成的可执行文件就是 server。
不要在 /cmd 目录中放置太多的代码,我们应该将公有代码放置到 /pkg 中,将私有代码放置到 /internal 中并在 /cmd 中引入这些包,保证 main 函数中的代码尽可能简单和少。
internal
私有的应用程序代码库。这些是不希望被其他人导入的代码。
可以在内部代码包中添加一些额外的结构,来分隔共享和非共享的内部代码。这不是必选项(尤其是在小项目中),但是有一个直观的包用途是很棒的。比如:应用程序代码放在 /internal/app 目录(如,internal/app/myapp),而应用程序的共享代码放在 /internal/pkg 目录(如,internal/pkg/myprivlib)中。
pkg
外部应用程序可以使用的库代码(如,/pkg/mypubliclib)。其他项目将会导入这些库来保证项目可以正常运行。
api
项目对外提供和依赖的 API 文件。比如:protocol 定义文件等。
web
configs
docs
examples
应用程序或公共库的示例程序。
二. Golang编码建议和最佳实践
gofmt
使用gofmt进行格式化。
文档注释
Go提供两种注释风格,C的块注释风格/**/,C++的行注释风格//
- 每一个包都应该有包注释,位于文件的顶部,在包名出现之前。
如果一个包有多个文件,包注释只需要出现在一个文件的顶部即可。
包注释建议使用C注释风格,如果这个包特别简单,需要的注释很少,也可以选择使用C++注释风格。 - 每个public函数都应该有注释,注释句子应该以该函数名开头,如:
| 1
2
3 | // Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) { | | —- | —- |
这样做的好处是,但你要查找某个public函数的注释时,grep函数名即可
Context
大部分使用context的函数都要将其作为第一个参数:
1 | func F(ctx context.Context, / 其他参数 /) {} |
---|---|
声明空数组分片
当你需要时,声明空的数组分片。
这是一个推荐的做法:
1 | var t []string |
---|---|
这是不好的:
1 | t := []string{} |
---|---|
原因是,前者能避免分配内存空间。有些时候,可能你从没向这个数组分片里面append元素。
初始化 Maps
对于空 map 请使用 make(..) 初始化。 这使得 map 初始化在表现上不同于声明,并且它还可以方便地在 make 后添加大小提示。
Bad | Good |
---|---|
var ( // m1 读写安全; // m2 在写入时会 panic m1 = map[T1]T2{} m2 map[T1]T2 ) | var ( // m1 读写安全; // m2 在写入时会 panic m1 = make(map[T1]T2) m2 map[T1]T2 ) |
声明和初始化看起来非常相似的。 | 声明和初始化看起来差别非常大。 |
在尽可能的情况下,请在初始化时提供 map 容量大小
另外,如果 map 包含固定的元素列表,则使用 map literals(map 初始化列表) 初始化映射。
Bad | Good |
---|---|
m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 | m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } |
基本准则是:在初始化时使用 map 初始化列表来添加一组固定的元素。否则使用 make (如果可以,请尽量指定 map 容量)。
不要抛出panic
尽量不要使用panic处理错误。函数应该设计成多返回值,其中包括返回相应的error类型。
error提示
错误提示不需要大写字母开头的单词,即使是句子的首字母也不需要。除非那是个专有名词或者缩写。
同时,错误提示也不需要以句号结尾,因为通常在打印完错误提示后还需要跟随别的提示信息。
尽可能减少正常逻辑代码的缩进
但函数调用返回错误时,我们需要判断错误是否为空,若不为空要进入错误处理的代码分支,结束后再进入正常逻辑代码。
应当尽可能减少正常逻辑代码的缩进,这有利于提高代码的可读性,便于快速分辨出哪些还是正常逻辑代码,例如:
这是一个不好的代码风格,正常逻辑代码被缩进在else分支里面:
1 2 3 4 5 |
if err != nil { // error handling } else { // normal code } |
---|---|
这是一个不错的代码风格,没有增加正常逻辑代码的缩进:
1 2 3 4 5 |
if err != nil { // error handling return // or continue, etc. } // normal code |
---|---|
另一种常见的情况,如果我们需要用函数的返回值来初始化某个变量,应该把这个函数调用单独写在一行,例如:
这是一个不好的代码风格,函数调用,初始化变量x,判断错误是否为空都在同一行,并增加了正常逻辑代码的缩进:
1 2 3 4 5 6 |
if x, err := f(); err != nil { // error handling return } else { // use x } |
---|---|
这是一个不错的代码风格,将函数调用,初始化变量x写在同一行,并且避免了正常逻辑代码的缩进:
1 2 3 4 5 6 |
x, err := f() if err != nil { // error handling return } // use x |
---|---|
传递值而不是指针
传值虽然有复制成本,但内存分配会优先分配到栈上,在一定程度上能减少GC压力。
除非要传递的是一个庞大的结构体或者可预知在将来会变得非常庞大的结构体,指针是一个不错的选择。
接受者命名
结构体函数中,接受者的命名不应该采用 me,this,self等通用的名字,而应该采用简短的(1或2个字符)并且能反映出结构体名的命名风格。
例如,结构体名为Client,接受者可以命名为c或者cl。
这样做的好处是,当生成了godoc后,过长或者过于具体的命名,会影响搜索体验。
当接受者的名字足够简单和短时,它几乎可能出现在每一行中(例如c,无处不在),它不必像参数命名那么具体,因为我们几乎不关心接受者的名字。
接受者类型
编写结构体函数时,接受者的类型到底是选择值还是指针通常难以决定。
一条万能的建议:如果你不知道要使用哪种传递时,请选择指针传递
以下是一些不错的建议:
- 当接受者是map, chan, func, 不要使用指针传递,因为它们本身就是引用类型。
- 当接受者是slice,而函数内部不会对slice进行切片或者重新分配空间,不要使用指针传递。
- 当函数内部需要修改接受者,必须使用指针传递。
- 当接受者是一个结构体,并且包含了sync.Mutex或者类似的用于同步的成员。必须使用指针传递,避免成员拷贝。
- 当接受者类型是一个结构体并且很庞大,或者是一个大数组,建议使用指针传递来提高性能。
- 当接受者是结构体,数组或slice,并且其中的元素是指针,并且函数内部可能修改这些元素,那么使用指针传递是个不错的选择,这能使得函数的语义更加明确。
- 当接受者是小型结构体,小数组,并且不需要修改里面的元素,里面的元素又是一些基础类型,使用值传递是个不错的选择。
在边界处拷贝 Slices 和 Maps
slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。
接收 Slices 和 Maps
请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。
Bad | Good |
---|---|
func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := … d1.SetTrips(trips) // 你是要修改 d1.trips 吗? trips[0] = … | func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := … d1.SetTrips(trips) // 这里我们修改 trips[0],但不会影响到 d1.trips trips[0] = … |
返回 Slices 或 Maps
同样,请注意用户对暴露内部状态的 map 或 slice 的修改。
Bad | Good |
---|---|
type Stats struct { mu sync.Mutex counters map[string]int } // Snapshot 返回当前状态。 func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() return s.counters } // snapshot 不再受互斥锁保护 // 因此对 snapshot 的任何访问都将受到数据竞争的影响 // 影响 stats.counters snapshot := stats.Snapshot() | type Stats struct { mu sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } // snapshot 现在是一个拷贝 snapshot := stats.Snapshot() |
使用 defer 释放资源
使用 defer 释放资源,诸如文件和锁。
Bad | Good |
---|---|
p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 当有多个 return 分支时,很容易遗忘 unlock | p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // 更可读 |
追加时优先指定切片容量
追加时优先指定切片容量
在尽可能的情况下,在初始化要追加的切片时为make()提供一个容量值。
Bad | Good |
---|---|
for n := 0; n < b.N; n++ { data := make([]int, 0) for k := 0; k < size; k++{ data = append(data, k) } } | for n := 0; n < b.N; n++ { data := make([]int, 0, size) for k := 0; k < size; k++{ data = append(data, k) } } |
BenchmarkBad-4 100000000 2.48s | BenchmarkGood-4 100000000 0.21s |
主函数退出方式 (Exit)
Go 程序使用 os.Exit 或者 log.Fatal 立即退出 (不要使用 panic。)
仅在main() 中调用其中一个 os.Exit 或者 log.Fatal。所有其他函数应将错误返回到信号失败中。
Bad | Good |
---|---|
func main() { body := readFile(path) fmt.Println(body) } func readFile(path string) string { f, err := os.Open(path) if err != nil { log.Fatal(err) } b, err := ioutil.ReadAll(f) if err != nil { log.Fatal(err) } return string(b) } | func main() { body, err := readFile(path) if err != nil { log.Fatal(err) } fmt.Println(body) } func readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return “”, err } b, err := ioutil.ReadAll(f) if err != nil { return “”, err } return string(b), nil } |
优先使用 strconv 而不是 fmt
将原语转换为字符串或从字符串转换时,strconv速度比fmt快。
Bad | Good |
---|---|
for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } | for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } |
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op | BenchmarkStrconv-4 64.2 ns/op 1 allocs/op |
避免字符串到字节的转换
不要反复从固定字符串创建字节 slice。相反,请执行一次转换并捕获结果。
Bad | Good |
---|---|
for i := 0; i < b.N; i++ { w.Write([]byte(“Hello world”)) } | data := []byte(“Hello world”) for i := 0; i < b.N; i++ { w.Write(data) } |
BenchmarkBad-4 50000000 22.2 ns/op | BenchmarkGood-4 500000000 3.25 ns/op |
给函数返回值命名
给函数返回值命名,是一条适用于任何场景的建议,我们直接来看对比例子:
这是一个不好的代码风格,我们只知道函数返回的类型,但不知道每个返回值的名字:
1 2 |
func (n Node) Parent1() Node func (n Node) Parent2() (Node, error) |
---|---|
这是一个不错的代码风格,我们准确知道每个返回值的名字:
1 2 |
func (n Node) Parent1() (node Node) func (n Node) Parent2() (node Node, err error) |
---|---|
更多可参考Uber Go 语言编码规范 https://github.com/xxjwxc/uber_go_guide_cn#%E8%A7%84%E8%8C%83