本文档列出了在 Fuchsia 源代码树中编写 Go 时应遵循的规范。这些规范结合了最佳实践、项目偏好设置以及为保持一致性而做出的一些选择。
此处所述的惯例可被视为添加或修改常用最佳实践(如有效的 Go 和 Go 代码审核注释中详述)。
常规
文件中声明的顺序
Go 文件中的声明的组织方式千差万别,特别是因为经常使用一个大型文件包含所有内容,或者使用许多较小的文件包含各元素。我们在此提供一些经验法则。
对于顶级声明(常量、接口、类型及其关联方法)的排序,通常按功能组列出,每个组中具有以下顺序:
- 常量(包括枚举);
- 导出的接口、类型和函数;
- 支持导出的类型的未导出的接口、类型和函数。
例如,fidlgen 库中的一组功能可能是处理名称,而另一组功能可能在读取 JSON IR 并对其进行反规范化。随着内容越来越多,此类分组很适合成为其自己的单独文件。
对于包含方法的类型,通常按顺序列出:
- 类型声明(例如
type myImpl struct { ...
); - 类型断言(如果有);
- 方法,首先是导出的方法,然后是未导出的方法;
- 最好将实现接口的方法(如果适用)分组,即对接口
Foo
的所有方法进行分组,然后对Bar
接口的所有方法进行分组; - 通常情况下,尽管已导出,
String()
方法仍是最后的方法。
有关枚举,请参阅如何定义枚举。
命名规范
Go 对如何命名非常有想法。一些惯例:
var err error
var buf bytes.Buffer
var buf strings.Builder
var mu sync.Mutex
var wg sync.WaitGroup
var ctx context.Context
_, ok := someMap[someKey]
_, ok := someValue.(someType)
定义方法时,请确保接收器的名称一致(如果存在)。另请参阅指针与值。
避免导出
在 Go 中,可以取消导出或导出标识符(不使用公共/私有术语)。导出的声明以大写字母 MyImportantThing
开头,而未导出的声明以小写字母 myLocalThing
开头。
仅导出您确实需要并且打算重复使用的内容。请注意,在后续更改中导出相对容易,因此除非必要,否则请勿导出。这样可以让文档更加简洁。
如果您的类型将用于反射(例如在模板中或自动差异 à la go-cmp 中),可以有一个包含导出字段的未导出的标识符。同样,除非您必须这样做,否则请优先选择未导出的字段。
局部变量与类型同名不会有什么问题。如果您为导出的类型编写 server := Server{...}
,则 server :=
server{...}
可以用于未导出的类型。
切勿使用已命名的返回值
语义混乱,并且容易出错。即使这在某个时间点很适合使用(罕见且可疑),但演变通常会导致情况不再如此,这样一来,差异会大于实际变化。
您将看到如下建议:“可以对此特殊情况使用已命名的返回值”或“该特殊情况非常适合已命名的返回值”。我们的规则比较简单,绝不使用已命名的返回值。
(在定义接口时,可以对内容进行命名。这与实现中的命名返回不同,它没有语义方面的影响。)
定义枚举
用于定义枚举的标准模式为:
type myEnumType int
const (
_ myEnumType = iota
myFirstEnumValue
mySecondEnumValue
)
只有在使用 iota
时才能省略该类型。如果指定显式值,则必须重复该类型:
const (
_ myEnumType = iota
myFirstEnumValue
mySecondEnumValue
// ...
myCombinedValue myEnumType = 100
)
此外,如果您希望此枚举具有文本表示形式,请执行以下操作:
var myEnumTypeStrings = map[myEnumType]string{
myFirstEnumValue: "myFirstEnumValue",
mySecondEnumValue: "mySecondEnumValue",
}
func (val myEnumType) String() string {
if fmt, ok := myEnumTypeStrings[val]; ok {
return fmt
}
return fmt.Sprintf("myEnumType(%d)", val)
}
例如 mdlint:
TokenKind。
与使用 switch
相比,首选使用 map
来改进代码以计算地图外的内容,例如 FromString
构造函数(通过搜索或预计算 stringsToMyEnum
反向映射而效率低下)。
仅当枚举成员的值为 0
时,才应使用自然默认值的枚举成员。如需了解详情,请参阅枚举 FIDL API 评分准则条目。
另一种方法是使用 cmd/stringer
生成枚举。这样可以生成更高效的代码,但维护工作需要更多工作量。您可以在枚举稳定后使用此方法。
集
如需表示一个集,请使用映射到空结构体:
allMembers := map[uint32]struct{}{
5: {},
8: {},
}
实例化 Slice
如果您要使用字面量定义 Slice,则应该使用 []T{ ... }
。
否则,请使用 var slice []T
创建空切片,即不要使用 slice:= []T{}
。
请注意,预先分配 Slice 可以带来有意义的性能提升,可能优先于更简单的语法。如需了解实例,请参阅切片使用情况和内部结构。
实例化地图
如果您要使用字面量定义地图,则可以使用 map[K]T{ ... }
。
否则,请使用 make(map[string]struct{})
创建空映射,即不要使用 myEmptyMap := map[K]T{}
。
非正常退货
当函数需要传达非正常返回(即“失败”)时,有以下几种模式:
func doSomething(...) (data, bool)
,即返回数据或 false。func doSomething(...) *data
,即返回数据或 nil。func doSomething(...) (data, error)
,即返回数据或错误。func doSomething(...) dataWrapper
,即返回一个封装容器,其中包含有关操作结果的结构信息。
关于何时不可能使用“哪种变种”的规则,给出了一些硬性规定,但有一些一般原则是适用的。
在多态上下文中使用时,在 Go 中依赖于 nil
与当前存在容易出错:nil
不是一个单元,许多 nil
并且它们彼此不相等!因此,当可能会在多态上下文中使用该方法时,最好返回额外的 ok
布尔值。仅当相应方法将仅用于单态上下文时,才使用 nil
与当前值作为指示。换句话说,使用模式 (2) 比 (1) 简单,并且当所有调用方都将使用函数而不通过接口的视角查看返回值时,使用模式是很好的替代方法。
返回 error
表示发生了问题,调用方会收到所发生情况的说明。调用方应以冒泡的方式显示错误,可能在此过程中封装它,或执行某种错误处理和恢复。与返回 ok
布尔值(或 nil
数据)的方法的一个关键区别在于,在返回 error
时,该方法会断言自身对上下文有充分的了解,以便将发生的情况归类为错误。
例如,如果未找到用户,LookupUser(user_id uint64)
会优先返回 ok 布尔值,而 LookupCountryCode(code IsoCountryCode)
则会在无法查找国家/地区(或请求了无效国家/地区)时返回 error
以标识配置有误。
如果方法的结果很复杂,并且需要结构化数据来描述,则应考虑返回某种封装容器。例如,使用数据封装容器的自然候选是一个验证 API,该 API 会遍历 XML 文档并返回错误路径列表,每个路径都附有警告或错误。
静态类型断言
由于 Go 使用结构子类型,也就是说,某个类型是否实现接口取决于其结构(而不是通过声明)。人们很容易认为某个类型会实现某种接口,但实际上它并未实现。这会导致距离较远、使用点发生损坏,并导致令人困惑的编译器错误。
为了解决此问题,您需要为类型实现的所有接口编写类型断言(没错,标准库中的接口也都是如此):
var _ MyInterface = (*myImplementation)(nil)
这会创建一个类型化的 nil,其赋值会强制编译器检查类型一致性。
通常情况下,有许多实现必须实现相同的接口,例如,表示一个抽象语法树,其中各个节点都实现表达式接口。在这些情况下,您应该一次执行所有类型断言,从而同时记录预期的所有子类型:
var _ = []MyInterface{
(*myImplementation)(nil),
(*myOtherImplementation)(nil),
...
}
关于应在何处放置这些类型断言的经验法则如下:
- 如果每个实现各自独立,最好在实现下方提供一个类型断言,例如此处;
- 如果所有实现都旨在协同使用(例如表示 AST 的
Expression
接口的表达式节点),请优先选择接口下方的分组类型断言,例如此处。
正在嵌入
嵌入是 Go 中非常强大的一个概念,好好利用它。如需简要了解,请参阅嵌入。
嵌入接口或结构体类型时,应首先在其封装接口或结构体类型中列出这些内容,也就是说,嵌入的类型应显示为第一个字段。
指针与值
对于方法接收器,请阅读指针与值以及接收器类型。tl;dr 是为了确保内容的一致性,但如有疑问,请使用指针接收器。对于给定的类型,应始终保持一致的传递方式(即始终按值传递、始终按引用传递),这取决于该类型上是否定义方法(使用值或指针接收器)。
另外值得注意的是,按值传递结构体并认为调用方不会对其进行更改是不正确的。您可以轻松保留对象的映射、切片或引用,并因此对这些元素执行 mutate 操作。因此,在 Go 中,认为“按值传递是常量”这种关联是错误的。
有关方法接收器的具体建议,请参阅实现接口。
实现接口
通常使用指针接收器实现接口;使用值接收器实现接口会导致该接口同时由值和指针实现,这会使尝试枚举接口可能实现的类型断言复杂化。请参阅 fxrev.dev/269371 实例。
在一些特定情况下,适合使用值接收器实现接口:
- 类型从未用作指针时。例如,自定义排序通常通过定义
type mySlice []myElement
并在mySlice
上实现sort.Interface
来完成。类型*mySlice
永远不会使用,因为[]myElement
已经是引用。点击此处查看示例。 - 不应使用类型断言或类型切换接口类型的值。例如,
Stringer
通常在值类型上实现。接受val Stringer
的函数会开启val.(type)
,这种情况不常见。
如有疑问,请始终使用指针接收器实现接口。
注释
请阅读评论,尤其是以下内容:
文档注释最好是完整的句子,可用于进行各种自动演示。第一个句子应该是一句话摘要,以所声明的名称开头。
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
对类型的文档注释可能有一个头条文章“A”“An”或“The”。Go 标准库中的所有文档均遵循这种做法,例如 // A Buffer is a
variable-sized ...
。
对于长度超过一句话的评论,风格通常首选一个总结句子,然后添加一个空行,再加上一个能够提供更多详细信息的段落。例如,请参阅 FileHeader 结构体。
封装时出错
使用 fmt.Errorf
传播错误时:
- 使用
%s
仅添加其字符串值; - 使用
%w
可允许调用方解封和观察封装的错误;请注意,%w
会将这些封装的错误纳入您的 API。
请参阅处理 Go 1.13 中的错误部分,详细了解是否换行。
在一些特定情况下,错误传播必须以符合 API 协定的方式完成,例如在 RDBMS 驱动程序中通常属于这种情况,其中返回的特定错误代码表示调用方可以从中恢复的情况。在这种情况下,您必须专门封装底层错误,而不是依赖于 fmt.Errorf
。
fmt
个动词
尽可能避免使用 %v
,首选操作数支持的特定 fmt 动词。这样做的好处是,允许 go vet
检查操作数是否确实支持动词。
如果操作数是不实现 fmt.Stringer
的结构体,则 %v
不太可能生成良好的结果;%+v
或 %#v
可能是更好的选择。
此规则的一个常见例外情况是在运行时可能为 nil
的操作数 - nil
值可以很好地由 %v
处理,但不是所有其他动词也会得到妥善处理。
在引用字符串时,请使用 %q
,而不是显式调用 strconv.Quote
。
传播错误时,请参阅错误封装。
GN 目标
Go 工具的典型 BUILD.gn
文件如下所示:
go_library("gopkg") {
sources = [
"main.go",
"main_test.go",
]
}
go_binary("foo") {
library = ":gopkg"
}
go_test("foo_test") {
library = ":gopkg"
}
如果您有嵌套软件包(并且仅在本例中),请在 go_library 中使用 name = "go.fuchsia.dev/fuchsia/<path>/..."
形式启用递归软件包源:
go_library("gopkg") {
name = "go.fuchsia.dev/fuchsia/tools/foo/..."
sources = [
"main.go",
"subdir/bar.go",
"extra/baz.go",
]
}
测试
以 _test 结尾的文件包
通常,_test.go
文件与其测试的代码位于同一软件包中(例如 package foo
),并且可以访问未导出的声明。Go 还允许为软件包名称添加 _test
后缀(例如 package foo_test
),在这种情况下,它会被编译为单独的软件包,但与主二进制文件相关联并运行。这种方法称为外部测试,与内部测试不同。请勿使用 _test
后缀命名软件包,除非您要编写外部测试,请改为参阅测试软件包。
在进行集成级测试时,请优先使用外部测试,或者与被测软件包中仅导出的部分进行交互。
使用以 _test
结尾的软件包也有助于提供经过编译的示例代码,此代码可随软件包选择器按原样复制。例如,示例代码及其源代码。
测试实用程序
测试实用程序是在软件包中使用的辅助程序,用于帮助进行测试。它属于“测试代码”,因为它位于后缀为 _test.go
的文件中。请将测试实用程序放在名为 testutils_test.go
的文件中;编译器会对此惯例进行解释,以使此代码不包含在非测试二进制文件中,从而确保不会在测试之外使用它。
测试软件包
测试软件包是一个库,其重点是简化测试的编写。它是“生产代码”,不以 _test.go
后缀结尾,只可在测试代码中使用。
测试软件包的命名惯例是使用“test”后缀,并加上用于测试的软件包名称。标准库中的示例包括 Fuchsia 树 fidlgentest 中的 httptest 和 iotest,以及 emulatortest。
其他资源
- 高效 Go
- Go 代码审核注释,请将本文视为对 Effective Go 的补充
- Go 常见问题解答
- golint
- Go at Google:软件工程服务的语言设计