Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 将内部的 Go 风格规范 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注,本文是对规范的整理
来源:https://github.com/xxjwxc/uber_go_guide_cn
1. 包名
当命名包时,请按下面规则命名:
- 全部小写、没有大写或下划线。
- 大多数使用命名导入的情况下,不需要重命名。
- 简短而简洁。
- 不用复数。例如
net/url
,而不是net/urls
。
- 不要用
common、util、shared、lib
,这些不好、信息量不足的名称。
1.1 最后一个词不是包名
如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。
import ( "net/http"
client "example.com/client-go" trace "example.com/trace/v2" )
|
1.2 有冲突时
在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名。
import ( "fmt" "os"
nettrace "golang.net/x/trace" )
import ( "fmt" "os" "runtime/trace"
nettrace "golang.net/x/trace" )
|
2. 函数名
我们遵循 Go 社区关于使用 MixedCaps 作为函数名 的约定,使用驼峰命名法,不要使用下划线。举例:MixedCaps
或者mixedCaps
有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:TestMyFunction_WhatIsBeingTested
3. 变量和常量
3.1 未导出带前缀
对于未导出的全局(包内)vars
和consts
, 前面加上前缀_
,用来明确表示它们是全局符号。错误类型的变量例外,错误类型变量应以err
开头。
1.不推荐写法
const ( defaultPort = 8080 defaultUser = "user" )
func Bar() { defaultPort := 9090 ... fmt.Println("Default port", defaultPort) }
|
2. 推荐写法
const ( _defaultPort = 8080 _defaultUser = "user" )
|
3.2 相似声明放一组
Go
语言支持将相似的声明放在一个组内。
1. 反面示例
import "a" import "b"
const a = 1 const b = 2
var a = 1 var b = 2
type Area float64 type Volume float64
type Operation int
const ( Add Operation = iota + 1 Subtract Multiply EnvVar = "MY_ENV" )
|
2. 推荐用法
import ( "a" "b" )
const ( a = 1 b = 2 )
var ( a = 1 b = 2 )
type ( Area float64 Volume float64 )
type Operation int
const ( Add Operation = iota + 1 Subtract Multiply ) const EnvVar = "MY_ENV"
|
3.3 本地变量声明
如果将变量明确设置为某个值,则应使用短变量声明形式 (:=
)。
但是,在某些情况下,var
使用关键字时默认值会更清晰。例如,声明空切片。
a. 反面示例
func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } }
|
b. 推荐写法
func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } }
|
3.4 缩小变量作用域
如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。
err := ioutil.WriteFile(name, data, 0644) if err != nil { return err }
if err := ioutil.WriteFile(name, data, 0644); err != nil { return err }
|
4. import 分组
导入应该分为两组:标准库、其他库,用空行分开
4.1 反面示例
import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
4.2 推荐用法
import ( "fmt" "os"
"go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
4.defer使用
使用 defer 释放资源,诸如文件和锁。
4.1 反面示例
p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount
|
4.2 推荐写法
p.Lock() defer p.Unlock()
if p.count < 10 { return p.count }
p.count++ return p.count
|
5. Channel的size
Channel 的 size 要么是 1,要么是无缓冲的
Bad |
Good |
// 应该足以满足任何情况!
c := make(chan int, 64) |
// 大小:1
c := make(chan int, 1)
// 或者无缓冲 channel,大小为 0
c := make(chan int) |
6. 枚举从1开始
在 Go
中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota
的 const
组。由于变量的默认值为 0
,因此通常应以非零值开头枚举。
6.1 反面示例
type Operation int
const ( Add Operation = iota Subtract Multiply )
|
6.2 推荐写法
type Operation int
const ( Add Operation = iota + 1 Subtract Multiply )
|
7. 断言使用
类型断言 将会在检测到不正确的类型时,以单一返回值形式时会抛 panic
。
t := i.(string)
t, ok := i.(string) if !ok { }
|
8. 不要使用panic
在生产环境中运行的代码必须避免出现 panic
。panic
是 级联失败 的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。
8.1 反面示例
func run(args []string) { if len(args) == 0 { panic("an argument is required") } }
func main() { run(os.Args[1:]) }
|
8.2 推荐写法
func run(args []string) error { if len(args) == 0 { return errors.New("an argument is required") } return nil }
func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
|
9. 避免使用init()
尽可能避免使用init()
。当init()
是不可避免或可取的,代码应先尝试:
- 无论程序环境或调用如何,都要完全确定。
- 避免依赖于其他
init()
函数的顺序或副作用。虽然init()
顺序是明确的,但代码可以更改, 因此init()
函数之间的关系可能会使代码变得脆弱和容易出错。
- 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
- 避免
I/O
,包括文件系统、网络和系统调用。
尽可能的作为main()
函数流程中调用的一个环节。
9.1 反面示例
type Config struct { } var _config Config func init() { cwd, _ := os.Getwd() raw, _ := ioutil.ReadFile( path.Join(cwd, "config", "config.yaml"), ) yaml.Unmarshal(raw, &_config) }
|
9.2 推荐写法
type Config struct { }
func loadConfig() Config { cwd, err := os.Getwd() raw, err := ioutil.ReadFile( path.Join(cwd, "config", "config.yaml"), ) var config Config yaml.Unmarshal(raw, &config) return config }
|
10. 流程优化
10.1 减少嵌套
代码应尽可能先处理错误情况、特殊情况并尽早返回,依次来减少嵌套。
a. 反面示例
for _, v := range data { if v.F1 == 1 { v = process(v) if err := v.Call(); err == nil { v.Send() } else { return err } } else { log.Printf("Invalid v: %v", v) } }
|
b. 推荐写法
for _, v := range data { if v.F1 != 1 { log.Printf("Invalid v: %v", v) continue }
v = process(v) if err := v.Call(); err != nil { return err } v.Send() }
|
10.2 不必要的else
var a int if b { a = 100 } else { a = 10 }
a := 10 if b { a = 100 }
|
11. 字符串
11.1 使用**``**,避免转义
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`
|
11.2 格式化字符串
如果你在函数外声明Printf-style
函数的格式字符串,请将其设置为const
常量。
这有助于go vet
对格式字符串执行静态分析。
msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2)
const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2)
|