初始化 struct
- 使用字段名初始化结构体。
// 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},
// ...
}
- 声明零值结构使用关键字 var。
如果在声明中省略了结构的所有字段,请使用 var 声明结构,因为这样更加简洁,其各个字段值为字段类型对应的零值。
// Bad
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"}
初始化 map
初始化 map 优先使用 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
)
// 声明和初始化在视觉上是不同的
尽可能地在初始化时提供 map 容量大小。
例外:如果 map 包含固定的元素列表,则使用字面量初始化 map,这样可以在初始化时指定元素。
// 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,
}
初始化 slice
- 非零值 slice 使用
make()
初始化,并指定容量。
// Bad
nums := []int{}
// Good
nums := make([]int, 0, CAP)
- 空切片使用 var 声明
不管是全局切片还是局部切片,使用 var 申明 nil 切片,代码会更加简洁清晰。
// Bad
func foo() {
// 长度为 0 的非 nil 切片
nums := []int{}
}
// Good
func foo() {
// nil 切片
var nums []int
}
- nil 是一个有效的 slice。
nil 是一个有效的长度为 0 的 slice,这意味着,
(1)不应明确返回长度为零的切片,应返回 nil 来代替。
// Bad
if x == "" {
return []int{}
}
// Good
if x == "" {
return nil
}
(2)要检查切片是否为空,请始终使用 len(s) == 0 而非 nil。
// Bad
func isEmpty(s []string) bool {
return s == nil
}
// Good
func isEmpty(s []string) bool {
return len(s) == 0
}
(3)零值切片(用var声明的切片)可立即使用,无需调用 make() 创建。
// Bad
nums := []int{}
// or, nums := make([]int)
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
// Good
var nums []int
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
记住,虽然 nil 切片是有效的切片,但它不等于长度为 0 的切片(一个为 nil,另一个不是),并且在不同的情况下(例如序列化),这两个切片的处理方式可能不同。
申明变量
- 就近申明。
变量申明的位置尽量靠近使用的地方。
// Bad
func foo(m map[string]interface{}) string {
info, _ := m["key"].(Info)
...
return handle(info)
}
// Good
func foo(m map[string]interface{}) string {
...
info, _ := m["key"].(Info)
return handle(info)
}
- 相似的声明放在一组。
对于变量、常量的声明,相似的声明应该放在一组。类型的定义同样适用。
// Bad
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
// Good
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
仅将相关的声明放在一组,不要将不相关的声明放在一组。
// Bad
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
)
// Good
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV"
另外,分组使用的位置没有限制,我们也可以在函数内部使用它们。
// Bad
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
...
}
// Good
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
- 全局变量申明使用 var 关键字并省略类型。
全局变量使用 var 关键字申明,一般情况下其类型与表达式的类型一致,这种情况下可省略其类型。
// Bad
var s string = F()
func F() string { return "A" }
// Good
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定类型。
var s = F()
func F() string { return "A" }
如果表达式的类型与所需的类型不完全匹配,请指定类型。
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
// F 返回一个 myError 类型的实例,但是我们要 error 类型。
var _e error = F()
- 局部变量使用短变量声明形式(
:=
)。
// Bad
func foo() {
var s = "foo"
}
// Good
func foo() {
s := "foo"
}
例外 1:如果是相似的一组变量,请使用 var 声明到一组。
// Bad
func foo() {
s1 := "foo"
s2 := "bar"
}
// Good
func foo() {
var (
s1 = "foo"
s2 = "bar"
)
}
例外 2:局部零值变量使用 var。
// Bad
func foo() {
i := in64(0) // 显示指明 0 有些冗余。
}
// Good
func foo() {
var i int64 // 默认为相应类型的零值。
}
- 如果全局变量仅在单个函数内使用,则应该定义为局部变量。
尽可能避免使用 init()
尽可能避免使用 init(),当 init() 不可避免时,init() 应该做到:
(1)无论程序环境或调用如何,行为都必须是完全确定的。
(2)避免依赖其他 init() 函数的顺序或副作用。虽然 init() 顺序是明确的,但代码可以更改, 因此 init() 函数之间的关系可能会使代码变得脆弱和容易出错。
(3)避免访问或操作全局变量和环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
(4)避免 I/O,包括文件系统、网络和系统调用。
不能满足这些要求的代码可能要在 main() 函数中被调用(或程序生命周期中的其他地方),或作为 main() 函数本身的一部分。特别是打算给其他程序使用的库应该特别注意代码行为的完全确定性, 而不是执行 “init magic”。
// Bad
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
// Good
var _defaultFoo = Foo{
// ...
}
// 或者为了更好的可测试性
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
// Bad
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: 基于当前目录
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
// Good
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle err
raw, err := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err
var config Config
yaml.Unmarshal(raw, &config)
return config
}
凡事无绝对,某些情况下,init() 可能更可取或是必要的:
(1)不能表示为单个赋值的复杂表达式。
(2)可插入的钩子,如 database/sql、编码类型注册表等。
(3)对 Google Cloud Functions 和其他形式的确定性预计算的优化。