使用评分准则

本文档列出了在 Fuchsia 源代码中编写 Go 时应遵循的规范 树。这些惯例由最佳做法、项目 以及为了保持一致性而做出的一些选择

此处所述的惯例可以视为添加或修订通用政策, 有效的 GoGo 代码审核注释中详细介绍了这些最佳实践。

常规

文件中声明的顺序

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{}

非正常退货

当函数需要传达非正常返回(即“失败”)时,有以下几种模式:

  1. func doSomething(...) (data, bool),即返回数据或 false。
  2. func doSomething(...) *data,即返回数据或 nil。
  3. func doSomething(...) (data, error),即返回数据或 错误。
  4. 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”后缀替换为 它用于测试的软件包名称标准库中的示例包括 httptestiotest,位于 Fuchsia 树中 fidlgentestemulatortest

其他资源