本指南借鉴了Uber和官方的代码风格,结合工程中的实践,总结了一些大家经常犯的一些"错误"写法以及建议写法,一部分也是我们code review的规则,希望帮助每一个对代码有追求的工程师,写出优雅的代码。
导入应该分为两组:
- 标准库
- 其他库
默认情况下,这是 goimports 应用的分组。
Bad | Good |
---|---|
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
Bad | Good |
---|---|
func max(x, y int) int {
if x > y {
return x
} else {
return y
}
} |
func max(x, y int) int {
if x > y {
return x
}
return y
} |
这点其实大家经常会忽略,不够重视。因为bad写法其实反而是顺着大脑思绪的,很多时候会下意识这样写。
Bad | Good |
---|---|
if success == true {
// do something
}
if a == b && success == false {
// do something
} |
if success {
// do something
}
if a == b && !success {
// do something
} |
减少嵌套! 减少嵌套! 减少嵌套!
当你看到一大堆'}}}}}'像楼梯一样连在一起,绝对会想打人的,我认为除非是多重循环,否则超过4层嵌套,肯定是可以优化的,code review要坚决打回。 (生产环境有的代码远比此处的例子更糟糕)。
Bad | Good |
---|---|
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)
}
} |
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()
} |
Do | Better |
---|---|
flag := false
cnt := 0
user := User{} |
var flag bool
var cnt int
var user User |
Bad | Good |
---|---|
var ans int
ans = 100 |
ans := 100 |
Do | Better |
---|---|
var head int
var tail int
left := 0
right := len(nums) - 1 |
var head, tail int
left, right := 0, len(nums)-1 |
type assertion 的单个返回值形式针对不正确的类型将产生 panic。因此,请始终使用“comma ok”的惯用法。
Bad | Good |
---|---|
t := i.(string) |
t, ok := i.(string)
if !ok {
// 优雅地处理错误
} |
在初始化结构引用时,请使用&T{}
代替new(T)
,以使其与结构体初始化一致。前面的&可以很直观地体现出取引用,而使用new关键字对不熟悉C或C++的同学可能有点奇怪(个人理解)。
Bad | Good |
---|---|
a := new(Test)
b := new(Test)
b.head = 1
b.tail = 2 |
a := &Test{}
// 少于3个字段可考虑省略字段名
b := &Test{1, 2}
// 更加建议全部包含字段名(不然IDE也会有虚线)
b := &Test{
head: 1,
tail: 2,
} |
嵌入式类型应位于结构体内的字段列表的顶部,并且建议有一个空行将嵌入式字段与常规字段分隔开。
Bad | Good |
---|---|
type Client struct {
version int
http.Client
} |
type Client struct {
http.Client
version int
} |
个人偏爱使用var声明,认为优雅大气,此处保留意见。
Do | Better |
---|---|
arr := make([]int, 0) // arr != nil
arr := []int{} // arr != nil
arr := []int(nil) // arr == nil |
var arr []int |
有时候要直接修改数组值,若元素不为指针,则只可通过arr[i] = val这样的语句修改。 还是有一部分同学使用Bad方法来写的,提醒一下。
Bad | Good |
---|---|
// 画蛇添足,多半不知道可以省略下划线
for i, _ := range arr {
arr[i] = val
} |
for i := range arr {
arr[i] = val
}
for i := 0; i < len(arr); i++ {
arr[i] = val
} |
Go语言定于并初始化二维数组是比较麻烦的,两种方法差不多,go语言源码都有使用。 个人偏爱第二个,写起来非常之快。若是对i有要求的话,第一种更直观和方便。
Do | Or |
---|---|
matrix := make([][]int, m)
for i := 0; i < n; i++ {
matrix[i] = make([]int, n)
} |
matrix := make([][]int, m)
for i := range matrix {
matrix[i] = make([]int, n)
} |
在尽可能的情况下,在使用make()
初始化切片时提供容量信息。
make([]T, length, capacity)
与map不同,slice capacity不是一个提示:编译器将为提供给make()
的slice的容量分配足够的内存,这意味着后续的append()操作将导致零内存分配。
(直到slice的长度与容量匹配,在此之后,任何append都可能调整大小以容纳其他元素)。
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)
}
} |
|
|
对于空 map 请使用 make(..)
初始化, 并且 map 是通过编程方式填充的。
这使得 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 包含固定的元素列表,则使用 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 容量)。
Go 支持使用 原始字符串字面值,也就是 " ` " 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。
可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。
Bad | Good |
---|---|
wantError := "unknown name:\"test\"" |
wantError := `unknown error:"test"` |