GO 代码风格指南

风格原则

​ 有一些总体原则总结了如何考虑编写可读的 Go 代码。以下是可读代码的属性,按重要性排序:

  1. 清晰:代码的目的和基本原理对读者来说是清楚的。
  2. 简单性:代码以尽可能简单的方式实现其目标。
  3. 简洁:代码具有高信噪比。
  4. 可维护性:代码的编写使其易于维护。
  5. 一致性:代码与更广泛的 Google 代码库一致。

命名

下划线

Go 中的名称通常不应包含下划线。这个原则有三个例外:

  1. 仅由生成的代码导入的包名称可能包含下划线。有关如何选择多词包名称的更多详细信息,请参阅包名称
  2. 文件中的测试、基准和示例函数名称*_test.go可能包含下划线。
  3. 与操作系统或 cgo 互操作的低级库可能会重用标识符,如syscall. 在大多数代码库中,这预计是非常罕见的。

包名

​ Go 包名称应该简短并且只包含小写字母。由多个单词组成的包名称应全部小写。例如,包tabwriter未命名为tabWriterTabWritertab_writer

​ 避免选择可能被常用局部变量名遮蔽的包名。例如,usercount是一个比 更好的包名 count,因为count是一个常用的变量名。

​ Go 包名称不应该有下划线。如果您需要导入名称中确实有一个包(通常来自生成的或第三方代码),则必须在导入时将其重命名为适合在 Go 代码中使用的名称。

一个例外是仅由生成的代码导入的包名称可能包含下划线。具体例子包括:

  • 使用_test外部测试包的后缀,例如集成测试
  • 使用包级文档示例_test的后缀

​ 避免使用无意义的包名称,例如util, utility, common,helper等。查看更多关于 所谓的“实用程序包”的信息。

​ 当导入的包被重命名(例如import foopb "path/to/foo_go_proto")时,包的本地名称必须符合上述规则,因为本地名称决定了包中的符号在文件中的引用方式。如果给定的导入在多个文件中重命名,特别是在相同或附近的包中,则应尽可能使用相同的本地名称以保持一致性。

另请参阅:https://go.dev/blog/package-names

参数别名

接收器变量名称必须是:

  • 短(通常是一两个字母的长度)
  • 类型本身的缩写
  • 一致地应用于该类型的每个接收器
长名更好的名字
func (tray Tray)func (t Tray)
func (info *ResearchInfo)func (ri *ResearchInfo)
func (this *ReportWriter)func (w *ReportWriter)
func (self *Scanner)func (s *Scanner)

常量名称

​ 常量名称必须像 Go 中的所有其他名称一样使用MixedCaps导出 的常量以大写字母开头,而未导出的常量以小写字母开头。)即使它打破了其他语言的约定,这也适用。

常量名称不应是其值的派生词,而应解释值的含义。

1
2
3
4
5
6
7
8
// Good:
const MaxPacketSize = 512

const (
    ExecuteBit = 1 << iota
    WriteBit
    ReadBit
)

不要使用非 MixedCaps 常量名称或带有K前缀的常量。

1
2
3
4
// Bad:
const MAX_PACKET_SIZE = 512
const kMaxBufferSize = 1024
const KMaxUsersPergroup = 500

根据它们的角色而不是它们的值来命名常量。如果一个常量除了它的值之外没有其他作用,那么就没有必要将它定义为一个常量。

1
2
3
4
5
6
7
// Bad:
const Twelve = 12

const (
    UserNameColumn = "username"
    GroupColumn    = "group"
)

缩写词

​ 名称中的首字母缩略词或首字母缩略词(例如,URLNATO)应该具有相同的大小写。URL应显示为URLurl(如urlPony, 或 URLPony),绝不能显示为Url. 这也适用于ID“标识符”的缩写;写appID而不是appId

  • 在具有多个首字母缩写词的名称中(例如XMLAPI,因为它包含XML and API),给定首字母缩写词中的每个字母都应该具有相同的大小写,但名称中的每个首字母缩写词不需要具有相同的大小写。
  • 在首字母缩写包含小写字母(例如DDoS, iOS, gRPC)的名称中,首字母缩写应该像在标准散文中一样出现,除非您需要为了导出性而更改第一个字母。在这些情况下,整个首字母大写应该是相同的情况(例如ddos, IOS, GRPC)。
初始主义范围正确的不正确
XML API已出口XMLAPIXmlApi, XMLApi, XmlAPI,XMLapi
XML API未出口xmlAPIxmlapi,xmlApi
iOS已出口IOSIos,IoS
iOS未出口iOSios
gRPC已出口GRPCGrpc
gRPC未出口gRPCgrpc
分布式拒绝服务已出口DDoSDDOS,Ddos
分布式拒绝服务未出口ddosdDoS,dDOS

函数名称

​ 函数和方法名称不应使用Getget前缀,除非底层概念使用单词“get”(例如 HTTP GET)。更喜欢直接以名词开头的名称,例如使用Countsover GetCounts

如果该函数涉及执行复杂的计算或执行远程调用,则可以使用不同的词(如Compute或)代替,以向读者清楚地表明函数调用可能需要时间并且可能会阻塞或失败。Fetch Get

变量名

一般的经验法则是,名称的长度应与其范围的大小成正比,并与其在该范围内使用的次数成反比。在文件范围内创建的变量可能需要多个单词,而作用域为单个内部块的变量可能是单个单词甚至只是一两个字符,以保持代码清晰并避免无关信息。

这是一个粗略的基线。这些数字准则不是严格的规则。根据上下文、清晰度简洁性应用判断。

  • 小范围是执行一两个小操作的范围,比如 1-7 行。
  • 中等范围是一些小的或一个大的操作,比如 8-15 行。
  • 大范围是一个或几个大操作,比如 15-25 行。
  • 非常大的范围是指超过一页(例如,超过 25 行)的任何内容。

在小范围内可能非常清楚的名称(例如,c对于计数器)在较大范围内可能是不够的,并且需要澄清以提醒读者其在代码中的目的。一个作用域中有很多变量,或者表示相似值或概念的变量,可能需要比作用域建议的更长的变量名称。

概念的特殊性也有助于保持变量名称的简洁。例如,假设只有一个数据库在使用,像db这样的短变量名通常可能为非常小的范围保留,即使范围非常大也可能保持完全清晰。在这种情况下,根据范围的大小,单个词 database可能是可以接受的,但不是必需的,因为这db是一个非常常见的词缩写,几乎没有其他解释。

局部变量的名称应该反映它包含的内容以及它在当前上下文中的使用方式,而不是值的来源。例如,通常情况下最佳局部变量名称与结构或协议缓冲区字段名称不同。

一般来说:

  • count或这样的单词名称options是一个很好的起点。

  • 可以添加其他词来消除相似名称的歧义,例如 userCountand projectCount

  • 不要为了节省打字而简单地放下字母。例如Sandbox比 更受欢迎Sbx,特别是对于导出的名称。

  • 从大多数变量名中 省略

    类型和类似类型的词。

    • 对于数字,userCount是比numUsersor 更好的名称usersInt
    • 对于切片,users是一个比userSlice.
    • 如果范围内有两个版本的值,则包含类似类型的限定符是可以接受的,例如,您可能将输入存储在解析值中ageStringage用于解析值。
  • 省略周围上下文清楚的词。例如,在一个UserCount方法的实现中,调用的一个局部变量 userCount很可能是多余的;count, users, 甚至c都一样可读。

单字母变量名

单字母变量名可能是减少 重复的有用工具,但也可能使代码不必要地变得不透明。将它们的使用限制在完整单词很明显以及它会重复出现以代替单字母变量的情况。

一般来说:

  • 对于方法接收者变量,首选一个字母或两个字母的名称。
  • 对常见类型使用熟悉的变量名通常很有帮助:
    • r对于一个io.Reader*http.Request
    • w对于一个io.Writerhttp.ResponseWriter
  • 单字母标识符作为整数循环变量是可接受的,特别是对于索引(例如,i)和坐标(例如,xy)。
  • 当范围很短时,缩写可以是可接受的循环标识符,例如for _, n := range nodes { ... }.

重复

​ 一段 Go 源代码应该避免不必要的重复。一个常见的来源是重复名称,其中通常包含不必要的单词或重复其上下文或类型。如果相同或相似的代码段在很近的地方多次出现,代码本身也可能是不必要的重复。

重复命名可以有多种形式,包括:

包与导出的符号名称

​ 命名导出的符号时,包的名称始终在包外可见,因此应减少或消除两者之间的冗余信息。如果一个包仅导出一种类型并且它以包本身命名,则构造函数的规范名称是Newif one is required。

**示例:**重复名称 -> 更好的名称

  • widget.NewWidget->widget.New
  • widget.NewWidgetWithName->widget.NewWithName
  • db.LoadFromDatabase->db.Load
  • goatteleportutil.CountGoatsTeleported->gtutil.CountGoatsTeleportedgoatteleport.Count
  • myteampb.MyTeamMethodRequest->mtpb.MyTeamMethodRequestmyteampb.MethodRequest

变量名与类型

​ 编译器总是知道变量的类型,并且在大多数情况下,读者也可以通过变量的使用方式清楚地知道变量是什么类型。如果一个变量的值在同一范围内出现两次,则只需明确变量的类型。

重复名称更好的名字
var numUsers intvar users int
var nameString stringvar name string
var primaryProject *Projectvar primary *Project

如果该值以多种形式出现,则可以使用像rawandparsed或底层表示这样的额外词来澄清:

1
2
3
4
5
6
// Good:
limitStr := r.FormValue("limit")
limit, err := strconv.Atoi(limitStr)
// Good:
limitRaw := r.FormValue("limit")
limit, err := strconv.Atoi(limitRaw)

外部上下文与本地名称

​ 包含来自周围上下文信息的名称通常会产生额外的噪音而没有任何好处。包名、方法名、类型名、函数名、导入路径,甚至文件名都可以提供自动限定其中所有名称的上下文。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Bad:
// In package "ads/targeting/revenue/reporting"
type AdsTargetingRevenueReport struct{}

func (p *Project) ProjectName() string
// Good:
// In package "ads/targeting/revenue/reporting"
type Report struct{}

func (p *Project) Name() string
// Bad:
// In package "sqldb"
type DBConnection struct{}
// Good:
// In package "sqldb"
type Connection struct{}

// Bad:
// In package "ads/targeting"
func Process(in *pb.FooProto) *Report {
    adsTargetingID := in.GetAdsTargetingID()
}
// Good:
// In package "ads/targeting"
func Process(in *pb.FooProto) *Report {
    id := in.GetAdsTargetingID()
}

​ 重复通常应该在符号用户的上下文中进行评估,而不是孤立地进行评估。例如,下面的代码有很多名称,在某些情况下可能没问题,但在上下文中是多余的:

1
2
3
4
5
6
7
8
9
// Bad:
func (db *DB) UserCount() (userCount int, err error) {
    var userCountInt64 int64
    if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil {
        return 0, fmt.Errorf("failed to load user count: %s", dbLoadError)
    }
    userCount = int(userCountInt64)
    return userCount, nil
}

相反,通常可以省略有关从上下文或用法中清楚的名称的信息:

1
2
3
4
5
6
7
8
// Good:
func (db *DB) UserCount() (int, error) {
    var count int64
    if err := db.Load("count(distinct users)", &count); err != nil {
        return 0, fmt.Errorf("failed to load user count: %s", err)
    }
    return int(count), nil
}

imports

导入重命名

只应重命名导入以避免与其他导入的名称冲突。(由此推论,好的包名不应该需要重命名。)在名称冲突的情况下,更愿意重命名最本地或特定于项目的导入。包的本地名称(别名)必须遵循 包命名指南,包括禁止使用下划线和大写字母。

​ 生成的 protocol buffer 包必须重命名以从名称中删除下划线,并且它们的别名必须有pb后缀。有关详细信息,请参阅 原型和存根最佳实践

1
2
3
4
// Good:
import (
    fspb "path/to/package/foo_service_go_proto"
)

导入的包名称没有有用的识别信息(例如 package v1)应该重命名以包括以前的路径组件。重命名必须与导入相同包的其他本地文件一致,并且可以包括版本号。

**注意:**最好重命名包以符合 良好的包名称,但这对于 vendored 目录中的包通常不可行。

1
2
3
4
5
// Good:
import (
    core "github.com/kubernetes/api/core/v1"
    meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1"
)

​ 如果您需要导入一个名称与您要使用的公共局部变量名称(例如url, ssh)冲突的包,并且您希望重命名该包,首选方法是使用pkg后缀(例如urlpkg)。请注意,可以使用局部变量隐藏包;仅当此类变量在范围内时仍需要使用包时,才需要重命名。

导入分组

导入应分为两组:

  • 标准库包
  • 其他(项目和销售)包
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Good:
package main

import (
    "fmt"
    "hash/adler32"
    "os"

    "github.com/dsnet/compress/flate"
    "golang.org/x/text/encoding"
    "google.golang.org/protobuf/proto"
    foopb "myproj/foo/proto/proto"
    _ "myproj/rpc/protocols/dial"
    _ "myproj/security/auth/authhooks"
)

​ 将项目包分成多个组是可以接受的,例如,如果您想要一个单独的组来重命名、import-only-for-side-effects 或另一个特殊的导入组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Good:
package main

import (
    "fmt"
    "hash/adler32"
    "os"


    "github.com/dsnet/compress/flate"
    "golang.org/x/text/encoding"
    "google.golang.org/protobuf/proto"

    foopb "myproj/foo/proto/proto"

    _ "myproj/rpc/protocols/dial"
    _ "myproj/security/auth/authhooks"
)

注意:goimports工具不支持维护可选组 - 超出标准库和 Google 导入之间强制分离所需的拆分。额外的导入子组需要作者和审稿人的注意,以保持符合状态。

​ 也是 AppEngine 应用程序的 Google 程序应该有一个单独的组用于 AppEngine 导入。

导入“空白” ( import _)

仅为了副作用import _ "package"而导入的包(使用语法 )只能在主包或需要它们的测试中导入。

此类软件包的一些示例包括:

避免在库包中导入空白,即使库间接依赖于它们。将副作用导入限制到主包有助于控制依赖性,并使得编写依赖于不同导入的测试成为可能,而不会发生冲突或浪费构建成本。

以下是此规则的唯一例外情况:

**提示:**如果您创建的库包间接依赖于生产中的副作用导入,请记录预期用途。

导入“点”( import .)

import .表单是一种语言功能,允许将从另一个包导出的标识符无条件地带到当前包中。有关更多信息,请参阅语言规范

不要在 Google 代码库使用此功能;这使得更难判断功能来自何处。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Bad:
package foo_test

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

var myThing = Bar() // Bar defined in package foo; no qualification needed.
// Good:
package foo_test

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

var myThing = foo.Bar()

错误

返回错误

​ 用于error表示函数可能会失败。按照惯例,error是最后一个结果参数。

1
2
// Good:
func Good() error { /* ... */ }

​ 返回nil错误是表示操作成功的惯用方式,否则可能会失败。如果函数返回错误,除非另有明确说明,否则调用者必须将所有非错误返回值视为未指定。通常,非错误返回值是它们的零值,但这不能假设。

1
2
3
4
5
6
7
8
// Good:
func GoodLookup() (*Result, error) {
    // ...
    if err != nil {
        return nil, err
    }
    return res, nil
}

​ 返回错误的导出函数应该使用error类型返回它们。具体的错误类型容易受到细微错误的影响:具体的nil指针可以包装到接口中,从而变成非零值(请参阅 主题上的 Go FAQ 条目)。

1
2
// Bad:
func Bad() *os.PathError { /*...*/ }

提示:带有context.Context参数的函数通常应返回一个error,以便调用者可以确定在函数运行时上下文是否被取消。

错误字符串

​ 错误字符串不应大写(除非以导出名称、专有名词或首字母缩写词开头)并且不应以标点符号结尾。这是因为错误字符串通常在打印给用户之前出现在其他上下文中。

1
2
3
4
// Bad:
err := fmt.Errorf("Something bad happened.")
// Good:
err := fmt.Errorf("something bad happened")

​ 另一方面,完整显示消息(日志记录、测试失败、API 响应或其他 UI)的样式取决于,但通常应大写。

1
2
3
4
// Good:
log.Infof("Operation aborted: %v", err)
log.Errorf("Operation aborted: %v", err)
t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err)

处理错误

​ 遇到错误的代码应该慎重选择如何处理它。_使用变量丢弃错误通常是不合适的。如果函数返回错误,请执行以下操作之一:

  • 立即处理并解决错误。
  • 将错误返回给调用者。
  • 在特殊情况下,请致电log.Fatal或(如果绝对必要) panic

注意: log.Fatalf不是标准库日志。参见 [#logging]。

​ 在极少数情况下,忽略或丢弃错误是合适的(例如(*bytes.Buffer).Write,记录的调用永远不会失败),随附的注释应该解释为什么这是安全的。

1
2
3
4
// Good:
var b *bytes.Buffer

n, _ := b.Write(p) // never returns a non-nil error

有关错误处理的更多讨论和示例,请参阅 Effective Go and best practices

带内错误

​ 在 C 和类似语言中,函数返回值(如 -1、null 或空字符串)以表示错误或缺少结果是很常见的。这称为带内错误处理。

1
2
3
// Bad:
// Lookup returns the value for key or -1 if there is no mapping for key.
func Lookup(key string) int

​ 未能检查带内错误值可能会导致错误,并将错误归因于错误的函数。

1
2
3
4
// Bad:
// The following line returns an error that Parse failed for the input value,
// whereas the failure was that there is no mapping for missingKey.
return Parse(Lookup(missingKey))

​ Go 对多个返回值的支持提供了更好的解决方案(请参阅 Effective Go 部分的多个返回值)。函数不应要求客户端检查带内错误值,而应返回一个附加值以指示其其他返回值是否有效。这个返回值可能是一个错误,也可能是一个布尔值,无需解释,应该是最终的返回值。

1
2
3
// Good:
// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

​ 此 API 可防止调用者错误写入Parse(Lookup(key))而导致编译时错误,因为Lookup(key)它有 2 个输出。

以这种方式返回错误鼓励更健壮和明确的错误处理:

1
2
3
4
5
6
// Good:
value, ok := Lookup(key)
if !ok {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

​ 一些标准库函数,如 package中的函数strings,返回带内错误值。这极大地简化了字符串操作代码,但代价是需要程序员更加勤奋。一般来说,Google 代码库中的 Go 代码应该为错误返回额外的值。

缩进错误流程

​ 在继续您的代码的其余部分之前处理错误。这通过使读者能够快速找到正常路径来提高代码的可读性。同样的逻辑适用于任何测试条件然后以终止条件结束的块(例如return,,,paniclog.Fatal

​ 如果不满足终止条件,则运行的代码应出现在if 块之后,并且不应在else子句中缩进。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Good:
if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code
// Bad:
if err != nil {
    // error handling
} else {
    // normal code that looks abnormal due to indentation
}

**提示:**如果您在多行代码中使用一个变量,通常不值得使用if-with-initializer 样式。在这些情况下,通常最好将声明移出并使用标准if 语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Good:
x, err := f()
if err != nil {
  // error handling
  return
}
// lots of code that uses x
// across multiple lines
// Bad:
if x, err := f(); err != nil {
  // error handling
  return
} else {
  // lots of code that uses x
  // across multiple lines
}

有关详细信息,请参阅Go 技巧 #1:视线TotT:通过减少嵌套 来降低代码复杂性。

文字格式语法

无切片

nil对于大多数用途,空切片和空切片之间没有功能差异。内置函数在切片上的表现lencap预期一样。nil

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Good:
import "fmt"

var s []int         // nil

fmt.Println(s)      // []
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
for range s {...}   // no-op

s = append(s, 42)
fmt.Println(s)      // [42]

​ 如果您将空切片声明为局部变量(尤其是如果它可以作为返回值的来源),则首选 nil 初始化以降低调用者出现错误的风险。

1
2
3
4
5
// Good:
var t []string

// Bad:
t := []string{}

​ 不要创建强制其客户区分 nil 和空切片的 API。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Good:
// Ping pings its targets.
// Returns hosts that successfully responded.
func Ping(hosts []string) ([]string, error) { ... }

// Bad:
// Ping pings its targets and returns a list of hosts
// that successfully responded. Can be empty if the input was empty.
// nil signifies that a system error occurred.
func Ping(hosts []string) []string { ... }

​ 在设计接口时,避免区分nil切片和nil非零长度切片,因为这会导致细微的编程错误。这通常是通过使用lento 检查是否为空来完成的,而不是== nil.

​ 此实现接受两个nil和零长度切片为“空”:

1
2
3
4
5
6
7
8
// Good:
// describeInts describes s with the given prefix, unless s is empty.
func describeInts(prefix string, s []int) {
    if len(s) == 0 {
        return
    }
    fmt.Println(prefix, s)
}

​ 而不是依赖于区别作为 API 的一部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Bad:
func maybeInts() []int { /* ... */ }

// describeInts describes s with the given prefix; pass nil to skip completely.
func describeInts(prefix string, s []int) {
  // The behavior of this function unintentionally changes depending on what
  // maybeInts() returns in 'empty' cases (nil or []int{}).
  if s == nil {
    return
  }
  fmt.Println(prefix, s)
}

describeInts("Here are some ints:", maybeInts())

有关进一步讨论,请参阅带内错误

函数格式化

函数或方法声明的签名应保留在一行中以避免缩进混淆

函数参数列表可以构成 Go 源文件中最长的几行。但是,它们先于缩进的变化,因此很难以不使后续行看起来像函数体的一部分的方式以令人困惑的方式断行:

1
2
3
4
5
6
// Bad:
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
    foo4, foo5, foo6 int) {
    foo7 := bar(foo1)
    // ...
}

请参阅最佳实践,了解一些选项,以缩短函数的调用站点,否则会有很多参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Good:
good := foo.Call(long, CallOptions{
    Names:   list,
    Of:      of,
    The:     parameters,
    Func:    all,
    Args:    on,
    Now:     separate,
    Visible: lines,
})
// Bad:
bad := foo.Call(
    long,
    list,
    of,
    parameters,
    all,
    on,
    separate,
    lines,
)

通常可以通过分解出局部变量来缩短行。

1
2
3
// Good:
local := helper(some, parameters, here)
good := foo.Call(list, of, parameters, local)

​ 同样,函数和方法调用不应仅根据行长度来分开。

1
2
3
4
5
// Good:
good := foo.Call(long, list, of, parameters, all, on, one, line)
// Bad:
bad := foo.Call(long, list, of, parameters,
    with, arbitrary, line, breaks)

​ 不要为特定的函数参数添加注释。相反,使用 选项结构或向函数文档添加更多详细信息。

1
2
3
4
5
6
7
// Good:
good := server.New(ctx, server.Options{Port: 42})
// Bad:
bad := server.New(
    ctx,
    42, // Port
)

​ 如果调用站点长得让人不舒服,请考虑重构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Good:
// Sometimes variadic arguments can be factored out
replacements := []string{
    "from", "to", // related values can be formatted adjacent to one another
    "source", "dest",
    "original", "new",
}

// Use the replacement struct as inputs to NewReplacer.
replacer := strings.NewReplacer(replacements...)

​ 如果 API 无法更改或本地调用异常(无论调用是否太长),如果有助于理解调用,则始终允许添加换行符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Good:
canvas.RenderCube(cube,
    x0, y0, z0,
    x0, y0, z1,
    x0, y1, z0,
    x0, y1, z1,
    x1, y0, z0,
    x1, y0, z1,
    x1, y1, z0,
    x1, y1, z1,
)

请注意,上例中的线条并未环绕在特定的列边界处,而是根据坐标三元组进行分组。

​ 函数内的长字符串字面值不应因行长而被打断。对于包含此类字符串的函数,可以在字符串格式后添加换行符,并且可以在下一行或后续行提供参数。关于换行符应该去哪里的决定最好基于输入的语义分组,而不是纯粹基于行的长度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Good:
log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)",
    currentCustomer, currentOffset, currentKey,
    txCustomer, txOffset, txKey)

// Bad:
log.Warningf("Database key (%q, %d, %q) incompatible in"+
    " transaction started by (%q, %d, %q)",
    currentCustomer, currentOffset, currentKey, txCustomer,
    txOffset, txKey)

条件和循环

​ 声明if不应断行;多行if子句会导致 缩进混乱

1
2
3
4
5
6
7
// Bad:
// The second if statement is aligned with the code within the if block, causing
// indentation confusion.
if db.CurrentStatusIs(db.InTransaction) &&
    db.ValuesEqual(db.TransactionKey(), row.Key()) {
    return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}

如果不需要短路行为,可以直接提取布尔操作数:

1
2
3
4
5
6
// Good:
inTransaction := db.CurrentStatusIs(db.InTransaction)
keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key())
if inTransaction && keysMatch {
    return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}

可能还可以提取其他局部变量,尤其是在条件已经重复的情况下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Good:
uid := user.GetUniqueUserID()
if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) {
    // ...
}

// Bad:
if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) {
    // ...
}

if包含闭包或多行结构文字的语句应确保大括号匹配以避免 缩进混淆

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Good:
if err := db.RunInTransaction(func(tx *db.TX) error {
    return tx.Execute(userUpdate, x, y, z)
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}

// Good:
if _, err := client.Update(ctx, &upb.UserUpdateRequest{
    ID:   userID,
    User: user,
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}

同样,不要尝试在for语句中插入人为的换行符。如果没有优雅的重构方式,你总是可以让这条线很长:

1
2
3
4
// Good:
for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ {
    // ...
}

但是,通常有:

1
2
3
4
5
6
7
// Good:
for i, max := 0, collection.Size(); i < max; i++ {
    if collection.HasPendingWriters() {
        break
    }
    // ...
}

switchcase语句也应该保持在一行中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Good:
switch good := db.TransactionStatus(); good {
case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting:
    // ...
case db.TransactionCommitted, db.NoTransaction:
    // ...
default:
    // ...
}


// Bad:
switch bad := db.TransactionStatus(); bad {
case db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting:
    // ...
case db.TransactionCommitted,
    db.NoTransaction:
    // ...
default:
    // ...
}

如果行太长,缩进所有的案例并用空行分隔它们以避免缩进混淆

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Good:
switch db.TransactionStatus() {
case
    db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting,
    db.TransactionCommitted:

    // ...
case db.NoTransaction:
    // ...
default:
    // ...
}

在比较变量和常量的条件语句中,将变量值放在相等运算符的左侧:

1
2
3
4
// Good:
if result == "foo" {
  // ...
}

取而代之的是常数首先出现的不太清晰的措辞(“Yoda style conditionals”):

1
2
3
4
// Bad:
if "foo" == result {
  // ...
}

复制

为避免意外的别名和类似错误,从另一个包复制结构时要小心。例如,sync.Mutex 不得复制诸如此类的同步对象。

bytes.Buffer类型包含一个[]byte切片,并且作为对小字符串的优化,还包含一个切片可能引用的小字节数组。如果您复制 a Buffer,则副本中的切片可能会为原始数组设置别名,从而导致后续方法调用产生令人惊讶的效果。

T通常,如果类型的方法与指针类型关联,则不要复制该类型的值*T

1
2
3
// Bad:
b1 := bytes.Buffer{}
b2 := b1

调用采用值接收器的方法可以隐藏副本。当您编写 API 时,如果您的结构包含不应复制的字段,您通常应该获取和返回指针类型。

这些是可以接受的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Good:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func New() *Record {...}

func (r *Record) Process(...) {...}

func Consumer(r *Record) {...}

但这些通常是错误的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Bad:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}


func (r Record) Process(...) {...} // Makes a copy of r.buf

func Consumer(r Record) {...} // Makes a copy of r.buf

本指南也适用于复印sync.Mutex

接口

​ Go接口通常属于使用接口类型值的包,而不是实现接口类型的包。实施包应该返回具体的(通常是指针或结构)类型。这样,无需大量重构即可将新方法添加到实现中。有关详细信息,请参阅GoTip #49:接受接口,返回具体类型

​ 不要从使用接口的 API 导出接口的测试替身实现。相反,设计 API 以便可以使用实际实现公共 API对其进行测试。有关详细信息,请参阅 GoTip #42:编写用于测试的存根。即使在使用真实实现不可行的情况下,也可能没有必要引入一个完全覆盖真实类型中所有方法的接口;消费者可以创建一个仅包含其所需方法的接口,如 GoTip #78:最小可行接口中所示。

​ 要测试使用 Stubby RPC 客户端的包,请使用真实的客户端连接。如果无法在测试中运行真实服务器,Google 的内部做法是使用内部 rpctest 包(即将推出!)获取到本地 [test double] 的真实客户端连接。

​ 不要在使用之前定义接口(请参阅 TotT:代码健康:消除 YAGNI 气味)(YAGNI: 你不需要它( You Aren’t Gonna Need It)。如果没有实际的使用示例,就很难判断一个接口是否必要,更不用说它应该包含哪些方法了。

软件开发的大部分成本是维护成本。减少维护成本的一个方法是:“只在当你真正需要它时,才去编写它”

如果包的用户不需要为他们传递不同的类型,请不要使用接口类型参数。

不要导出包的用户不需要的接口

**TODO:**在接口上写一个更深入的文档并在此处链接到它。

1
2
3
4
5
6
// Good:
package consumer // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { ... }
1
2
3
4
5
6
7
// Good:
package consumer // consumer_test.go

type fakeThinger struct{ ... }
func (t fakeThinger) Thing() bool { ... }
...
if Foo(fakeThinger{...}) == "x" { ... }
1
2
3
4
5
6
7
8
9
// Bad:
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{ ... }
func (t defaultThinger) Thing() bool { ... }

func NewThinger() Thinger { return defaultThinger{ ... } }
1
2
3
4
5
6
7
// Good:
package producer

type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }

func NewThinger() Thinger { return Thinger{ ... } }

传值

不要为了节省几个字节而将指针作为函数参数传递。

​ 如果一个函数x只读取它的参数*x,那么这个参数不应该是一个指针。这方面的常见实例包括传递一个指向字符串 ( *string) 的指针或一个指向接口值 ( *io.Reader) 的指针。在这两种情况下,值本身都是固定大小的,可以直接传递。

​ 此建议不适用于大型结构,甚至可能会增加大小的小型结构。特别是,protocol buffer 消息通常应该由指针而不是值来处理。指针类型满足 proto.Message接口(被 , 等接受proto.Marshalprotocmp.Transform,并且协议缓冲区消息可能非常大,并且通常会随着时间的推移而变大。

接收器类型

​ 方法接收者可以作为值或指针传递,就好像它是常规函数参数一样。选择哪个应该基于该方法应该属于哪个方法集

​ **正确性胜过速度或简单性。**在某些情况下,您必须使用指针值。在其他情况下,如果您对代码将如何增长没有很好的了解,则为大型类型选择指针或作为面向未来的指针,并为简单的普通旧数据使用值。

下面的列表更详细地说明了每个案例:

  • 如果接收者是一个切片并且该方法不重新切片或重新分配切片,请使用值而不是指针。

    1
    2
    3
    4
    
    // Good:
    type Buffer []byte
    
    func (b Buffer) Len() int { return len(b) }
    
  • 如果该方法需要改变接收者,则接收者必须是一个指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // Good:
    type Counter int
    
    func (c *Counter) Inc() { *c++ }
    
    // See https://pkg.go.dev/container/heap.
    type Queue []Item
    
    func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) }
    
  • 如果接收者是一个包含 无法安全复制的字段的结构,请使用指针接收者。常见的例子是sync.Mutex和其他同步类型。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // Good:
    type Counter struct {
        mu    sync.Mutex
        total int
    }
    
    func (c *Counter) Inc() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.total++
    }
    

    **提示:**检查类型的Godoc以获取有关复制是否安全的信息。

  • 如果接收器是“大”结构或数组,指针接收器可能更有效。传递结构等同于将其所有字段或元素作为参数传递给方法。如果这看起来太大而不能 按值传递,那么指针是一个不错的选择。

  • 对于将调用或与其他修改接收器的函数同时运行的方法,如果这些修改对您的方法不可见,请使用一个值;否则使用指针。

  • 如果接收器是一个结构或数组,其中任何一个元素都是指向可能发生变化的东西的指针,则更喜欢指针接收器以使读者清楚地了解可变性的意图。

    1
    2
    3
    4
    5
    6
    7
    8
    
    // Good:
    type Counter struct {
        m *Metric
    }
    
    func (c *Counter) Inc() {
        c.m.Add(1)
    }
    
  • 如果接收者是内置类型,例如整数或字符串,不需要修改,则使用值。

    1
    2
    3
    4
    
    // Good:
    type User string
    
    func (u User) String() { return string(u) }
    
  • 如果接收者是映射、函数或通道,请使用值而不是指针。

    1
    2
    3
    4
    5
    
    // Good:
    // See https://pkg.go.dev/net/http#Header.
    type Header map[string][]string
    
    func (h Header) Add(key, value string) { /* omitted */ }
    
  • 如果接收者是一个“小”数组或结构,它自然是一个没有可变字段和指针的值类型,那么值接收者通常是正确的选择。

    1
    2
    3
    4
    5
    
    // Good:
    // See https://pkg.go.dev/time#Time.
    type Time struct { /* omitted */ }
    
    func (t Time) Add(d Duration) Time { /* omitted */ }
    
  • 如有疑问,请使用指针接收器。

作为一般准则,更喜欢使类型的方法全部为指针方法或全部为值方法。

**注意:**关于将值或指针传递给函数是否会影响性能,存在很多错误信息。编译器可以选择将指针传递给堆栈上的值以及复制堆栈上的值,但在大多数情况下,这些考虑不应超过代码的可读性和正确性。当性能确实很重要时,重要的是在决定一种方法优于另一种方法之前,用一个现实的基准来分析这两种方法。

switchbreak

​ 不要在子句break末尾使用没有目标标签的switch 语句;他们是多余的。与 C 和 Java 不同,switchGo 中的子句会自动中断,并且需要一条fallthrough语句来实现 C 风格的行为。break如果您想阐明空子句的目的,请使用注释而不是。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Good:
switch x {
case "A", "B":
    buf.WriteString(x)
case "C":
    // handled outside of the switch statement
default:
    return fmt.Errorf("unknown value: %q", x)
}
// Bad:
switch x {
case "A", "B":
    buf.WriteString(x)
    break // this break is redundant
case "C":
    break // this break is redundant
default:
    return fmt.Errorf("unknown value: %q", x)
}

**注意:**如果switch子句在for循环内,则使用breakwithin switch不会退出封闭for循环。

1
2
3
4
5
6
for {
  switch x {
  case "A":
     break // exits the switch, not the loop
  }
}

for要退出封闭循环,请在语句上使用标签:

1
2
3
4
5
6
7
loop:
  for {
    switch x {
    case "A":
       break loop // exits the loop
    }
  }