Golang编码规范1

Olivia的小跟班 Lv3

题记

本文主要用来规范自己编写Golang的代码,感谢本人实习的mentor孙晨辉(万声音乐),让我对编码规范有了新的认识。

正文

函数和方法命名

  • 函数和方法名称中通常可以省略以下内容:

    • 输入和输出的类型(当没有冲突时)
    • 方法的接收器的类型
    • 输入或输出是否为指针
  • 对于函数,不要重复包的名称。

  • 对于方法,不要重复方法接收器的名称。

  • 不要重复作为参数传递的变量的名称。

  • 不要重复返回值的名称和类型。

  • 当需要消除名称相似的函数的歧义时,它是可以包含额外的信息。

命名约定

在为函数和方法:

  • 返回某物的函数被赋予类似名词的名称。函数和方法名称应避免使用前缀 Get
  • 执行某事的函数被赋予类似动词的名称。
  • 仅因所涉及的类型而有所不同的相同函数包括名称 名称末尾的类型
  • 如果有明确的“主要”版本,则可以从名称中省略该类型对于该版本:

测试双封装和类型

  1. 创建测试辅助包:

    • 当需要为某个包创建测试替身时,可以创建一个新的Go包用于测试。一个安全的命名选择是在原始包名后加上“test”这个单词,例如,原始包名是“creditcard”,则测试包名为“creditcardtest”。

    • 例如:

      1
      package creditcardtest
  2. 简单情况下的测试替身:

    • 当只需要为某个类型(例如Service)创建测试替身时,可以使用简洁的命名方式。例如,对于Service类型的测试替身,可以直接将其命名为Stub,因为Card类型类似于普通数据类型,不需要特殊处理。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import (
      "path/to/creditcard"
      "path/to/money"
      )

      // Stub替身creditcard.Service,不提供自己的行为。
      type Stub struct{}

      func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }
  3. 多种替身行为的命名:

    • 当需要多种类型的测试替身(例如,有的替身总是成功,有的替身总是失败)时,建议根据替身的行为对其进行命名。例如,将Stub替身命名为AlwaysCharges,另一个替身命名为AlwaysDeclines,以便清晰地表示它们的行为。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // AlwaysCharges替身creditcard.Service,模拟成功。
      type AlwaysCharges struct{}

      func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil }

      // AlwaysDeclines替身creditcard.Service,模拟拒绝的情况。
      type AlwaysDeclines struct{}

      func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error {
      return creditcard.ErrDeclined
      }
  4. 多种类型的测试替身:

    • 当一个包中有多个类型需要创建测试替身时,建议使用更明确的命名方式。例如,对于ServiceStoredValue类型的替身,可以使用更明确的命名,例如StubServiceStubStoredValue

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // StubService替身creditcard.Service。
      type StubService struct{}

      func (StubService) Charge(*creditcard.Card, money.Money) error { return nil }

      // StubStoredValue替身creditcard.StoredValue。
      type StubStoredValue struct{}

      func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil }
  5. 测试中的局部变量:

    • 在测试中,当变量引用测试替身时,应根据上下文选择一个能够清晰区分测试替身和其他生产类型的名称。例如,在测试中引用了名为“spy”的测试替身,可以在其名称前加上前缀以提高清晰度,例如spyCC

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // Good:
      func TestProcessor(t *testing.T) {
      var spyCC creditcardtest.Spy

      proc := &Processor{CC: spyCC}

      // declarations omitted: card and amount
      if err := proc.Process(card, amount); err != nil {
      t.Errorf("proc.Process(card, amount) = %v, want %v", got, want)
      }

      charges := []creditcardtest.Charge{
      {Card: card, Amount: amount},
      }

      if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) {
      t.Errorf("spyCC.Charges = %v, want %v", got, want)
      }
      }

阴影

  • Stomping是指在同一作用域内覆盖已有变量的值,适用于变量值不再需要的情况。(使用:=)
  • Shadowing是指在新的作用域内声明与外部作用域同名的变量,需要小心使用,避免意外遮蔽外部变量,造成程序逻辑错误。(使用=)

Util软件包

  1. 包的命名和可读性
    • 包名称应该与包内提供的功能相关,具有信息性的名字有助于代码的可读性。
    • 避免使用过于通用或不具信息性的名称(如utilhelper等),因为它们会导致代码难以阅读,可能引发导入冲突。
  2. 导入路径和包的组织
    • 包的导入路径指定了包的位置,但包的名称对于可读性更为重要。
    • 包的组织应该根据功能相关性进行,相关的类型和方法应该被组织在同一个包内。例如,如果两个类型可能需要相互交互,可以将它们放在同一个包内。
    • 如果有几个相关的类型实现紧密耦合,可以将它们放在同一个封装中,这样可以在不公开这些细节的情况下实现耦合。不过,包中的代码可以访问包中未导出的标识符。
  3. 包的尺寸
    • 决定包的大小时,可以考虑将相关的类型放在同一个包中,但是当某些概念在逻辑上是不同的时候,可以将它们放在不同的小包中。
    • 避免将整个项目放在一个包中,因为这样可能会使包变得太大。文件的大小和组织可以根据需要进行调整,维护者可以根据需要将代码分割成多个文件。
  4. 目录布局
    • Go的目录布局通常不需要遵循“一种类型,一种文件”的约定,文件的组织可以根据维护者的需要进行调整。
    • 在Google代码库和使用Bazel的项目中,Go的目录布局与一般的开源Go项目可能有所不同,因为它们可能需要符合特定的项目结构。

Imports

  1. Proto和Stubs的导入
    • Protobuf库的导入在Go语言中有一些特殊规则,主要因为Protobuf是一种跨语言的数据序列化协议。对于生成的包名,通常有以下约定:
      • pb后缀通常用于go_proto_library规则生成的包。
      • grpc后缀通常用于go_grpc_library规则生成的包。
    • 为了避免导入冲突,一般使用一个或两个字母的前缀来重命名导入的Proto库。
  2. 导入的组织和顺序
    • 通常,导入语句被分为以下几个(或更多)块,按顺序排列:
      • 标准库导入(例如:”fmt”)
      • 非标准库导入(例如:“/path/to/somelib”
      • (可选)Protobuf导入(例如:fpb "path/to/foo_go_proto"
      • (可选)副作用导入(例如:_ "path/to/package"
    • 如果文件中不包含上述任何可选类别的导入,那么相关的导入应该放在项目导入组内。
    • 可以根据团队的需求,选择是否将gRPC导入和Protobuf导入分开组织。
  3. 导入排序工具(goimports)
    • Go语言提供了goimports工具来自动处理导入,确保符合最佳实践。该工具会将导入分组并排序,符合上述规范的两个必须组的情况下。
    • 对于可选的导入组,goimports工具没有特定规则,因此在使用可选组时,需要开发者和代码审查者保持一致,以确保导入语句的组织结构符合约定。

错误处理

  1. 错误值的处理
    • 在Go中,错误是值,它们由代码创建并由代码消费。错误可以被转化为诊断信息供人类查看,也可以被维护者使用,还可以被最终用户解释。
    • 错误消息可能会显示在各种不同的界面上,包括日志消息、错误转储和用户界面。
  2. 错误处理的原则
    • 处理(产生或消费)错误的代码应该有意识地进行处理。避免忽视或盲目传播错误返回值,而是考虑当前函数是否最适合有效地处理错误。这是一个庞大的主题,很难给出绝对的建议。需要根据判断力来处理,但要考虑以下几点:
      • 在创建错误值时,考虑是否为其提供结构。
      • 在处理错误时,考虑是否添加调用者和/或被调用者可能没有的信息。
  3. 错误结构
    • 如果调用者需要查询错误(例如,区分不同的错误条件),则给错误值提供结构,以便可以通过程序而不是通过字符串匹配来进行查询。可以使用无参的全局错误值来实现简单的结构化错误。
  4. 向错误添加信息
    • 返回错误的任何函数都应该努力使错误值有用。有时,函数可能位于调用链的中间,只是从它调用的其他函数(甚至是其他包的函数)传播错误。在这种情况下,有机会使用额外的信息对错误进行注释,但程序员应该确保错误中有足够的信息,而不是添加重复或无关的细节。
    • 避免使用%w,除非你还记录(并有测试来验证)你公开的底层错误。如果你不希望调用者调用errors.Unwrap,errors.Is等方法,就不要使用%w。
  5. 日志记录错误
    • 函数有时需要告知外部系统发生了错误,但又不希望将错误传播给调用者。在这种情况下,日志记录是一个明显的选择,但要注意日志记录的内容和方式。好的日志消息应该清楚地表达发生了什么问题,并包含有助于诊断问题的相关信息。
    • 避免重复。通常最好不要在函数内部记录错误,而是让调用者处理错误。调用者可以选择记录错误,也可以使用rate.Sometimes来限制日志记录频率。
  6. 自定义详细程度级别
    • 利用verbose logging(log.V)进行详细的日志记录,可以帮助开发和跟踪。可以建立一套关于详细程度级别的约定,例如:
      • 在V(1)级别记录少量额外信息
      • 在V(2)级别记录更多信息
      • 在V(3)级别记录大型内部状态

程序

  1. 程序初始化错误
    • 初始化期间的错误(例如,不良的标志和配置)应该向上传播到main函数,然后由log.Exit使用一个解释如何修复错误的错误信息来终止程序。在这种情况下,通常不应使用log.Fatal,因为指向检查点的堆栈跟踪可能不如人为生成的可操作消息有用。
  2. 程序检查和恐慌
    • 应该围绕错误返回值来构建标准的错误处理,而不是使用panic中止程序,特别是对于临时错误,库应该倾向于将错误返回给调用者而不是中止程序。
    • 在某些情况下,需要对不变量执行一致性检查,并且如果不变量检查失败,则终止程序。通常,只有当不变量检查的失败意味着内部状态不可恢复时才会执行此操作。在Google代码库中,最可靠的方法是调用log.Fatal。在这些情况下,使用panic是不可靠的,因为可能会发生延迟函数死锁或进一步损坏内部或外部状态的情况。
  3. **何时使用panic**:
    • 标准库在API误用时会触发panic。例如,在很多情况下,reflect在值被以被误解释的方式访问时会引发panic。这类似于在访问超出边界的切片元素时的核心语言错误触发的panic。这些panic起到了不变性检查的作用,不依赖于库,因为标准库无法访问Google代码库使用的分级日志包。
    • 另一个使用panic的情况,虽然不常见,是作为包的内部实现细节。解析器和类似的深度嵌套、紧密耦合的内部函数组可能会受益于这种设计,其中在没有价值的情况下,添加错误返回只会增加复杂性。这种设计的关键属性是这些panic不允许跨包边界传播,并且不构成包的API的一部分。

文档

  1. 参数和配置
    • 不需要在文档中列举每个参数。只需文档化那些容易出错或不明显的字段和参数,并解释它们为什么重要。
    • 文档应该考虑到可能的读者,包括维护者、新团队成员、外部用户以及将来的自己。不同的读者可能需要不同深度的信息。
  2. 上下文(Context)
    • 如果上下文参数的取消会中断所提供给的函数,而函数可能返回错误,通常错误应该是ctx.Err()
    • 当上下文行为不同或不明显时,应该明确地文档化。例如,如果函数在上下文取消时返回与ctx.Err()不同的错误,或者函数有其他可能中断它的机制,应该进行文档化。
  3. 并发性
    • Go用户默认假设只读操作是安全的,无需额外的同步。在文档中,通常可以省略关于并发性的额外说明,除非操作不明确是否只读。
    • 如果操作是不明确的只读操作或是可能是有害的,文档中应该明确说明它不是并发安全的。
    • 如果API提供了同步,应该明确指出。例如,如果一个函数是并发安全的,则应该在文档中说明。
  4. 清理
    • 如果API需要显式的清理操作,文档中应该明确指出。清理操作应该由调用者负责,而不是API内部自动处理。如果清理可能不明显,文档中应该解释如何进行清理。

预览

  1. Godoc服务器和预览
    • Go提供了一个文档服务器,建议在代码审查过程中预览生成的文档,以确保Godoc格式被正确渲染。
  2. Godoc格式
    • 文档中的段落之间需要有空行来分隔。
    • 测试文件可以包含可运行的示例,这些示例会在Godoc中与相应的文档一起显示。
    • 如果需要格式化不支持的内容(如列表和表格),可以通过额外缩进两个空格来将文本格式化为等宽字体。
    • 以大写字母开头,不包含标点符号(除括号和逗号外),并在后面跟着另一个段落的单行文本会被格式化为标题。
  3. 信号提升(Signal Boosting)
    • 有时候一行代码看起来很常见,但实际上并非如此。例如,err == nil检查相对较不常见(因为err != nil更常见)。为了突出条件的差异,可以通过添加注释来“提升”条件的信号,引起读者的注意。

变量初始化

  1. 初始化

    • 在初始化新变量时,使用:=代替var,尤其是在赋予非零值时。
    • 例如,使用i := 42代替var i = 42
  2. 非指针类型的零值

    • 对于变量的零值,可以使用var关键字来声明,例如:

      1
      2
      3
      4
      5
      var (
      coords Point
      magic [4]byte
      primes []int
      )
    • 当你想要表示一个准备好用于后续使用的空值时,应该使用变量的零值。这种方式比使用显式初始化的复合字面量更简洁。

  3. 零值初始化的应用场景

    • 当将变量用作解封包(unmarshalling)时的输出时,常常使用变量的零值进行初始化。
    • 如果结构体中有不可复制的字段(例如锁),可以将其声明为值类型以利用零值初始化。但需要注意,包含该字段的类型必须以指针的形式传递,并且该类型的方法必须使用指针接收器。
  4. 复合字面量

    • 使用复合字面量(composite literals)来声明具有初始元素或成员的值。例如:

      1
      2
      3
      4
      5
      6
      var (
      coords = Point{X: x, Y: y}
      magic = [4]byte{'I', 'W', 'A', 'D'}
      primes = []int{2, 3, 5, 7, 11}
      captains = map[string]string{"Kirk": "James Tiberius", "Picard": "Jean-Luc"}
      )
    • 当你知道变量的初始元素或成员时,可以使用复合字面量来声明变量。

  5. 指向零值的指针

    • 当你需要指向零值的指针时,可以使用空的复合字面量或new关键字。两者都可以,但是new关键字可以提醒读者,如果需要非零值,复合字面量将无法使用。
  6. 大小提示

    • 在某些情况下,可以使用大小提示来预先分配切片(slice)或映射(map)的容量。例如:

      1
      2
      3
      4
      5
      var (
      buf = make([]byte, 131072) // 预分配大小为131072的字节切片
      q = make([]Node, 0, 16) // 预分配切片的容量为16,初始长度为0
      seen = make(map[string]bool, shardSize) // 预分配map的容量为shardSize
      )
    • 使用大小提示和经验性的代码分析结合起来,可以创建性能敏感和资源高效的代码。然而,在大多数情况下,不需要使用大小提示或预分配,可以让运行时根据需要动态增长切片或映射。

    • 警告:预分配比实际需要的内存更多的内存可能会浪费内存,甚至可能影响性能。在不确定的情况下,可以参考GoTip #3: Benchmarking Go Code,通常可以默认使用零值初始化或复合字面量声明。

  7. 通道方向

    • 在函数签名中指定通道的方向是一种良好的做法。例如:

      1
      2
      3
      4
      // sum计算所有值的和。它从通道中读取值直到通道被关闭。
      func sum(values <-chan int) int {
      // ...
      }
    • 指定通道的方向可以防止一些可能的编程错误。如果没有指定方向,程序员可能会意外地在不应该关闭通道的时候关闭它,导致错误。通过指定方向,编译器可以捕获到这类简单错误,同时也有助于传达类型的所有权信息

可变参数类型

  1. 选项结构(Option Structure)
    • 选项结构是一个结构体类型,它包含函数或方法的所有或部分参数,并作为函数或方法的最后一个参数传递。
    • 选项结构的优势包括结构体字面量中包含字段和值,使其易于自我描述,不相关或默认的字段可以省略,调用者可以共享选项结构,结构体提供了更清晰的字段文档,选项结构可以随时间增长而不影响调用点。
    • 通过选项结构,可以提高函数的可读性和可维护性,特别是当函数需要大量参数时。
    • 选项结构不包括上下文(Context)参数。
  2. 可变参数选项(Variadic Options)
    • 可变参数选项允许创建返回闭包的导出函数,这些闭包可以传递给函数的可变参数部分。这些函数的参数是选项的值,返回的闭包接受一个可变参数的引用(通常是指向结构体类型的指针),并根据输入更新它。
    • 可变参数选项的优势包括:当不需要配置时,选项不占用调用点的空间,选项仍然是值,因此调用者可以共享它们,编写帮助函数并积累它们,选项可以接受多个参数(例如cartesian.Translate(dx, dy int) TransformOption),选项函数可以返回一个命名类型以在godoc中将选项组合在一起。
    • 使用可变参数选项需要编写大量的额外代码,因此只有在优势超过开销时才应该使用。

复杂命令行界面

  1. **cobra**:
    • Flag Convention: 使用 getopt 约定。
    • 特点: 在Google codebase之外比较常见,提供了许多额外的功能。
    • 使用注意: 使用 cmd.Context() 获取上下文(Context),而不是创建自己的根上下文(root context)。
    • 建议: 如果你需要较多的功能,并且不介意复杂性,可以选择 cobra
  2. **subcommands**:
    • Flag Convention: 使用Go语言的约定。
    • 特点: 简单易用,提供基本功能,适合不需要太多额外功能的情况。
    • 使用注意: subcommands 包的命令函数(command functions)应该使用 cmd.Context() 获取上下文,而不是创建自己的根上下文。
    • 建议: 如果你不需要太多额外功能,可以选择 subcommands,因为它简单易用。

测试

  1. 测试辅助函数和断言辅助函数的区分:
    • 测试辅助函数(Test helpers): 用于进行设置或清理任务的函数。这些函数中发生的任何失败都被视为环境失败,而不是被测试的代码的失败。通常在测试辅助函数中调用 t.Helper 是合适的,用于标记它们是测试辅助函数。
    • 断言辅助函数(Assertion helpers): 用于检查系统的正确性,如果不符合预期,则使测试失败。在Go中,不鼓励使用断言辅助函数。
  2. 测试的目的:
    • 测试的目的是报告被测试代码的通过/失败情况。最好的地方来使测试失败是在Test函数本身,以确保失败消息和测试逻辑清晰可见。
  3. 测试中的代码复用:
    • 如果多个独立的测试用例需要相同的验证逻辑,可以选择以下方法之一,而不是使用断言辅助函数或复杂的验证函数:
      • 将逻辑(验证和失败)内联到Test函数中,即使它很重复。这在简单情况下效果最好。
      • 如果输入相似,考虑将它们统一到一个表驱动的测试中,同时在循环中将逻辑内联。这有助于避免重复,并保持验证和失败在Test函数中的内联。
      • 如果多个调用方需要相同的验证函数但表驱动测试不合适(通常是因为输入不够简单或验证需要作为一系列操作的一部分),可以将验证函数设计为返回一个值(通常是错误),而不是将testing.T参数作为输入并用它来使测试失败。在Test中使用逻辑来决定是否失败,并提供有用的测试失败信息。也可以创建测试辅助函数来复用常见的样板设置代码。
  4. 设计可扩展的验证API:
    • 文本中介绍了如何提供用于其他人测试其代码的设施,以确保其符合你的库的要求。这种类型的测试被称为“验收测试”(Acceptance testing),主要是验证用户实现的接口是否正确,而不是具体的实现细节。
  5. 使用真实传输方式:
    • 在测试组件集成时,特别是在组件之间使用HTTP或RPC作为底层传输时,推荐使用真实的底层传输与后端的测试版本进行连接。这比手动实现客户端更可靠,因为模拟客户端行为的复杂性较高。使用生产环境的客户端与测试特定的服务器,确保测试尽可能地使用真实代码。
  6. t.Error vs. t.Fatal:
    • 在测试中,通常不应该在遇到问题时立即中止测试。然而,在某些情况下,测试不能继续进行。当测试设置失败时,特别是在测试设置帮助函数中,调用t.Fatal是适当的。如果测试表中的某个入口无法继续,应该报告相应的错误。
  7. 测试辅助函数中的错误处理:
    • 测试辅助函数可能会失败,例如设置目录和文件。当测试辅助函数失败时,通常表示测试无法继续,因此最好在辅助函数中调用t.Fatal或t.Fatalf,将失败信息包含在错误消息中。这有助于在测试失败时确定失败发生的位置和原因。
  8. 避免从独立的goroutine中调用t.Fatal:
    • 在测试中,不应该从任何goroutine(除了运行Test函数或子测试的goroutine)中调用t.FailNow、t.Fatal等函数。如果测试启动新的goroutine,它们不应该从这些goroutine内部调用这些函数。
  9. 使用结构体字面量的字段标签:
    • 在表驱动测试中,最好为每个测试用例指定字段标签。这在测试用例涉及大量垂直空间(例如超过20-30行)、相邻字段具有相同类型、以及希望省略具有零值的字段时非常有帮助。
  10. 将设置代码限定在特定测试范围内:
    • 尽可能将资源和依赖项的设置限制在特定测试用例的范围内。确保每个测试用例在修改任何全局状态后都进行恢复,以确保测试用例之间和测试用例内部的隔离性。
  11. 摊销常见测试设置:
    • 如果常见的测试设置具有以下特点,可以考虑使用sync.Once:它昂贵、仅适用于某些测试,并且不需要拆卸。使用sync.Once可以确保设置函数只会被调用一次,以便摊销设置的成本。

字符串连接

  1. 使用“+”进行简单拼接:

    • 在拼接少量字符串时,可以使用“+”操作符。这种方法在语法上最简单,无需引入其他包。

    • 示例:

      1
      key := "projectid: " + p
  2. 使用fmt.Sprintf进行格式化:

    • 在构建带有格式的复杂字符串时,推荐使用fmt.Sprintf。使用多个“+”操作符可能会使结果不太清晰。

    • 示例:

      1
      str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst)
    • 避免在构建字符串操作的输出是io.Writer时,构造临时字符串再将其发送到Writer。相反,可以使用fmt.Fprintf直接将格式化内容发送到Writer

  3. 使用strings.Builder逐步构建字符串:

    • 当需要逐步构建字符串时,推荐使用strings.Builder。与连续调用“+”和fmt.Sprintf形成更大字符串时,strings.Builder具有均摊线性时间复杂度,性能更好。

    • 示例:

      1
      2
      3
      4
      5
      b := new(strings.Builder)
      for i, d := range digitsOfPi {
      fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d)
      }
      str := b.String()
  4. 常量字符串使用反引号(`):

    • 在构建常量的多行字符串文字时,建议使用反引号(`)。这种方式更清晰和简洁。

    • 示例:

      1
      2
      usage := `Usage:
      custom_tool [args]`

全局状态和石蕊测试

  1. 不要强制客户端使用依赖于全局状态的API:
    • 库不应该暴露依赖于全局状态的API或导出包级别的变量来控制所有客户端的行为。相反,应该允许客户端创建和使用实例值。
  2. 推荐使用显式依赖传递:
    • 当功能需要维护状态时,应该允许客户端通过构造函数、函数、方法或结构字段的参数将依赖关系传递给库。这种显式依赖传递的方式更加灵活、可维护和可测试。
  3. 避免使用全局状态:
    • 不要使用全局变量、服务定位器模式、回调注册表等形式的全局状态。这样的设计会导致测试不可靠,并且在多客户端情况下可能产生混乱。
  4. 全局状态的问题:
    • 使用全局状态会引发一系列问题,例如无法在测试中使用不同的插件集、无法替换注册的插件实现、无法确保插件实例之间的隔离性、可能出现插件名称冲突等。此外,全局状态还可能导致测试无法并行运行,影响程序的健壮性和可维护性。
  5. 全局状态的形式:
    • 常见的全局状态形式包括顶级变量、服务定位器模式、回调注册表和厚客户端单例。这些形式都会引发问题,因此应该避免使用它们。
  6. 不安全的情况:
    • 当多个函数在同一程序中通过全局状态进行交互,尽管它们本质上是相互独立的(例如,由不同作者在不同目录中编写的函数)时,使用全局状态是不安全的。
    • 当独立的测试用例之间通过全局状态进行交互时,使用全局状态是不安全的。
    • 当用户试图为测试目的替换或替代全局状态,特别是替换状态的任何部分为测试替身(如stub、fake、spy或mock)时,使用全局状态是不安全的。
    • 当用户在与全局状态进行交互时必须考虑特殊的顺序要求,例如func init函数的执行顺序,标志(flags)是否已解析等时,使用全局状态是不安全的。
  7. 安全的情况:
    • 当全局状态在逻辑上是常量时,使用全局状态可能是安全的。
    • 当包的可观察行为是无状态的时,使用全局状态可能是安全的。例如,公共函数可能使用私有全局变量作为缓存,但只要调用者无法区分缓存的命中与未命中,该函数就是无状态的。
    • 当全局状态不会泄露到外部实体,比如辅助进程或共享文件系统中时,使用全局状态可能是安全的。
    • 当不需要可预测的行为时,使用全局状态可能是安全的。
  8. 提供默认实例:
    • 虽然不推荐使用,但在某些情况下,如果需要为用户最大程度地提供便利性,可以提供使用包级别状态的简化API。
    • 在这种情况下,包必须允许客户端创建隔离的包类型实例。
    • 使用全局状态的公共API必须是对以前API的简单代理。例如,http.Handle在内部调用(*http.ServeMux).Handle,后者使用包级别变量http.DefaultServeMux
    • 除非库正在进行重构以支持依赖传递,否则这种包级别的API只能由二进制构建目标使用,而不能由库使用。可以被其他包导入的基础设施库不能依赖导入包的包级别状态。

参考文章

Go Style Best Practices

  • 标题: Golang编码规范1
  • 作者: Olivia的小跟班
  • 创建于 : 2023-11-06 13:55:10
  • 更新于 : 2023-11-06 17:34:30
  • 链接: https://www.youandgentleness.cn/2023/11/06/Golang编码规范1/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论