各个公司或组织,都有各自不同的 Go 编码规范,但大同小异。规范是一种倡导,不遵守并不代表错误,但当大家都遵守规范时,你会发现,整个世界将变得整洁有序。
本文结合官方编码建议,大厂编码规范和自身项目经验,尽可能以简短的语言给出一套行之有效 Go 编码规范建议,让您的代码高效易读。
本文所述内容均为参考意见,并非标准。其中许多是 Go 的通用准则,而其他扩展准则依赖于下面官方指南:
时间处理很复杂。关于时间的错误假设通常包括以下几点。
例如,在一个时间点上加上 24 小时并不总是产生一个新的日历日。
因此,在处理时间时始终使用 “time” 包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。
在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。
// Bad
func isActive(now, start, stop int) bool {
return start <= now && now < stop
}
// God
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}
// Bad
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // 是几秒钟还是几毫秒?
// Good
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}
poll(10*time.Second)
尽可能在与外部系统交互中使用 time.Duration
和 time.Time
,例如:
flag
通过 time.ParseDuration
支持 time.Duration
encoding/json
通过其 UnmarshalJSON method
方法支持将 time.Time
编码为 RFC 3339 字符串database/sql
支持将 DATETIME
或 TIMESTAMP
列转换为 time.Time
,如果底层驱动程序支持则返回gopkg.in/yaml.v2
支持将 time.Time
作为 RFC 3339 字符串,并通过 time.ParseDuration
支持 time.Duration。当不能在这些交互中使用 time.Duration
时,请使用 int
或 float64
,并在字段名称中包含单位。
例如,由于 encoding/json
不支持 time.Duration
,因此该单位包含在字段的名称中。
// Bad
// {"interval": 2}
type Config struct {
Interval int `json:"interval"`
}
// Good
// {"intervalMillis": 2000}
type Config struct {
IntervalMillis int `json:"intervalMillis"`
}
嵌入类型会泄漏实现细节、禁止类型演化、产生模糊的文档,应该尽可能地避免。
// Bad
// ConcreteList 是一个实体列表。
type ConcreteList struct {
*AbstractList
}
// Good
// ConcreteList 是一个实体列表
type ConcreteList struct {
list *AbstractList
}
// 添加将实体添加到列表中
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化。
尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。
// Bad
k := User{"John", "Doe", true}
// Good
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
例外:如果有 3 个或更少的字段,则可以在测试表中省略字段名称。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
// Bad
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
// Good
user := User{
FirstName: "John",
LastName: "Doe",
}
例外:在字段名提供有意义上下文的地方包含零值。例如,表驱动测试中的测试用例可以受益于字段的名称,即使它们是零值的。
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
// Bad
var user := User{}
// Good
var user User
在初始化结构引用时,请使用&T{}
代替new(T)
,以使其与结构体初始化一致。
// Bad
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
// Good
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
make(..)
初始化。// Bad
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = map[T1]T2{}
m2 map[T1]T2
)
// Good
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)
// Bad
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
// Good
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}
make()
初始化,并指定容量// Bad
nums := []int{}
// Good
nums := make([]int, 0, SIZE)
make()
创建// Bad
// 非 nil 切片
nums := []int{}
// Good
// nil 切片
var nums []int
:=
)// Bad
var s = "foo"
// Good
s := "foo"
// Bad
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
// Good
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
尽可能避免使用 init()。当init()是不可避免或可取的,代码应先尝试:
defer xx.Close()
可以不用显式处理// 不要采用这种方式
func do() (error, int) {
}
// 要采用下面的方式
func do() (int, error) {
}
// 不要采用这种方式
if err != nil {
// error handling
} else {
// normal code
}
// 而要采用下面的方式
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
x, err := f()
if err != nil {
// error handling
return // or continue, etc.
}
// use x
// 不要采用这种方式:
x, y, err := f()
if err != nil || y == nil {
return err // 当y与err都为空时,函数的调用者会出现错误的调用逻辑
}
// 应当使用如下方式:
x, y, err := f()
if err != nil {
return err
}
if y == nil {
return fmt.Errorf("some error")
}
fmt.Errorf("module xxx: %v", err)
,而不是 errors.New(fmt.Sprintf("module xxx: %v",err))
。// 不推荐为传递 error 而在包内使用 panic。以下为反面示例
// TError 包内定义的错误类型
type TError string
// Error error接口方法
func (e TError) Error() string {
return string(e)
}
func do(str string) {
// ...
// 此处的 panic 用于传递 error
panic(TError("错误信息"))
// ...
}
// Do 包级访问入口
func Do(str string) (err error) {
defer func() {
if e := recover(); e != nil {
err = e.(TError)
}
}()
do(str)
return nil
}
package main
import (
"log"
)
func main() {
defer func() {
if err := recover(); err != nil {
// do something or record log
log.Println("exec panic error: ", err)
// log.Println(debug.Stack())
}
}()
getOne()
panic(44) //手动抛出panic
}
// getOne 模拟 slice 越界 runtime 运行时抛出的 panic
func getOne() {
defer func() {
if err := recover(); err != nil {
// do something or record log
log.Println("exec panic error: ", err)
// log.Println(debug.Stack())
}
}()
var arr = []string{"a", "b", "c"}
log.Println("hello,", arr[4])
}
// 执行结果:
2021/10/04 11:07:13 exec panic error: runtime error: index out of range [4] with length 3
2021/10/04 11:07:13 exec panic error: 44
// 不要采用这种方式
t := i.(string)
// 而要采用下面的方式
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
将原语转换为字符串或从字符串转换时,strconv
比fmt
快。
// Bad
// BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
// Good
// BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
在尽可能的情况下,在使用make()
初始化切片时提供容量信息,特别是在追加切片时。
// Bad
// BenchmarkBad-4 100000000 2.48s
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
// Good
// BenchmarkGood-4 100000000 0.21s
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
向make()
提供容量提示会在初始化时尝试调整map
的大小,这将减少在将元素添加到map
时为map
重新分配内存。
注意,与 slice 不同。map capacity 提示并不保证完全的抢占式分配,而是用于估计所需的 hashmap bucket 的数量。 因此,在将元素添加到 map 时,甚至在指定 map 容量时,仍可能发生分配。
make(map[T1]T2, hint)
在编码阶段同步写好类型、变量、函数、包注释,注释可以通过godoc
导出生成文档。
程序中每一个被导出的(大写的)名字,都应该有一个文档注释。
所有注释掉的代码在提交 code review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。
// Package math provides basic constants and mathematical functions.
package math
// 或者
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template
// NewtAttrModel 是属性数据层操作类的工厂方法
func NewAttrModel(ctx *common.Context) *AttrModel {
// TODO
}
函数调用中的意义不明确的参数可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */
)
// Bad
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
// Good
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
// User 用户结构定义了用户基础信息
type User struct {
Name string
Email string
Demographic string // 族群
}
// FlagConfigFile 配置文件的命令行参数名
const FlagConfigFile = "--config"
// 命令行参数
const (
FlagConfigFile1 = "--config" // 配置文件的命令行参数名1
FlagConfigFile2 = "--config" // 配置文件的命令行参数名2
FlagConfigFile3 = "--config" // 配置文件的命令行参数名3
FlagConfigFile4 = "--config" // 配置文件的命令行参数名4
)
// FullName 返回指定用户名的完整名称
var FullName = func(username string) string {
return fmt.Sprintf("fake-%s", username)
}
// StorageClass 存储类型
type StorageClass string
// FakeTime 标准库时间的类型别名
type FakeTime = time.Time
命名是代码规范中很重要的一部分,统一的命名规范有利于提高代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。
util、common、misc、global
。package 名字应该追求清晰且越来越收敛,符合‘单一职责’原则。而不是像common
一样,什么都能往里面放,越来越膨胀,让依赖关系变得复杂,不利于阅读、复用、重构。注意,xx/util/encryption
这样的包名是允许的。// User 多行声明
type User struct {
Name string
Email string
}
// 多行初始化
u := User{
UserName: "john",
Email: "john@example.com",
}
// Reader 字节数组读取接口
type Reader interface {
// Read 读取整个给定的字节数据并返回读取的长度
Read(p []byte) (n int, err error)
}
// Car 小汽车结构申明
type Car interface {
// Start ...
Start([]byte)
// Stop ...
Stop() error
// Recover ...
Recover()
}
// AppVersion 应用程序版本号定义
const AppVersion = "1.0.0"
// Scheme 传输协议
type Scheme string
// 传输协议
const (
HTTP Scheme = "http" // HTTP 明文传输协议
HTTPS Scheme = "https" // HTTPS 加密传输协议
)
const appVersion = "1.0.0"
Go 语言规范 language specification 概述了几个内置的,不应在 Go 项目中使用的名称标识 predeclared identifiers。
Types:
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr
Constants:
true false iota
Zero value:
nil
Functions:
append cap close complex copy delete imag len
make new panic print println real recover
编译器在使用预先分隔的标识符时不会生成错误, 但是诸如go vet
之类的工具会正确地指出这些和其他情况下的隐式问题。
// Bad
// 作用域内隐式覆盖 error interface
var error string
func handleErrorMessage(error string) {
}
// Good
var errorMessage string
func handleErrorMessage(msg string) {
}
if err := file.Chmod(0664); err != nil {
return err
}
// 不要采用这种方式
if nil != err {
// error handling
}
// 不要采用这种方式
if 0 == errorCode {
// do something
}
// 而要采用下面的方式
if err != nil {
// error handling
}
// 而要采用下面的方式
if errorCode == 0 {
// do something
}
var allowUserLogin bool
// 不要采用这种方式
if allowUserLogin == true {
// do something
}
// 不要采用这种方式
if allowUserLogin == false {
// do something
}
// 而要采用下面的方式
if allowUserLogin {
// do something
}
// 而要采用下面的方式
if !allowUserLogin {
// do something
}
如果在 if 的两个分支中都设置变量,则可以将其替换为单个 if。
// Bad
var a int
if b {
a = 100
} else {
a = 10
}
// Good
a := 10
if b {
a = 100
}
sum := 0
for i := 0; i < 10; i++ {
sum += 1
}
for key := range m {
if key.expired() {
delete(m, key)
}
}
sum := 0
for _, v := range array {
sum += v
}
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("MAC OS")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
d, err := f.Stat()
if err != nil {
return err
}
codeUsing(f, d)
业务代码禁止使用 goto,其他框架或底层源码推荐尽量不用。
os.Exit
或者log.Fatal*
立即退出,而不是panic
。main()
函数中最多一次 调用os.Exit
或者log.Fatal
。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 这会精简main()
函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。// Bad
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 如果我们调用log.Fatal f.Close 将不会被执行
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
// Good
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
// ...
}
// Parent1 ...
func (n *Node) Parent1() *Node
// Parent2 ...
func (n *Node) Parent2() (*Node, error)
// Location ...
func (f *Foo) Location() (lat, long float64, err error)
resp, err := http.Get(url)
if err != nil {
return err
}
// 如果操作成功,再defer Close()
defer resp.Body.Close()
// 不要这样使用
func filterSomething(values []string) {
for _, v := range values {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
// xxx
}
defer fields.Close()
// 继续使用fields
}
}
// 应当使用如下的方式:
func filterSomething(values []string) {
for _, v := range values {
func() {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
...
}
defer fields.Close()
// 继续使用fields
}()
}
}
// AddArea 添加成功或出错
func (s *BookingService) AddArea(areas ...string) error {
s.Lock()
defer s.Unlock()
for _, area := range areas {
for _, has := range s.areas {
if area == has {
return srverr.ErrAreaConflict
}
}
s.areas = append(s.areas, area)
s.areaOrders[area] = new(order.AreaOrder)
}
return nil
}
// 建议调整为这样:
// AddArea 添加成功或出错
func (s *BookingService) AddArea(areas ...string) error {
s.Lock()
defer s.Unlock()
for _, area := range areas {
if s.HasArea(area) {
return srverr.ErrAreaConflict
}
s.areas = append(s.areas, area)
s.areaOrders[area] = new(order.AreaOrder)
}
return nil
}
// HasArea ...
func (s *BookingService) HasArea(area string) bool {
for _, has := range s.areas {
if area == has {
return true
}
}
return false
}
func getArea(r float64) float64 {
return 3.14 * r * r
}
func getLength(r float64) float64 {
return 3.14 * 2 * r
}
// PI ...
const PI = 3.14
func getArea(r float64) float64 {
return PI * r * r
}
func getLength(r float64) float64 {
return PI * 2 * r
}
因此,导出的函数应先出现在文件中,放在struct
, const
, var
定义的后面。
在定义类型之后,但在接收者的其余方法之前,可能会出现一个newXYZ()/NewXYZ()
。
由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。
// Bad
func (s *something) Cost() {
return calcCost(s.weights)
}
type something struct{ ... }
func calcCost(n []int) int {...}
func (s *something) Stop() {...}
func newSomething() *something {
return &something{}
}
// Good
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n []int) int {...}
使用 table 驱动的方式编写 case 代码看上去会更简洁。
// Bad
// func TestSplitHostPort(t *testing.T)
host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)
host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
// Good
// func TestSplitHostPort(t *testing.T)
tests := []struct{
give string
wantHost string
wantPort string
}{
{
give: "192.0.2.0:8000",
wantHost: "192.0.2.0",
wantPort: "8000",
},
{
give: "192.0.2.0:http",
wantHost: "192.0.2.0",
wantPort: "http",
},
{
give: ":8000",
wantHost: "",
wantPort: "8000",
},
{
give: "1:8",
wantHost: "1",
wantPort: "8",
},
}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
host, port, err := net.SplitHostPort(tt.give)
require.NoError(t, err)
assert.Equal(t, tt.wantHost, host)
assert.Equal(t, tt.wantPort, port)
})
}
go mod init git.code.oa.com/group/myrepo
go.sum
文件必须提交,不要添加到.gitignore
规则中Go 本身在代码规范方面做了很多努力,很多限制都是语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错。此外 Go 还是提供了很多好用的工具帮助我们进行代码的规范。