Go 编码建议——风格篇

!!! 提示:本文停止更新,内容已迁移至开源书籍 《Go 编码建议》,欢迎大家协同共建。

为形成统一的 Go 编码风格,提高代码的可读性、可靠性和易维护性,在官方 Google Go Code Review Comments 的基础上,给出编码风格建议。使用时,可根据实际情况进行了调整和补充。

1.格式化

主体风格

  • 代码必须用 gofmt 工具格式化。

gofmt 使用制表符进行缩进,使用空白符进行对齐。

IDE 在保存代码时可设置自动执行 gofmt,如 GoLand 的 Settings > Tools > File Watchers 中可勾选 go fmt 并指定作用范围。

占位符

  • 通过%v打印错误信息,%v前建议加冒号。
// Bad
logger.Errorf("num %d, err %s", num, err.Error())

// Good
logger.Errorf("num:%d, err:%v", num, err.Error())
  • 字符串占位符,如果输入数据来自外部,建议使用%q进行安全转义。
  • 格式化字符串中多个占位符之间需要有空格。
fmt.Printf("%v这里要空格%v", a, b)

2.代码行

行长度

一行代码不要超过120列,超过的情况,使用合理的方法换行。

例外场景:

  • import 模块语句
  • struct tag
  • 工具生成的代码

换行方式

采用惰性换行,换行前应尽可能占满当前行不留空位。

// Bad
fmt.Printf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v\n",
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,89, 144, 233)

// Good
fmt.Printf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v\n", 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
89, 144, 233)

不必要的空行

  • 函数体第一行不要换行。
// Bad
func foo() {

	// func body
}

// Good
func foo() {
	// func body
}
  • 函数调用和对调用结果的处理,是紧密相连的,不能加空行
// Bad
res, err := foo()

if err != nil || res.Ret != 0 {
	return
}

// Good
res, err := foo()
if err != nil || res.Ret != 0 {
	return
}
  • return 语句前不要换行
// Bad
func foo() {
	// func body
	
	return
}

// Good
func foo() {
	// func body
	return
}

括号和空格

  • 遵循 gofmt 的逻辑。
  • 运算符和操作数之间要留空格。
  • 作为输入参数或者数组下标时,运算符和操作数之间不需要空格,紧凑展示。
var i int = 1 + 2 					// 运算符和操作数之间要留空格
v := []float64{1.0, 2.0, 3.0}[i-i]  // i-i 作为下标不留空格
fmt.Printf("%f\n", v+1)				// v+1 作为入参不要留空格
  • 不要添加没必要的括号。
// Bad
if foo && (int(bar) > 0) {
	// ...
}

// Good
if foo && int(bar) > 0 {
	// ...
}

行数

  • 函数长度不能超过 80 行。
  • 文件长度不能超过 800 行。

3.字符串

  • 字符串字面量
    Go 支持使用原始字符串字面值,可以使用反引号来表示原生字符串。在需要转义的场景下,我们应该尽量使用使用反引号表示字符串,避免转义。
// Bad
wantError := "unknown name:\"test\""

// Good
wantError := `unknown error:"test"`
  • 不要使用字符串表示 list 和 map 结构。
// Bad
var Receivers = "tom,jerry,spike"

// Good
var Receivers = []string{"tom","jerry","spike"}
  • 字符串拼接

行内字符串拼接时,出于性能的考虑,待拼接字符串数量较少(<=3)且不涉及类型转换时,使用运算符 + 而非fmt.Sprintf()

// Bad
str := fmt.Sprintf("rsp code is %v", code)
str := "rsp code is " + code " and msg is" + msg  

// Good
str := "rsp code is " + code
str :=  fmt.Sprintf("rsp code is %v and msg is %v", code, msg)

4.依赖管理

依赖规范

  • go1.11 以上必须使用 go modules 模式。
go mod init git.code.oa.com/group/myrepo
  • 使用 go modules 作为依赖管理的项目不要提交 vendor 目录。
  • 使用 go modules 管理依赖的项目, go.sum文件必须提交,不要添加到.gitignore规则中。

import 规范

  • 使用 goimports 工具自动格式化引入的包名,import 规范原则上以 goimports 规则为准。

goimports 会自动添加依赖的包,并移除未引用的包。把依赖包按字母序升序排列,并对包进行分组管理。通过空行隔开,默认分为标准库包和非标准库包(第三方包和内部包)。

  • 导入的包按照先后顺序应该分为三组:
    • 标准包
    • 外部包
    • 内部包

带域名的包名都属于外部包,如 github.com/xxx/xxx。内部包是指不能被外部 import 的包。

// Bad
import (
	"fmt"
	"os"
	"go.uber.org/atomic"
	"golang.org/x/sync/errgroup"
	"myproject/models"
    "myproject/controller"
)

// Good
import (
	"encoding/json"
    "strings"
	
	"go.uber.org/atomic"
	"golang.org/x/sync/errgroup"
	
	"myproject/models"
    "myproject/controller"
)
  • 不要使用相对路径导入内部包,应该使用完整的路径引入包。
// Bad
import (
    "../net"
)

// Good
import (
    "xxxx.com/proj/net"
)
  • 必要时给包起个别名

包名和 git 路径名不一致时,或者多个相同包名冲突时,使用别名代替会有更好的可读性。

// Bad
import (
	elastic "github.com/olivere/elastic/v7"
)

// Good
import (
	elastic "github.com/olivere/elastic/v7"
)
  • 通用的功能包,应该放在 public 目录下,而不是具体业务目录下。
// Bad
import "github.com/xxxxxxxx/XXXServer/pkg/formatlog"

// Good
import "github.com/xxxxxxxx/utils/formatlog"
  • import . 只能用于测试文件,且必须是为了解决循环依赖,才能使用。
package foo_test

import (
	"bar/testutil" // also imports "foo"
	. "foo"
)

在这种情况下,测试文件不能在包 foo 中,因为它使用 bar/testutil,后者导入 foo。所以我们使用import .形式导入包 foo,让测试文件假装是包 foo 的一部分,尽管它不是。除了这一种情况,不要使用import .,因为它使程序难以阅读,比如使用 Baz 这样的标识符,不知道其是顶级标识符还是导入包中的顶级标识符。

  • 引入第三方包要慎重。

如引入 Github 上的包要确认活跃度,不知名的包可能会被下架或出现不兼容的版本升级情况,必要情况下可 fork 一份。

// 该包已经 404 了。
github.com/astaxie/beego/orm

5.初始化

初始化 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 c = 1
var d = 2

type Area float64
type Volume float64

// Good
const (
  a = 1
  b = 2
)

var (
  c = 1
  d = 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 和其他形式的确定性预计算的优化。

6.错误处理

书写风格

  • 与标准库风格保持一致,首字母小写且不加结束标点符号。
// Bad
var ErrRecordNotFound = errors.New("Record not found")
var ErrRecordNotFound = errors.New("record not found.")
var ErrRecordNotFound = errors.New("Record not found.")

// Good
var ErrRecordNotFound = errors.New("record not found")

error 处理

  • 需显示处理 error。

如果 error 作为函数的值返回,必须对 error 进行处理,或使用空白标识符忽略。对于defer xx.Close()可以不用显式处理。

  • error 作为函数的返回值且有多个返回值的时候,error 必须是最后一个参数
// Bad
func do() (error, int) {
}

// Good
func do() (int, error) {
}
  • 采用独立的错误流进行处理。
// Bad
if err != nil {
    // handle error
} else {
    // normal code
}

// Good
if err != nil {
    // handle error
    return // or continue, etc.
}
// normal code
  • Fail Fast 原则。

如果出现失败应该立即返回error,如果继续处理,则属于特殊情况需要添加注释。

  • 如果函数返回值需用于初始化其他变量,则采用下面的方式。
x, err := f()
if err != nil {
    // error handling
    return // or continue, etc.
}
// use x
  • 错误判断独立处理,不与其他变量组合判断。

一个可能引发的问题就是 err 如果为 nil,但是满足其他逻辑进入到 if 块内,读取 err 值将引发 panic。

// Bad
x, y, err := f()
if err != nil || y == nil {
    return err   // 当y与err都为空时,函数的调用者会出现错误的调用逻辑
}

// Good
x, y, err := f()
if err != nil {
    return err
}
if y == nil {
    return fmt.Errorf("some error")
}
  • 生成带参数的 error 使用 fmt.Errorf。
// Bad
errors.New(fmt.Sprintf("module xxx: %v",err))

// Good
fmt.Errorf("module xxx: %v", err)
  • 不要包装系统调用错误,并给出一些没意义的附加信息。
// Bad
err := exe.Run()
if err != nil {
    return fmt.Errorf("run error %s", err.Error())
}

// Good
return exe.Run()

panic 处理

  • 不要随便 panic。

在业务逻辑处理中禁止使用 panic。因为 panic 是级联失败(cascading failures)的主要根源。如果发生错误,该函数应该返回错误,让调用方决定如何处理它。

// Bad
func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

// Good
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)
  }
}

panic/recover 不是错误处理的合适策略,仅当发生不可恢复的异常(如 nil 引用)时,才可以 panic。

在 main 包中程序初始化是一个例外,如程序启动时,文件无法打开或数据库无法连接导致程序无法正常运行可使用 panic。

对于其它的包,可导出的接口也不能有 panic。

  • 在 main 包中使用 log.Fatal 或 log.Fatalf 结束程序而不是 panic。

如果 main 中需要使用 panic,建议使用 log.Fatal 或 log.Fatalf 来取代 panic,因为这样可以记录错误的同时结束程序,方便排查问题。

  • panic 只能在当前 Goroutine 被捕获。

panic 捕获最晚要在当前 Goroutine 最顶层将其捕获,在其他 Goroutine 中无法捕获当前 Goroutine 的 panic。每个自行启动的 Goroutine,必须在入口处捕获 panic,并打印详细堆栈信息或进行其它处理。

下面是一个反面示例,其他 Goroutine 中无法捕获当前 Goroutine 的 panic。

package main

import (
	"fmt"
	"time"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	go func() {
		fmt.Println("======begin work======")
		panic("nil pointer exception")
	}()
	time.Sleep(1 * time.Second)
	fmt.Println("======after work======")
}

程序将意外终止并输出:

======begin work======
panic: nil pointer exception

goroutine 6 [running]:
main.main.func2()
        /Users/dablelv/work/code/test/main.go:16 +0x65
created by main.main
        /Users/dablelv/work/code/test/main.go:14 +0x48

recover 处理

  • recover 用于捕获 runtime 的异常,禁止滥用 recover。
  • recover 只有在 defer 中调用才会生效。

必须在 defer 中使用,一般用来捕获程序运行期间发生异常抛出的 panic 或程序主动抛出的 panic。

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 越界运行时抛出的 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])
}

运行结果:

2022/03/27 10:48:42 exec panic error:  runtime error: index out of range [4] with length 3
2022/03/27 10:48:42 exec panic error:  44

类型断言

  • 类型断言使用 comma ok 式

类型断言的单个返回值形式如果断言失败将产生 panic。因此,请始终使用 comma ok 式。如果不关心是否成功,ok 可显示使用空标识符(下划线)忽略。

// Bad
t := i.(string)

// Good
t, ok := i.(string)
if !ok {
    // 优雅地处理错误。
}

// 如果不关心是否成功,可显示忽略 ok。
t, _ := i.(string)

7.注释

在编码阶段同步写好类型、变量、函数、包注释,注释可以通过godoc导出生成文档。

程序中每一个被导出的(大写的)名字,都应该有一个文档注释。

所有注释掉的代码在提交 Code Review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。

通用

  • 不要用注释删除代码。
  • // 后面要有空格。
  • 中英文之间应该有空格。
  • 特殊实现需要注释。
  • 注释结束添加点或句号,参考标准库源码注释。

包注释

  • 每个包都应该有一个包注释。
  • 包如果有多个 go 文件,只需要出现在一个 go 文件中(一般是和包同名的文件)即可。
  • 格式为:“// Package 包名 包信息描述”。
// 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 */)

对于上面的示例代码,还有一种更好的处理方式是将上面的 bool 类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)。

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

func printInfo(name string, region Region, status Status)
  • 非导出的函数,如果比较简单,不需要注释。
  • 解析函数应该注明解析字符串的范例,并明确不能处理的异常情况。

结构体注释

  • 每个需要导出的自定义结构体或者接口都必须有注释说明。

注释对结构进行简要介绍,放在结构体定义的前一行。格式为:“// 结构体名 结构体信息描述”。

  • 必要情况下字段给出注释。

结构体内的可导出成员变量名,如果是个生僻词或意义不明确的词,必须要单独给出注释,放在成员变量的前一行或同一行的末尾。

// User 用户结构定义了用户基础信息
type User struct {
    Name  string
    Email string
    Demographic string // 族群
}

变量和常量注释

  • 每个需要导出的变量和常量都必须有注释说明。

注释对变量和常量进行简要介绍,放在常量或变量定义的前一行。独行注释格式为:“// 变量名 描述”,斜线后面紧跟一个空格。

// FlagConfigFile 配置文件的命令行参数名。
const FlagConfigFile = "--config"

// FullName 返回指定用户名的完整名称。
var FullName = func(username string) string {
    return fmt.Sprintf("fake-%s", username)
}
  • 大块变量或常量定义时的注释方式。

块注释即在代码块前给出一个总的说明,然后每行变量或常量的末尾给出详细注释,这样看起来更加简洁。

// 命令行参数。
const (
    FlagConfigFile1 = "--config" // 配置文件的命令行参数名 1。
    FlagConfigFile2 = "--config" // 配置文件的命令行参数名 2。
    FlagConfigFile3 = "--config" // 配置文件的命令行参数名 3。
    FlagConfigFile4 = "--config" // 配置文件的命令行参数名 4。
)
  • 命名清晰的地方,不要添加无意义的注释。

类型注释

  • 每个需要导出的类型定义(type definition)和类型别名(type aliases)都必须有注释说明。
  • 该注释对类型进行简要介绍,放在定义的前一行。
  • 格式为:“// 类型名 描述”。
// StorageClass 存储类型
type StorageClass string

// FakeTime 标准库时间的类型别名
type FakeTime = time.Time

接口注释

  • 导出与非导出接口均需要有注释。
  • 需要描述谁,在什么场景下,如何使用接口。

8.命名规范

命名是代码规范中很重要的一部分,统一的命名规范有利于提高代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。

通用规则

  • 不要用宽泛、无意义的名字,如:

    • util
    • helper
    • info
    • common
  • 缩略语要么全小写,要么全大写。

// Bad
Md5
Uid

// Good
MD5
md5
UID
uid
  • 非缩略语则应该使用驼峰命名。
  • 不要使用2/4来表达英文 to/for。
  • 如无必要,不要起和包相同的名字。

项目命名

  • 小写,如果有多个单词使用连字符分隔。
// Bad
GoEcharts
goecharts
goEcharts

// Good
go-echarts

包命名

  • 保持 package 的名字和目录一致。
  • 尽量采取有意义、简短的包名,尽量不要和标准库冲突。
  • 包名应该为小写单词,不要使用下划线或者混合大小写,使用多级目录来划分层级。
  • 简单明了的包命名,如:time、list、http。
  • 不用复数。如 net/url 而不是net/urls。
  • 包名谨慎使用缩写。当缩写是程序员广泛熟知的词时,可以使用缩写。例如:
    • strconv (string conversion)
    • syscall (system call)
    • fmt (formatted I/O)
  • 不要使用大而全的无意义包名。

util、common、misc、global。package 名字应该追求清晰且越来越收敛,符合‘单一职责’原则,而不是像common一样,什么都能往里面放,越来越膨胀,让依赖关系变得复杂,不利于阅读、复用、重构。注意,xxx/utils/encryption这样的包名是允许的。

  • 只有一个源文件的包,包名应该和文件名保持一致。
  • 不要轻易使用别名。

更多可参考 Package names - The Go BlogStyle guideline for Go packages

文件命名

  • 采用有意义、简短的文件名。
  • 文件名应该采用小写,并且使用下划线分割各个单词。

函数命名

  • 函数名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。
  • 代码生成工具自动生成的代码可排除此规则(如协议生成文件 xxx.pb.go,gotests 工具自动生成文件 xxx_test.go 里面的下划线)。
  • 函数应该以动词开头。
// Bad
func panicLinesParsing(){}
func (f VerifyFlow) DataETL(ctx context.Context, datas []Data){}

// Good
func parsePanicLines(){}
func (f VerifyFlow) ETLData(ctx context.Context, datas []Data){}

结构体命名

  • 采用驼峰命名方式,首字母根据访问控制采用大写或者小写。
  • 结构体名应该是名词或名词短语,如 Customer、WikiPage、Account、AddressParser,它不应是动词。
  • 避免使用 Data、Info 这类意义太宽泛的结构体名。
  • 结构体的定义和初始化格式采用多行。
// User 多行定义。
type User struct {
    Name  string
    Email string
}

// 多行初始化。
u := User{
    UserName: "john",
    Email:    "john@example.com",
}

接口命名

  • 命名规则基本保持和结构体命名规则一致。
  • 单个函数的接口名以 er 作为后缀,例如 Reader,Writer。
// Reader 字节数组读取接口。
type Reader interface {
    // Read 读取整个给定的字节数据并返回读取的长度
    Read(p []byte) (n int, err error)
}
  • 两个函数的接口名综合两个函数名。
  • 三个以上函数的接口名,类似于结构体名。
// Car 小汽车结构申明。
type Car interface {
    // Start ...
    Start([]byte)
    // Stop ...
    Stop() error
    // Recover ...
    Recover()
}

量命名

通用

  • 量名不应该以类型作为前缀/后缀。
// map
filterHandlerMap -> opToHandler

// slice
uidSlice -> uids

// array
uidArray -> uids 

// 二维切片或数组。
// 比如多个班级下的学生ID。
uidSliceSlice -> classesUIDs
  • 量名应该是名词,进行时和过去式可以做形容词,成为量名的一部分。
  • 特有名词时,需遵循以下规则:
    • 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient;
    • 其他情况都应该使用该名词原有的写法,如 APIClient、repoID、UserID;
    • 错误示例:UrlArray,应该写成 urlArray 或者 URLArray;
    • 详细的专有名词列表可参考这里
  • 尽量不要用拼音命名。
  • 量名遵循驼峰式,根据是否导出决定首字母大小写。
// 导出全局变量。
var AppVersion = "1.0.0"
// 未导出全局变量。
var appVersion = "1.0.0"

// 导出全局常量。
const AppVersion = "1.0.0"
// 未导出全局常量。
const appVersion = "1.0.0"
  • 若量类型为 bool 类型,则名称应以 Has,Is,Can 或 Allow 等单词开头。
  • 私有量和局部量规范一致,均以小写字母开头。
  • 作用域较小的名字(局部变量/函数参数),尽量使用简短的名字。

如 c 比 lineCount 要好,i 比 sliceIndex 要好。

// Bad
lineCount := getlineCount()
	for sliceIndex := range msgs {
}

// Good
c := getlineCount()
	for i := range msgs {
}
  • 作用域较大的名字(全局变量),不要使用缩写,要有明确的意义。

如 lineCount 要比 c 好,sliceIndex 要比 i 好。

// Bad
var c, i int

// Good
var lineCount, sliceIndex int
  • 全局量中不要包含格式化字符,否则必然违反就近原则。
// Bad
var (
	tGitHost     = "https://git.code.oa.com"
	mrCommitsUri = "/api/v3/projects/%s/merge_request/%s/commits"
)

// Good
func getMRCommitsUri() string {
	return fmt.Sprintf("/api/v3/projects/%s/merge_request/%s/commits", "foo", "bar")
}

常量命名

  • 如果是枚举类型的常量,需要先创建相应类型:
// Scheme 传输协议。
type Scheme string

// 传输协议。
const (
    HTTP Scheme = "http" 	// HTTP 明文传输协议
    HTTPS Scheme = "https" 	// HTTPS 加密传输协议
)

方法接收器命名

  • 推荐以类名第一个英文首字母的小写作为接收器的命名。
  • 接收器的名称在函数超过 20 行的时候不要用单字符。
  • 命名不能采用 me,this,self 这类易混淆名称。

错误命名

对于存储为全局变量的错误值,根据是否导出,使用前缀 Err 或 err。

var (
  // 导出以下两个错误,以便此包的用户可以将它们与errors.Is 进行匹配。
  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // 这个错误没有被导出,因为我们不想让它成为我们公共 API 的一部分。
  errNotFound = errors.New("not found")
)

对于自定义错误类型,请改用后缀 Error。

// 这个错误被导出,以便这个包的用户可以将它与 errors.As 匹配。
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// 这个错误没有被导出,因为我们不想让它成为公共 API 的一部分。
// 但我们仍然可以在的包内将其和 errors.As 一起使用。
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

避免使用内置名称

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) {
	// 作用域隐藏内置 error
}

type Foo struct {
    // 虽然这些使用内置标识符的自定义字段可以编译通过,但对 error 或 string 字符串的搜索存在二义性
    error  error
    string string
}

func (f Foo) Error() error {
    // error 和 f.error 在视觉上是相似的
    return f.error
}

func (f Foo) String() string {
    // string and f.string 在视觉上是相似的
    return f.string
}

// Good
var errorMessage string

func handleErrorMessage(msg string) {
}

type Foo struct {
    // error 和 string 现在是明确的
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

9.流程控制

if

  • 最小化变量作用域。

if 接受初始化语句,尽可能缩小变量作用域。

// Bad
err := file.Chmod(0664)
if err != nil {
    return err
}

// Good
if err := file.Chmod(0664); err != nil {
    return err
}

如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。

// Bad
if data, err := ioutil.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

// Good
data, err := ioutil.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil
  • if 对两个值进行判断时,被比较的值放在左边。
// Bad
if nil != err {
    // error handling
}
if 0 == errorCode {
    // do something
}

// Good
if err != nil {
    // error handling
}   
if errorCode == 0 {
    // do something
}
  • if 对于 bool 类型的变量,应直接进行真假判断。
var allowUserLogin bool
// Bad
if allowUserLogin == true {
    // do something
}
if allowUserLogin == false {
    // do something
}

// Good
if allowUserLogin {
    // do something
}
if !allowUserLogin {
    // do something
}
  • 不必要的 else。

如果在 if 的两个分支中都设置变量,则可以将其替换为单个 if。

// Bad
var a int
if b {
  a = 100
} else {
  a = 10
}

// Good
a := 10
if b {
  a = 100
}

又如 if else 通常可以简写为 if return。

// Bad
func Foo(bar int) {
	if bar == 1 {
		// ...
	} else {
		// ...
	}
}

// Good
func Foo() {
	if bar == 1 {
		// ...
		return
	}
	// ...
}
  • 多个相似 if 用 switch 替换。
// Bad
func foo(key string) {
	if key == pathKey {
		...
	}
	if key == urlKey {
		...
	}
}

// Good
func foo(key string) {
	switch key {
	case pathKey:
		...
	case urlKey:
		...
	}
}
  • 使用 == “” 判断字符串是否为空,这样更加直观。
// Bad
if len(str) == 0 {
	...
}

// Good
if str == "" {
	...
}
  • 把简单的逻辑判断放前面,复杂的逻辑判断放后面。
  • 不要使用双重否定。
  • 判断条件较为复杂时,考虑封装成函数。
  • 使用了 else if 则需要以 else 结束。
// Bad
if foo == "a" {
	...
} else if foo == "b" {
	...
}

// Good
if foo == "a" {
	...
} else if foo == "b" {
	...
} else {
	// 需要有一个缺省处理逻辑
}

for

  • 最小化变量作用域。

for 接受初始化语句,尽可能缩小变量作用域。

// Bad
sum := 0
i := 0
for ; i < 10; i++ {
    sum += 1
}

// Good
sum := 0
for i := 0; i < 10; i++ {
    sum += 1
}
  • 循环变量的地址不要存储。

循环变量的地址指向的是同一个变量,我们可以通过赋值给一个同名的变量,通过变量逃逸,来达到取不同地址的目的。

// Bad
func main() {
	ints := []int{1, 2, 3, 4, 5}
	for _, v := range ints {
		fmt.Println(&v) // 打印的是相同的地址
	}
}

// Good
func main() {
	ints := []int{1, 2, 3, 4, 5}
	for _, v := range ints {
		v := v
		fmt.Println(&v) // 打印的是不同的地址
	}
}

range

  • 如果只需要第一项(key),就丢弃第二个(value)。
for key := range m {
    if key.expired() {
        delete(m, key)
    }
}
  • 如果只需要第二项,则把第一项置为空标识符(下划线)。
sum := 0
for _, v := range array {
    sum += v
}

switch

  • 必须要有 default。
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)
}

return

  • 尽早 return,一旦有错误发生,马上返回。
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

业务代码禁止使用 goto,其他框架或底层源码推荐尽量不用。

程序退出方式

  • 使用os.Exit或者log.Fatal*退出程序,而不是panic
  • 在 main() 中退出程序且只退出一次。

仅在 main() 函数中调用os.Exitlog.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
  }
  // ...
}

当程序的多个函数具有退出能力时会存在一些问题:
(1)不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理;
(2)难以测试:退出程序的函数也将退出调用它的测试,这使得函数很难测试,并跳过了尚未被运行的其他代码;
(3)跳过清理:当函数退出程序时,会跳过已经进入 defer 队列里的函数调用,这增加了跳过重要清理任务的风险。

10.函数

入参&返回值

  • 入参和返回值均以小写字母开头。
  • 入参和返回值个数均不能超过 5 个,如果超过,请封装成新的类型。
  • 尽量用值传递,非指针传递。
  • 类型为 map,slice,chan,interface 不要传递指针。
  • 返回值超过 3 个,或有相同类型的返回值,或者从上下文中不清楚返回值的含义,使用命名返回,其它情况不建议使用命名返回。
// Parent1 ...
func (n *Node) Parent1() *Node

// Parent2 ...
func (n *Node) Parent2() (*Node, error)

// Location ...
func (f *Foo) Location() (lat, long float64, err error)
  • 入参和返回值顺序根据关联性排在一起。
  • 尽量用 error 表示执行是否成功,而不是用 bool 或者 int。
  • 表示执行状态的返回值应该放在最后。
// Bad
ret, info := ModifyUserInfo(user)

// Good
info, ret := ModifyUserInfo(user)
  • 不要返回多个用于控制流程的状态。
// Bad
isContinue, retCode := p.processUnity()

// Good
retCode := p.processUnity()
  • 如果传入的参数通常是固定的,考虑通过实现多个函数实现默认参数。

如下面这个函数的第二个参数是没有必要的,大部分时候都是 +1,一个 IncCounter() 和一个 IncCounterN() 即可。可参考标准库包的 Split() 和 SplitN()。

metrics.IncrCounter(cntCacheMissKey, 1)
  • 批量查询函数返回值使用 slice 还是 map。

有时后我们需要根据多个 ID 查询对应的值,可能会出现部分失败的情况,如某个 ID 不存在。如果不允许部分失败,使用 slice 返回值,如果允许部分失败使用 map。

// GetUserInfoBatch 批量获取用户信息(需全部成功)。
func GetUserInfoBatch(uids ...uint64) ([]UserInfo, error) {
	...
}

// GetUserInfoBatch 批量获取用户信息(允许部分失败)。
func GetUserInfoBatch(uids ...uint64) (map[uint64]UserInfo, error) {
	...
}

成员函数

  • 如果方法不使用类的成员,应该实现为非成员函数。
  • 如果非成员函数要使用类的多个属性时,应该实现为成员函数。

局部变量

  • 如果局部变量仅被使用一次,且不能起到解释逻辑的作用时,应该删除局部变量,直接内联。
// Bad
ids := GetIDs()
Foo(ids)

// Good
Foo(GetIDs())

defer

  • 当存在资源管理时,应紧跟 defer 函数进行资源的释放。
  • 判断是否有错误发生之后,再 defer 释放资源。
resp, err := http.Get(url)
if err != nil {
    return err
}
// 如果操作成功,再 defer Close()
defer resp.Body.Close()
  • 禁止在循环中使用 defer,举例如下:
// 不要这样使用
func filterSomething(values []string) {
    for _, v := range values {
        fields, err := db.Query(v) // 示例,实际不要这么查询,防止 SQL 注入
        if err != nil {
            // ...
        }
        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
        }()
    }
}
  • 正常逻辑不应该在 defer 中执行。

减少嵌套(圈复杂度)

  • 嵌套深度不能超过4层

从函数名开始算第一层,当函数的嵌套深度超过4层,往往会导致圈复杂度过高,函数变得复杂不可读,我们可以通过拆分函数的方式来减少嵌套深度。

// 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
}
  • 单函数圈复杂度最大值 <=10。
  • 条件不满足或出现错误应尽早返回。

代码也可以优先处理条件不满足或错误的情况,尽早返回或继续循环来减少嵌套。

// Bad
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)
  }
}

// Good
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()
}

魔法字面量

  • 除了 0 和 1,不要使用魔法数字。
// Bad
func getArea(r float64) float64 {
    return 3.14 * r * r
}
func getLength(r float64) float64 {
    return 3.14 * 2 * r
}

// Good
// PI 圆周率
const PI = 3.14

func getArea(r float64) float64 {
    return PI * r * r
}

func getLength(r float64) float64 {
    return PI * 2 * r
}
  • 如果字符串字面量出现 >=2 次,则禁止使用,用一个有名称的常量代替,可读性更好。
// Bad
rsp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(req)))

// Good
const JsonContentType = "application/json"
rsp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(req)))

函数分组与顺序

  • 函数应该放在 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 {...}

11.单元测试

  • 单元测试文件名命名规范为 example_test.go。
  • 测试用例的函数名称必须以 Test 开头,例如 TestExample。
  • 单测文件行数限制是普通文件的 2 倍(1600 行)。单测函数行数限制也是普通函数的2倍(160行)。圈复杂度、列数限制、 import 分组等其他规范细节和普通文件保持一致。
  • 由于单测文件内的函数都是不对外的,所有可导出函数可以没有注释,但是结构体定义时尽量不要导出。
  • 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。
  • 表驱动测试.

使用表驱动的方式编写用例,代码看上去会更简洁。

// 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)
  })
}

12.杂项

12.1 基本类型偏执

有时候你会看到某个类里有很多基本类型字段,但是你隐约地能区分出某些字段好像应该在一起,如手机号和带区号的电话号码、描述某个日期范围的开始日期和结束日期,它们应该有一个真实的载体,如联系方式类和时间范围类,而不应全部零散放在一个大类型中。

通用场景

  • 复杂的执行/错误信息,需要定义结构体保存。
  • 出现状态/类型等字眼时,需要使用枚举。
  • 时间类型尽量使用内置定义,如 time.Second,不要使用 int。

结构体

  • 一个文件中出现多个结构体时,需要注意观察是否有重复的成员。
  • 一个结构体中,成员较多,且多个成员有明显关联关系,需要封装新的结构体。
  • 意义不明的成员变量,应该定义类型描述作用。

12.2 单一职责

包&文件

  • 需要判断当前文件是否应该归属于当前包,主要以职责进行判断。
  • 导出的函数/变量的职责必须与包&文件职责高度一致。
  • 除了包的主逻辑文件中内容允许导出,包内的辅助函数都应该是非导出的。

函数

  • 一个函数只负责一个职责。
    • 配置文件的读取,和对象初始化应该分开,不要让对象自己根据配置文件初始化,保证构造函数足够简单
    • 解析、校验、计算的逻辑应该进行分离
    • 读、写、计算的逻辑应该进行分离
    • rpc、db 相关操作需要独立封装
  • 一个函数内不应该混杂多个实现细节,需要将独立的逻辑封装成函数。
  • 一次循环尽量只做一件事,不用担心多次循环。
  • 同一层级的逻辑细节不要拆分。

12.3 goroutine

  • 启动的 goroutine 最好有 recover。

因为其他 goroutine 是无法捕当前 goroutine 抛出的异常。如果启动的 goroutine 没有 recover,很容易发生 panic 导致整个进程退出。

  • 遇到 goroutine一定要梳理清楚 goroutine 的退出机制,防止泄漏。
  • 如果要开启多个线程执行一组动作,并且要等待全部完成后继续后续逻辑,考虑使用 errgroup.Group

12.4 应用服务

  • 应用服务建议有 README.md 说明文档,介绍服务功能、使用方法、部署时的限制与要求、基础环境依赖等
  • 应用服务必须要有接口测试

12.5 常用工具

Go 本身在代码规范方面做了很多努力,很多限制都是语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错。此外 Go 还是提供了很多好用的工具帮助我们进行代码的规范。

  • gofmt ,大部分的格式问题可以通过 gofmt 解决, gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,于是所有格式有关问题,都以 gofmt 的结果为准。
  • goimports ,此工具在 gofmt 的基础上增加了自动删除和引入包。
  • go vet ,vet 工具可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前 return 的逻辑, struct 的 tag 是否符合标准等。编译前先执行代码静态分析。
  • golint ,类似 javascript 中的 jslint 的工具,主要功能就是检测代码中不规范的地方。

参考文献

Go Code Review Comments
github.com/uber-go/guide
Golang Readability 考点汇总

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Google 的 Go 语言编码风格主要规定了 Go 程序应该如何编写和格式化。这包括命名规则、代码布局、注释风格以及如何组织代码等方面。 Google 的 Go 语言编码风格的目的是使代码易于阅读和维护。通过规范的编码风格,可以使团队中的开发人员之间的代码风格保持一致,从而提高代码的可读性和可维护性。 Google 的 Go 语言编码风格的具体规则可以在以下地址查看: https://golang.org/doc/effective_go.html#formatting https://golang.org/doc/style 希望这对你有帮助。 ### 回答2: Google的Go语言编码风格中文版是一种规范化的编码指南,用于指导开发者在编写Go代码时遵循一致的风格和最佳实践。此指南旨在提高代码的可读性、可维护性和可重用性,使不同开发者的代码在语法结构和格式上保持一致。 Go语言编码风格中文版包含了对代码布局、命名规范、注释规则和错误处理等方面的要求。例如,代码布局要求使用四个空格缩进代码,每行代码不应超过80个字符,并遵循大括号换行的规则。命名规范要求使用驼峰命名法并避免使用简写或缩略词,同时使用具有明确含义的变量和函数名。注释规则要求在代码中添加清晰的注释,解释代码的功能、参数、返回值等信息。错误处理要求在代码中显式处理错误,并避免使用不明确的错误处理方式。 此外,Go语言编码风格中文版还包含了一些建议和最佳实践,例如推荐使用Go提供的标准库和语言特性,避免滥用指针和全局变量,以及使用测试驱动开发等等。 遵循Google的Go语言编码风格中文版可以帮助开发者写出一致、清晰和高质量的Go代码。通过统一的编码风格,团队成员可以更容易地理解和维护彼此的代码,减少代码出错的概率,并提高整体代码的可读性和可扩展性。 ### 回答3: Go语言是一种由Google开发的开源编程语言,具有简洁、高效、安全等特点。为了保持代码的一致性和可读性,Google提供了Go语言的编码风格指南,包括中文版。 首先,指南强调使用英文命名变量、函数和类型,并且遵循驼峰拼写法。这样做可以增加代码的可读性,方便其他开发者理解和维护代码。 其次,指南强调使用标准库提供的包和函数,而不是自己编写重复的代码。Go语言拥有丰富的标准库,通过使用标准库可以提高开发效率,减少代码的复杂性。 另外,指南还推荐使用清晰的注释,解释代码的功能和意图。良好的注释可以帮助其他开发者更好地理解代码,并且在以后的维护过程中提供指导。 指南还提供了一些建议和规则,例如使用错误处理机制代替错误码、避免过度使用全局变量、尽量避免循环依赖等。这些规则有助于提高代码的质量,降低bug的产生概率。 总的来说,Google的Go语言编码风格指南中文版旨在帮助开发者编写清晰、一致、可读性强的代码,并且遵循Go语言的最佳实践。遵循这些指南可以提高代码的可靠性、可维护性和可扩展性,对于Go语言的开发者来说是非常有价值的参考文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值