本文档列出了在 Fuchsia 源代码中编写 Go 时应遵循的规范 树。这些惯例由最佳做法、项目 以及为了保持一致性而做出的一些选择
此处所述的惯例可以视为添加或修订通用政策, 有效的 Go 和 Go 代码审核注释中详细介绍了这些最佳实践。
常规
文件中声明的顺序
Go 文件中声明的组织方式千差万别, 因为通常使用一个大文件里面包含各种信息, 每个文件的内容我们在此提供了一些经验法则。
顶级声明(常量、接口、类型以及 关联的方法),因此通常会按功能组列出, 请在每个组中按以下顺序排列:
- 常量(包括枚举);
- 导出的接口、类型和函数;
- 不支持导出的类型的接口、类型和函数。
例如,fidlgen lib 中的一组功能可能是 而另一个组可能正在读取 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
。
请只导出您真正需要并且打算重复使用的内容。注意事项 在后续更改中导出相对容易,因此最好不要导出 直到必要。这样可让文档更加简洁。
如果您的类型将用于反射,例如自动创建 diffing à 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.
map
优于使用 switch
,因为在
改进代码,以便计算地图以外的内容,例如FromString
(通过搜索或预先计算 stringsToMyEnum
,效率低下)
反向映射)。
仅当枚举成员是自然默认值时,才应使用值为 0
的枚举成员。
如需了解详情,请参阅枚举 FIDL API 评分准则条目。
另一种方法是使用 cmd/stringer
生成枚举。这会生成
但需要的维护工作量增加一点。您可以使用
此方法可在枚举稳定后使用。
集
如需表示集,请使用映射到空结构体的映射:
allMembers := map[uint32]struct{}{
5: {},
8: {},
}
将 Slice 实例化
如果您要使用字面量定义 Slice,那么 []T{ ... }
就是
就这么简单。
否则,使用 var slice []T
创建一个空 Slice,即不要使用
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
,即返回一个封装容器,其中包含 有关操作结果的结构化信息。
明确规定何时不可能使用“where-flavor-where”, 但有一些适用的一般原则
在多态中使用时,在 Go 中使用 nil
时很容易出错
上下文:nil
不是一个单位,存在许多nil
,但每个都不等于
其他!因此,最好返回
额外的 ok
布尔值(当使用此方法可能会以多态方式使用时)
上下文。仅使用 nil
(而非呈现)作为指示该方法何时将
只用在单态语境中。换句话说,使用模式 (2) 可
比 (1) 更简单,并且当所有调用方都使用
函数,而不必通过接口的视角观察返回值。
返回 error
表示发生了问题,调用方
并对所发生的情况作出解释应为调用方提供
指出错误,可能会在此过程中封装错误,
错误处理和恢复。与返回 ok
的方法的主要区别
布尔值(或 nil
数据),即在返回 error
时,该方法会
声称自己足够了解上下文,能够将所发生的情况分类为
错误。
例如,LookupUser(user_id uint64)
会倾向于返回 ok 布尔值
如果未找到用户,则 LookupCountryCode(code IsoCountryCode)
会优先考虑
返回 error
,以指明某个国家/地区配置有误
无法查找(或请求的国家/地区无效)。
如果 方法很复杂,并且需要使用结构化数据进行描述。例如, 适合使用数据封装容器的验证 API 是遍历 XML 文档,并返回错误路径列表,其中每个路径都含有警告或错误
静态类型断言
Go 使用结构子类型,即类型是否实现接口 取决于其结构(而不是通过声明)。大家可以很容易地想, 类型会实现某种接口,但实际上它并未实现。这会导致 使用位置在一定距离内出现中断,导致编译器混淆 错误。
为弥补这一问题,需要为所有接口(包括 类型实现(是的,标准库中的实现 too):
var _ MyInterface = (*myImplementation)(nil)
这会创建一个类型化 nil,其赋值会强制编译器检查类型 一致性。
通常,我们有许多实现必须实现相同的接口, 例如代表抽象语法树,其中各个节点都实现 表达式接口。在这些情况下,您应该在 因此还要记录所有预期的子类型:
var _ = []MyInterface{
(*myImplementation)(nil),
(*myOtherImplementation)(nil),
...
}
关于应该放置这些类型断言的经验法则如下: 如下:
- 在以下情况下,最好在实现下面设置一个类型断言: 必须独立实施,例如 此处;
- 在所有实现中,首选接口下的已分组类型断言
需要结合使用(例如
Expression
的表达式节点) 表示 AST 的接口),例如 此处。
嵌入
嵌入是 Go 中的一个非常强大的概念。善加利用。已读 嵌入。
嵌入接口或结构体类型时,应在 其所属的接口或结构体类型,即嵌入的类型 显示为第一个字段。
指针与值
对于方法接收器,请参阅指针与值 和接收器类型。 要点是保持内容的一致性,但如果有疑问,则使用指针接收器。 对于给定的类型,应始终保持一致的传递方式,即 始终按值传递、始终通过引用传递 在该类型(使用值或指针接收器)上定义方法。
另外值得注意的是,按值传递结构体,认为调用方 是不正确的。你可以轻松保存地图、Slice 或 引用对象,从而更改这些对象。因此在 Go 中 将“按值传递”视为常量。
如需具体建议,请参阅实现接口。 方法接收器。
实现接口
通常使用指针接收器实现接口;实现接口 使用值接收器的接口会使该接口同时由值和 指针,这会使尝试枚举可能的 接口的实现查看示例 fxrev.dev/269371。
在某些情况下,使用值实现接口 接收器是否合适:
- 当类型从未用作指针时。例如,自定义排序
方法是定义
type mySlice []myElement
并实现sort.Interface
,日期:mySlice
。通过 永远不会使用类型*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
,优先使用
操作数。这样做的好处是允许 go vet
检查动词是否是
操作数所支持的
如果操作数是不实现 fmt.Stringer
的结构体,
无论如何,%v
都不太可能产生良好的结果;%+v
或%#v
可能相差很多
更好的选择。
此规则的一个常见例外情况是操作数在运行时可能为 nil
:
%v
可以良好地处理 nil
值,但并不是所有其他动词。
引用字符串时,请使用 %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
后缀,除非您要编写外部测试,否则请参阅
testing 软件包。
在进行集成级别测试或与 API 交互时,更喜欢进行外部测试 被测软件包中仅导出的那部分内容。
使用以 _test
结尾的软件包来提供编译示例也很有趣。
代码,可按原样使用软件包选择器复制。例如
示例
代码
及其
来源。
测试实用程序
测试实用程序是在软件包中使用的帮助程序,有助于进行测试。时间是
“测试代码”因为它位于带有 _test.go
后缀的文件中。地点
testutils_test.go
文件中包含测试实用程序;这个惯例是
由编译器解译,使此代码不包含在非测试内容中
二进制文件,确保其不会在测试之外使用。
测试软件包
测试软件包是一个侧重于简化测试编写过程的库。它
是“production code”— 不以 _test.go
后缀结尾,而仅用于
可以在测试代码中使用
测试软件包的命名惯例是使用“test”后缀替换为 它用于测试的软件包名称标准库中的示例包括 httptest 和 iotest,位于 Fuchsia 树中 fidlgentest 和 emulatortest。