Go standards and style guidelines

原文:https://docs.gitlab.com/ee/development/go_guide/

Go standards and style guidelines

本文档介绍了使用Go 语言的 GitLab 项目的各种指南和最佳实践.

Overview

GitLab 构建在Ruby on Rails之上,但我们还在有意义的项目中使用 Go. Go 是一种非常强大的语言,具有许多优点,最适合具有大量 IO(磁盘/网络访问),HTTP 请求,并行处理等的项目.由于我们在 git 上都有 Ruby on Rails 和 Go,因此我们应该仔细评估两者中哪一个最适合工作.

该页面旨在根据我们的各种经验来定义和组织我们的 Go 准则. 几个项目是从不同的标准开始的,但仍然可以有一些具体说明. 它们将在各自的README.mdPROCESS.md文件中进行描述.

Dependency Management

Go 使用基于源的策略进行依赖性管理. 依赖项从其源存储库中下载为源. 这不同于更常见的基于工件的策略,在后者中,依赖项是从与依赖项源存储库分开的程序包存储库中作为工件下载的.

Go 在 1.11 之前没有对版本管理的一流支持. 该版本引入了 Go 模块和语义版本控制的使用. Go 1.12 引入了模块代理,它们可以用作客户端和源版本控制系统之间的中介,以及校验和数据库,可以用于验证依赖项下载的完整性.

有关更多详细信息,请参见Go 中的依赖管理 .

Code Review

我们遵循Go Code Review Comments的通用原则.

审阅者和维护者应注意:

  • defer功能:在需要时以及在err检查之后确保存在.
  • 注入依赖项作为参数.
  • 封送至 JSON 时,其 Void 结构(生成null而不是[] ).

Security

安全是我们在 GitLab 的首要任务. 在代码审查期间,我们必须注意代码中可能存在的安全漏洞:

  • 使用文字/模板时的 XSS
  • 使用大猩猩的 CSRF 保护
  • 使用没有已知漏洞的 Go 版本
  • 不要泄漏秘密令牌
  • SQL 注入

记住要运行SAST依赖项扫描 在您的项目(或至少是gosec 分析器 )上,并遵守我们的安全要求 .

Web 服务器可以利用Secure等中间件的优势.

Finding a reviewer

我们的许多项目规模太小,无法拥有专职维护人员. 这就是为什么我们在 GitLab 有一个共享的 Go 评论者池. 要查找审阅者,请使用手册”工程项目”页面上” GitLab”项目的“执行”部分 .

要将您自己添加到此列表中,请将以下内容添加到team.yml文件中的个人资料中,并请您的经理进行审核和合并.

  1. projects:
  2. gitlab: reviewer go

Code style and format

  • 避免全局变量,即使在软件包中也是如此. 这样,如果多次包含该软件包,您将产生副作用.
  • 在提交之前使用goimports . goimports是一个工具,除了格式化导入行,添加缺少的行和删除未引用的行之外, 它还可以使用Gofmt自动格式化 Go 源代码.

    大多数编辑器/ IDE 允许您在保存文件之前/之后运行命令,您可以将其设置为运行goimports以便在保存时将其应用于每个文件.

  • 将私有方法放在源文件中第一个调用方方法的下面.

Automatic linting

所有 Go 项目均应包括以下 GitLab CI / CD 作业:

  1. lint:
  2. image: registry.gitlab.com/gitlab-org/gitlab-build-images:golangci-lint-alpine
  3. stage: test
  4. script:
  5. # Use default .golangci.yml file from the image if one is not present in the project root.
  6. - '[ -e .golangci.yml ] || cp /golangci/.golangci.yml .'
  7. # Write the code coverage report to gl-code-quality-report.json
  8. # and print linting issues to stdout in the format: path/to/file:line description
  9. - golangci-lint run --out-format code-climate | tee gl-code-quality-report.json | jq -r '.[] | "\(.location.path):\(.location.lines.begin) \(.description)"'
  10. artifacts:
  11. reports:
  12. codequality: gl-code-quality-report.json
  13. paths:
  14. - gl-code-quality-report.json
  15. allow_failure: true

在项目的根目录中包含.golangci.yml可以配置golangci-lint . 此示例中列出了golangci-lint所有选项.

递归包含可用后,您就可以共享作业模板,例如此分析器 .

Dependencies

依赖性应保持最小. 根据我们的批准指南 ,应在合并请求中对引入新的依赖项进行争论. 两种许可证管理依赖项扫描 应该在所有项目上激活,以确保新的依赖项安全状态和许可证兼容性.

Modules

从 Go 1.11 开始,名称Go 的模块后面提供了一个标准的依赖系统. 它提供了一种方法来定义和锁定可复制构建的依赖关系. 应尽可能使用它.

当使用 Go Modules 时,不应有vendor/目录. 相反,Go 会在需要构建项目时自动下载依赖项. 这与 Ruby 项目中 Bundler 处理依赖关系的方式一致,并使合并请求更易于查看.

在某些情况下,例如构建一个 Go 项目以充当另一个项目的 CI 运行的依赖项,删除vendor/目录意味着必须重复下载代码,这可能由于速率限制或网络而导致间歇性问题.失败. 在这种情况下,您应该在之间缓存下载的代码 .

Go <v1.11.4 中的模块校验和存在一个错误 ,因此请确保至少使用此版本,以避免checksum mismatch错误.

ORM

我们不在 GitLab 上使用对象关系映射库(ORM)(Ruby on Rails 中的ActiveRecord除外). 可以使用服务来结构化项目以避免它们. PQ应该足以与 PostgreSQL 数据库进行交互.

Migrations

在极少数情况下,如果管理托管数据库,则必须使用 ActiveRecord 提供的迁移系统. 像Journey这样的简单库,可以在postgres容器中使用,可以部署为长期运行的 pod. 新版本将部署新的 Pod,并自动迁移数据.

Testing

Testing frameworks

我们不应该使用任何特定的库或框架来进行测试,因为标准库已经提供了入门所需的一切. 如果需要更复杂的测试工具,则在我们决定使用特定的库或框架时,以下外部依赖项可能值得考虑:

Subtests

尽可能使用子测试,以提高代码的可读性和测试输出.

Better output in tests

When comparing expected and actual values in tests, use testify/require.Equal, testify/require.EqualError, testify/require.EqualValues, and others to improve readability when comparing structs, errors, large portions of text, or JSON documents:

  1. type TestData struct {
  2. // ...
  3. }
  4. func FuncUnderTest() TestData {
  5. // ...
  6. }
  7. func Test(t *testing.T) {
  8. t.Run("FuncUnderTest", func(t *testing.T) {
  9. want := TestData{}
  10. got := FuncUnderTest()
  11. require.Equal(t, want, got) // note that expected value comes first, then comes the actual one ("diff" semantics)
  12. })
  13. }

Table-Driven Tests

当您为同一功能输入/输出有多个条目时,使用表驱动测试通常是一个好习惯. 以下是编写表驱动测试时可以遵循的一些准则. 这些准则主要是从 Go 标准库源代码中提取的. 请记住,在合理的时候不要遵循这些准则.

Defining test cases

每个表条目都是一个完整的测试用例,其中包含输入和预期结果,有时还包含其他信息(例如测试名称),以使测试输出易于阅读.

Contents of the test case

  • 理想情况下,每个测试用例都应具有一个带有唯一标识符的字段,以用于命名子测试. 在 Go 标准库中,这通常是name string字段.
  • 当您在测试用例中指定将用于断言的内容时,请使用want / expect / actual .

Variable names

  • 每个表驱动的测试映射/结构片段都可以命名为tests .
  • 遍历tests ,匿名结构可以称为tttc .
  • 测试的描述可以称为name / testName / tn .

Benchmarks

处理大量 IO 或复杂操作的程序应始终包含基准测试 ,以确保随时间推移的性能一致性.

Error handling

Adding context

在返回错误之前添加上下文可能会有所帮助,而不仅仅是返回错误. 这使开发人员可以了解程序进入错误状态时试图做什么,从而使调试更加容易.

例如:

  1. // Wrap the error
  2. return nil, fmt.Errorf("get cache %s: %w", f.Name, err)
  3. // Just add context
  4. return nil, fmt.Errorf("saving cache %s: %v", f.Name, err)

A few things to keep in mind when adding context:

  • 确定是否要向调用者公开潜在的错误. 如果是这样,请使用%w ,否则请使用%v .
  • 不要使用failederrordidn't . 因为这是一个错误,所以用户已经知道某件事失败了,这可能导致出现诸如failed xx failed xx failed xx类的字符串. 解释什么 ,而不是失败.
  • 错误字符串不应大写或以标点符号或换行符结尾. 您可以使用golint进行检查.

Naming

  • 使用哨兵错误时,应始终将它们命名为ErrXxx .
  • 创建新的错误类型时,应始终将其命名为XxxError .

Checking Error types

  • 要检查错误是否相等,请不要使用== . 使用errors.Is代替(对于围棋版本> = 1.13).
  • 要检查错误是否属于某种类型,请不要使用类型断言,而应使用errors.As (对于 Go 版本> = 1.13).

References for working with errors

CLIs

每个 Go 程序都是从命令行启动的. cli是用于创建命令行应用程序的便捷软件包. 无论项目是守护程序还是简单的 cli 工具,都应使用它. 可以将标志直接映射到环境变量 ,这些变量同时记录和集中与程序的所有可能的命令行交互. 不要使用os.GetEnv ,它会将变量隐藏在代码深处.

Daemons

Logging

强烈建议为守护程序使用日志记录库. 即使标准库中有一个log包,我们通常也使用Logrus . 它的插件(”挂钩”)系统使其成为功能强大的日志记录库,并能够直接在记录器级别添加通知程序和格式化程序.

Structured (JSON) logging

理想情况下,每个二进制文件都必须具有结构化(JSON)日志记录,因为它有助于搜索和过滤日志. 在 GitLab,我们使用 JSON 格式的结构化日志记录,因为我们所有的基础架构都假定这样做. 使用Logrus 时 ,只需使用JSON 格式化程序中的构建即可打开结构化日志记录. 这遵循我们在Ruby 应用程序中使用的相同日志记录类型.

How to use Logrus

使用Logrus软件包时,应遵循一些准则:

  • 打印错误时,请使用WithError . 例如, logrus.WithError(err).Error("Failed to do something") .
  • 由于我们使用结构化日志记录 ,因此可以在该代码路径的上下文中记录字段,例如使用WithFieldWithFields的请求的 URI. 例如, logrus.WithField("file", "/app/go").Info("Opening dir") . 如果必须记录多个键,请始终使用WithFields而不是多次调用WithField .

Tracing and Correlation

LabKit是为 Go 服务保留通用库的地方. 目前,它已销售到两个项目:Workhorse 和 Gitaly,并且导出了两个主要(但相关)功能:

这为我们提供了对底层实现的精简抽象,该抽象实现在 Workhorse,Gitaly 以及将来的其他 Go 服务器之间保持一致. 例如,对于gitlab.com/gitlab-org/labkit/tracing我们可以从直接使用gitlab.com/gitlab-org/labkit/tracing切换为使用 Zipkin 或 Gokit 自己的跟踪包装器,而无需更改应用程序代码,同时仍保持相同的一致配置机制(即GITLAB_TRACING环境变量).

Context

由于守护程序是长期运行的应用程序,因此它们应具有管理取消的机制,并避免不必要的资源消耗(这可能导致 DDOS 漏洞). Go Context应该在可以阻塞并作为第一个参数传递的函数中使用.

Dockerfiles

每个项目都应在其存储库的根目录中具有一个Dockerfile ,以构建和运行该项目. 由于 Go 程序是静态二进制文件,因此它们不需要任何外部依赖关系,并且最终映像中的 shell 无用. 我们鼓励进行多阶段构建

  • 他们使用户可以使用正确的 Go 版本和依赖项来构建项目.
  • 它们生成一个小的,自包含的图像,该图像取自Scratch .

生成的 Docker 映像应在其Entrypoint处具有程序以创建可移植命令. 这样,任何人都可以运行该映像,并且没有参数就可以显示其帮助消息(如果已使用cli ).

Distributing Go binaries

除了发布自己的二进制文件的GitLab Runner之外,我们的 Go 二进制文件都是由” 分发”组管理的项目创建的.

Omnibus GitLab项目创建一个包含所有二进制文件的单一的操作系统软件包,而Cloud-Native GitLab(CNG)项目发布一组 Docker 映像和 Helm 图表以将它们粘合在一起.

两种方法对所有项目都使用相同版本的 Go,因此确保我们所有使用 Go 的项目在其测试矩阵中至少具有一个相同的 Go 版本非常重要. 您可以检查Omnibus当前正在使用的 Go 版本以及CNG正在使用的版本.

Updating Go version

我们应该始终使用受支持的 Go 版本 ,即三个最新的次要版本之一,并且应该始终使用该版本的最新补丁程序级别,因为它可能包含安全修复程序.

更改版本会影响正在编译的每个项目,因此在更改程序包构建器以使用它之前,请确保已更新所有项目以针对新的 Go 版本进行测试非常重要. 尽管Go 保证了兼容性 ,但次要版本之间的更改可能会暴露错误或在我们的项目中引起问题.

选择要使用的新 Go 版本之后,更新 Omnibus 和 CNG 的步骤如下:

为了减少两种分发方法之间不必要的差异,Omnibus 和 CNG 应该始终使用相同的 Go 版本 .

Supporting multiple Go versions

出于以下原因,各个 Golang 项目需要支持多个 Go 版本:

  1. 当新的 Go 版本发布时,我们应该开始将其集成到 CI 管道中,以验证与新编译器的兼容性.
  2. 我们必须支持Omnibus 官方的 Go 版本 ,该版本可能在最新的次要版本之后.
  3. 当 Omnibus 切换为 Go 版本时,我们仍可能需要支持旧版本进行安全反向移植.

保持对 Go 的 3 个最新次要版本的支持,可以轻松满足这 3 个要求.

可以放弃对最旧的 Go 版本的支持,并且仅支持 2 个最新版本,如果这足以支持向后 3 个 GitLab 次要版本的反向移植.

Example:

如果我们要丢弃的支持go 1.11在 GitLab 12.10 ,我们需要验证我们使用哪去版本12.912.812.7 .

我们将不考虑活动的里程碑12.10 ,因为在关键安全发布的情况下将需要12.7的反向端口.

  1. 如果从 GitLab 12.7开始Omnibus 和 CNG都在使用 Go 1.12 ,那么我们可以放心地放弃对1.11支持.
  2. 如果 Omnibus 或 CNG 在 GitLab 12.7中使用1.11 ,那么我们仍然需要保持对 Go 1.11支持,以便更轻松地向后移植安全修复程序.

Secure Team standards and style guidelines

以下是一些特定于安全团队的样式准则.

Code style and format

在提交之前,请使用goimports -local gitlab.com/gitlab-org . goimports是一个工具,除了格式化导入行,添加缺少的行和删除未引用的行之外, 它还可以使用Gofmt自动格式化 Go 源代码. 通过使用-local gitlab.com/gitlab-org选项, goimports会将本地引用的软件包与外部软件包分开分组. 有关更多详细信息,请参见 Go Wiki 上”代码审查注释”页面的导入部分 . 大多数编辑器/ IDE 允许您在保存文件之前/之后运行命令,您可以将其设置为运行goimports -local gitlab.com/gitlab-org以便在保存时将其应用于每个文件.


Return to Development documentation.