├── .github └── workflows │ ├── contributors.yml │ └── deploy.yml ├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── archetypes └── default.md ├── config.yml ├── content └── zh │ ├── _index.md │ ├── docs │ ├── 01-overview.md │ ├── 02-guide.md │ ├── 03-decisions.md │ └── 04-best-practices.md │ └── menu │ └── index.md ├── resources └── _gen │ └── assets │ └── scss │ ├── book.scss_50fc8c04e12a2f59027287995557ceff.content │ ├── book.scss_50fc8c04e12a2f59027287995557ceff.json │ ├── book.scss_e129fe35b8d0a70789c8a08429469073.content │ └── book.scss_e129fe35b8d0a70789c8a08429469073.json └── static └── README.md /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: contributes 7 | 8 | jobs: 9 | contrib-readme-en-job: 10 | runs-on: ubuntu-latest 11 | name: A job to automate contrib in readme 12 | steps: 13 | - name: Contribute List 14 | uses: akhilmhdh/contributors-readme-action@v2.3.4 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | submodules: true 16 | fetch-depth: 0 17 | 18 | - name: Setup Hugo 19 | uses: peaceiris/actions-hugo@v2 20 | with: 21 | hugo-version: 0.105.0 22 | extended: true 23 | 24 | - name: Build 25 | run: hugo --minify 26 | 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./public 32 | publish_branch: gh-pages 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .hugo_build.lock 3 | public/ 4 | .vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "themes/hugo-book"] 2 | path = themes/hugo-book 3 | url = https://github.com/alex-shpak/hugo-book 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MSG?=Generate site 2 | 3 | BASEDIR=$(CURDIR) 4 | OUTPUTDIR=$(BASEDIR)/public 5 | 6 | GITHUB_PAGES_BRANCH=gh-pages 7 | 8 | publish: clean build 9 | 10 | build: 11 | hugo 12 | touch $(OUTPUTDIR)/.nojekyll 13 | 14 | clean: 15 | [ ! -d $(OUTPUTDIR) ] || rm -rf $(OUTPUTDIR) 16 | git worktree prune 17 | rm -rf $(BASEDIR)/.git/worktrees/public/ 18 | echo "Checking out gh-pages branch into output directory" 19 | git worktree add -B gh-pages $(OUTPUTDIR) origin/$(GITHUB_PAGES_BRANCH) 20 | echo "Removing existing files" 21 | rm -rf $(OUTPUTDIR)/* 22 | 23 | github: publish 24 | cd $(OUTPUTDIR) && git add --all && git commit -m "$(MSG)" 25 | git push origin $(GITHUB_PAGES_BRANCH) 26 | 27 | .PHONY: publish clean github 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Go 编程规范 2 | 3 | [《Google Go 编程规范》](https://github.com/gocn/styleguide)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/styleguide/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/styleguide/pulls) 来共同参与贡献。 4 | 5 | 贡献者(按昵称首字母排序): 6 | 7 | > [astaxie](https://github.com/astaxie) | [Fivezh](https://github.com/fivezh) | [刘思家](https://github.com/lsj1342) | [Sijing233](https://github.com/sijing233) | [小超人](https://github.com/focozz) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang) | [784909593](https://github.com/784909593) 8 | 9 | 安装完 `hugo` 之后,需要先同步主题文件 10 | 11 | ```bash 12 | git submodule update --init --recursive 13 | ``` 14 | 15 | 同步完成后,可在根目录执行以下指令来测试网站: 16 | 17 | ```bash 18 | hugo server 19 | ``` 20 | 21 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。 22 | 23 | ## 目录 24 | 25 | 1. 概述 26 | 2. 风格指南 27 | 3. 风格决策 28 | 4. 最佳实践 29 | 30 | ## 授权 31 | 32 | The articles in 《Google Go 编程规范》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 33 | 34 | ## 贡献者 35 | 36 | 37 | 38 | 39 | 46 | 53 | 60 | 67 | 74 | 81 | 82 | 89 | 96 | 103 | 110 |
40 | 41 | fivezh 42 |
43 | Fivezh 44 |
45 |
47 | 48 | zhuyaguang 49 |
50 | 朱亚光 51 |
52 |
54 | 55 | zxmfke 56 |
57 | Xiaomin Zheng 58 |
59 |
61 | 62 | 784909593 63 |
64 | 784909593 65 |
66 |
68 | 69 | lsj1342 70 |
71 | 刘思家 72 |
73 |
75 | 76 | sijing233 77 |
78 | Sijing233 79 |
80 |
83 | 84 | xuxicheng00 85 |
86 | 小超人 87 |
88 |
90 | 91 | pseudoyu 92 |
93 | Yu ZHANG 94 |
95 |
97 | 98 | liuzengh 99 |
100 | Goodliu 101 |
102 |
104 | 105 | LinkinStars 106 |
107 | LinkinStars 108 |
109 |
111 | 112 | -------------------------------------------------------------------------------- /archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # hugo server --themesDir ... 2 | 3 | baseURL: https://gocn.github.io/styleguide/ 4 | title: Google Style Guides 5 | theme: hugo-book 6 | 7 | # language setting 8 | defaultContentLanguage: zh 9 | defaultContentLanguageInSubdir: true 10 | 11 | PygmentsCodeFences: true 12 | 13 | languages: 14 | zh: 15 | languageCode: zh 16 | languageName: 中文 17 | contentDir: content/zh 18 | weight: 1 19 | 20 | # en: 21 | # languageCode: en-US 22 | # languageName: English 23 | # contentDir: content/en 24 | # weight: 2 25 | 26 | # Book configuration 27 | disablePathToLower: true 28 | 29 | params: 30 | Description: How To Code in Go 31 | 32 | # show or hide table of contents for page 33 | BookShowToC: true 34 | 35 | # Set bundle to render side menu 36 | # if not specified file structure and weights will be used 37 | BookMenuBundle: /menu 38 | 39 | # specify section of content to render as menu 40 | # if bookMenuBundle is not set, 'docs' is value by default 41 | BookSection: docs 42 | 43 | # Enable "Edit this page" links for 'doc' page type. 44 | # Disabled by default. Uncomment to enable. 45 | BookEditURL: https://github.com/gocn/styleguide/edit/main/content/zh/ 46 | -------------------------------------------------------------------------------- /content/zh/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Google Go 编程规范 3 | type: docs 4 | --- 5 | 6 | # Google Go 编程规范 7 | 8 | [《Google Go 编程规范》](https://github.com/gocn/styleguide)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/styleguide/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/styleguide/pulls) 来共同参与贡献。 9 | 10 | 贡献者(按昵称首字母排序): 11 | 12 | > [astaxie](https://github.com/astaxie) | [Fivezh](https://github.com/fivezh) | [刘思家](https://github.com/lsj1342) | [Sijing233](https://github.com/sijing233) | [小超人](https://github.com/focozz) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang) | [784909593](https://github.com/784909593) 13 | 14 | 安装完 `hugo` 之后,需要先同步主题文件 15 | 16 | ```bash 17 | git submodule update --init --recursive 18 | ``` 19 | 20 | 同步完成后,可在根目录执行以下指令来测试网站: 21 | 22 | ```bash 23 | hugo server 24 | ``` 25 | 26 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。 27 | 28 | ## 目录 29 | 30 | 1. 概述 31 | 2. 风格指南 32 | 3. 风格决策 33 | 4. 最佳实践 34 | 35 | {{< button relref="./docs/01-overview.md" >}}开始阅读{{< /button >}} 36 | 37 | ## 授权 38 | 39 | The articles in 《Google Go 编程规范》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 40 | -------------------------------------------------------------------------------- /content/zh/docs/01-overview.md: -------------------------------------------------------------------------------- 1 | # 概览 2 | 3 | 原文:[https://google.github.io/styleguide/go](https://google.github.io/styleguide/go) 4 | 5 | [概述](https://gocn.github.io/styleguide/docs/01-overview/) | [风格指南](https://gocn.github.io/styleguide/docs/02-guide/) | [风格决策](https://gocn.github.io/styleguide/docs/03-decisions/) | [最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/) 6 | 7 | ## 关于 8 | 9 | 本系列的 Go 风格指南和相关文档整理了当前,最佳的一个编写易读和惯用方式的 Go 写法。 遵守风格指南并不是绝对的,这份文件也永远不会详尽无遗。我们的目的是尽量减少编写可读 Go 代码的猜测,以便该语言的新手可以避免常见的错误。此风格指南也用于统一 Google 内 Go 代码 review 者的风格指南。 10 | 11 | | 文档 | 链接 | 主要受众 | [视为标准(Normative)](https://google.github.io/styleguide/go/index#normative) | [视为规范(Canonical)](https://google.github.io/styleguide/go/index#canonical) | 12 | | ------------------- | ----------------------------------------------------- | ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 13 | | **风格指南** | https://gocn.github.io/styleguide/docs/02-guide/ | 所有人 | Yes | Yes | 14 | | **风格决策** | https://gocn.github.io/styleguide/docs/03-decisions/ | 可读性导师 | Yes | No | 15 | | **最佳实践** | https://gocn.github.io/styleguide/docs/04-best-practices/ | 任何有兴趣的人 | No | No | 16 | 17 | 18 | 19 | ### 文档说明 20 | 21 | 1. **风格指南(Style Guide)** (https://gocn.github.io/styleguide/docs/02-guide/) 概述了 Google Go 风格的基础。本文档为定义名词性质的文件,用作风格决策和最佳实践中建议的基础。 22 | 23 | 2. **风格决策(Style Decisions)** (https://gocn.github.io/styleguide/docs/03-decisions/) 是一份更详细的文档,它总结了特定场景下风格的决策理由,并在适当的时候讨论了决策背后的原因。 24 | 25 | 这些决定可能偶尔会根据新数据、新语言特性、新代码库或新出现的模式而改变,但不期望 Google 的 Go 程序员能及时了解本文档的更新。 26 | 27 | 3. **最佳实践(Best Practices)** (https://gocn.github.io/styleguide/docs/04-best-practices/) 描述了一些随时间演变的模式,这些模式可以解决通用问题,可读性强,并且对代码可维护的需要有很好的健壮性。 28 | 29 | 这些最佳实践并不规范,但鼓励 Google 的 Go 程序员尽可能使用它们,以保持代码库的统一和一致。 30 | 31 | 这些文件旨在: 32 | 33 | - 就权衡备选风格的一套原则达成一致 34 | - 整理最终的 Go 编码风格 35 | - 记录并提供 Go 编码惯用法的典型示例 36 | - 记录各种风格决策的利弊 37 | - 帮助减少在 Go 可读性 review 时的意外 38 | - 帮助可读性导师使用一致的术语和指导 39 | 40 | 本文档**无**意于: 41 | 42 | - 成为在可读性审查时的详尽的意见清单 43 | - 列出所有的规则,期望每个人在任何时候都能记住并遵守 44 | - 在语言特型和风格的使用上取代良好的判断力 45 | - 为了消除风格差异,证明大规模的改变是合理的 46 | 47 | 不同 Go 程序员之间以及不同团队的代码库之间总会存在差异。然而,我们的代码库尽可能保持一致符合 Google 和 Alphabet 的最大利益。 (有关一致性的更多信息,请参阅 [指南](https://google.github.io/styleguide/go/guide#consistency)。)为此,您可以随意进行风格改进,但不需要挑剔你发现的每一个违反风格指南的行为。特别是,这些文档可能会随着时间的推移而改变,这不是导致现有代码库做出额外改动的理由; 使用最新的最佳实践编写新代码,并随着时间的推移解决上下文或者关联到的代码的问题就足够了。 48 | 49 | 重要的是要认识到风格问题本质上是个人的,并且总是带有特定的权衡。这些文档中的大部分指南都是主观的,但就像 `gofmt` 一样,它提供的一致性具有重要价值。 因此,风格建议不会在未经适当讨论的情况下改变,Google 的 Go 程序员被鼓励遵循风格指南,即使他们可能不同意。 50 | 51 | ## 定义 52 | 53 | 风格文档中使用的以下词语定义如下: 54 | 55 | - #### 规范(Canonical) 56 | 57 | 制定规定性和持久性规则 58 | 59 | 在这些文档中,“规范”用于描述被认为是所有代码(旧的和新的)都应该遵循的标准,并且预计不会随着时间的推移而发生重大变化。规范文档中的原则应该被作者和审稿人理解,因此规范文档中包含的所有内容都必须达到高标准。 如此一来,与非规范文档相比,规范文档通常更短并且规定的风格元素更少。 60 | 61 | - #### 标准(Normative) 62 | 63 | 旨在建立一致性 64 | 65 | 在这些文档中,“规范”用于描述 Go 代码审查者使用的一致同意的风格元素,以便建议、术语和理由保持一致。 这些元素可能会随着时间的推移而发生变化,本文涉及的这些文件将反映出这些变化,以便审阅者可以保持一致和及时更新。 Go 代码编写者不被要求熟悉此文档,但这些文档将经常被审阅者用作可读性审查的参考。 66 | 67 | - #### 惯用写法(Idiomatic) 68 | 69 | 常见且熟悉 70 | 71 | 在这些文档中,“惯用写法”指在 Go 代码中普遍存在的东西,并已成为一种易于识别的常见写法。一般来说,如果两者在上下文中服务于相同的目的,那么惯用写法应该优先于非惯用写法,因为这将是读者最熟悉的写法。 72 | 73 | ## 附加参考 74 | 75 | 本指南假定读者熟悉 [Effective Go](https://go.dev/doc/effective_go),因为它为整个 Go 社区的 Go 代码提供了一个通用基线。 76 | 77 | 下面是一些额外的资源,供那些希望自学 Go 风格的人和希望在他们的评审中附上可链接的评审依据的代码审阅者。 不期望Go 可读性过程的参与者熟悉这些资源,但它们可能作为可读性审阅的相关依据出现。 78 | 79 | **外部参考** 80 | 81 | - [Go 语言规范](https://go.dev/ref/spec) 82 | - [Go FAQ](https://go.dev/doc/faq) 83 | - [Go 内存模型](https://go.dev/ref/mem) 84 | - [Go 数据结构](https://research.swtch.com/godata) 85 | - [Go 接口](https://research.swtch.com/interfaces) 86 | - [Go 谚语](https://go-proverbs.github.io/) 87 | - Go 技巧 - 敬请期待 88 | 89 | **马桶测试(Testing-on-the-Toilet) 相关文档** 90 | 91 | > 厕所里的测试(又称 TotT)是测试小组最明显的成就。从2006年开始,TotT 从一个会议上的玩笑开始,已经成为一个真正的谷歌机构,并且是在公司内部传播想法、产生讨论和推动新的内部工具采用的最有效的方式之一。[TotT](https://mike-bland.com/2011/10/25/testing-on-the-toilet.html#:~:text=Testing%20on%20the%20Toilet%20%28aka%20TotT%29%20is%20the,drive%20new%20internal%20tool%20adoption%20within%20the%20company) 92 | 93 | - [TotT: 标识命令](https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html) 94 | - [TotT: 测试状态与 vs Testing Interactions](https://testing.googleblog.com/2013/03/testing-on-toilet-testing-state-vs.html) 95 | - [TotT: 有效测试](https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html) 96 | - [TotT: 风险驱动测试](https://testing.googleblog.com/2014/05/testing-on-toilet-risk-driven-testing.html) 97 | - [TotT: 变化检测器测试被认为是有害的](https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html) 98 | 99 | **额外的外部文档** 100 | 101 | - [Go 与 教条](https://research.swtch.com/dogma) 102 | - [少即是成倍的多](https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html) 103 | - [Esmerelda 的想象力](https://commandcenter.blogspot.com/2011/12/esmereldas-imagination.html) 104 | - [用于解析的正则表达式](https://commandcenter.blogspot.com/2011/08/regular-expressions-in-lexing-and.html) 105 | - [Gofmt 的风格没有人喜欢,但 Gofmt 却是每个人的最爱](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=8m43s) (YouTube) 106 | 107 | {{< button relref="/" >}}首页{{< /button >}} 108 | {{< button relref="./02-guide.md" >}}下一章{{< /button >}} 109 | -------------------------------------------------------------------------------- /content/zh/docs/02-guide.md: -------------------------------------------------------------------------------- 1 | # Go 风格指南 2 | 3 | 原文:[https://google.github.io/styleguide/go](https://google.github.io/styleguide/go) 4 | 5 | [概述](https://gocn.github.io/styleguide/docs/01-overview/) | [风格指南](https://gocn.github.io/styleguide/docs/02-guide/) | [风格决策](https://gocn.github.io/styleguide/docs/03-decisions/) | [最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/) 6 | 7 | ## 风格原则 8 | 9 | 以下几条总体原则总结了如何编写可读的 Go 代码。以下为具有可读性的代码特征,按重要性排序: 10 | 11 | 1. [清晰](https://gocn.github.io/styleguide/docs/02-guide/#清晰):代码的目的和设计原理对读者来说是清楚的。 12 | 2. [简约](https://gocn.github.io/styleguide/docs/02-guide/#简约):代码以最简单的方式来完成它的目的。 13 | 3. [简洁](https://gocn.github.io/styleguide/docs/02-guide/#简洁):代码具有很高的信噪比,即写出来的代码是有意义的,非可有可无的。 14 | 4. [可维护性](https://gocn.github.io/styleguide/docs/02-guide/#可维护性):代码可以很容易地被维护。 15 | 5. [一致](https://gocn.github.io/styleguide/docs/02-guide/#一致):代码与更广泛的 Google 代码库一致。 16 | 17 | ### 清晰 18 | 19 | 可读性的核心目标是写出对读者来说很清晰的代码。 20 | 21 | 清晰性主要是通过有效的命名、有用的注释和有效的代码组织来实现的。 22 | 23 | 清晰性要从读者的角度来看,而不是从代码的作者的角度来看,代码的易读性比易写性更重要。代码的清晰性有两个不同的方面: 24 | 25 | - [该代码实际上在做什么?](https://gocn.github.io/styleguide/docs/02-guide/#该代码实际上在做什么) 26 | - [为什么代码会这么做?](https://gocn.github.io/styleguide/docs/02-guide/#为什么代码会这么做) 27 | 28 | #### 该代码实际上在做什么 29 | 30 | Go 被设计得应该是可以比较直接地看到代码在做什么。在不确定的情况下或者读者可能需要先验知识才能理解代码的情况下,我们值得投入时间以使代码的意图对未来的读者更加明确。例如,它可能有助于: 31 | 32 | - 使用更具描述性的变量名称 33 | - 添加额外的评论 34 | - 使用空白与注释来划分代码 35 | - 将代码重构为独立的函数/方法,使其更加模块化 36 | 37 | 这里没有一个放之四海而皆准的方法,但在开发 Go 代码时,优先考虑清晰性是很重要的。 38 | 39 | #### 为什么代码会这么做 40 | 41 | 代码的逻辑通常由变量、函数、方法或包的名称充分传达。如果不是这样,添加注释是很重要的。当代码中包含读者可能不熟悉的细节时,“为什么?”就显得尤为重要,例如: 42 | 43 | - 编程语言中的细微差别,例如,一个闭包将捕获一个循环变量,但闭包在许多行之外 44 | - 业务逻辑的细微差别,例如,需要区分实际用户和虚假用户的访问控制检查 45 | 46 | 一个 API 可能需要小心翼翼才能正确使用。例如,由于性能原因,一段代码可能错综复杂,难以理解,或者一连串复杂的数学运算可能以一种意想不到的方式使用类型转换。在这些以及更多的情况下,附带的注释和文档对这些方面进行解释是很重要的,这样未来的维护者就不会犯错,读者也可以理解代码而不需要进行逆向工程。 47 | 48 | 同样重要的是,我们要意识到,一些基于清晰性考虑的尝试(如添加额外的注释),实际上会通过增加杂乱无章的内容、重述代码已经说过的内容、与代码相矛盾或增加维护负担来保持注释的最新性,以此来掩盖代码的目的。让代码自己说话(例如,通过代码中的名称本身进行描述),而不是添加多余的注释。通常情况下,注释最好是解释为什么要做某事,而不是解释代码在做什么。 49 | 50 | Google 的代码库基本上是统一和一致的。通常情况下,那些比较突兀的代码(例如,应用一个不熟悉的模式)是基于充分的理由,通常是为了性能。保持这种特性很重要,可以让读者在阅读一段新的代码时清楚地知道他们应该把注意力放在哪里。 51 | 52 | 标准库中包含了许多这一原则发挥作用的例子。例如: 53 | 54 | - 在 `package sort` 中的维护者注释 55 | - 好的[在同一包中可运行的例子](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/sort/example_search_test.go),这对用户(他们会[查看 godoc](https://pkg.go.dev/sort#pkg-examples))和维护者(他们[作为测试的一部分运行](https://gocn.github.io/styleguide/docs/03-decisions/#示例examples))都有利 56 | - `strings.Cut` 只有四行代码,但它们提高了 [callsites 的清晰性和正确性](https://github.com/golang/go/issues/46336) 57 | 58 | ### 简约 59 | 60 | 你的 Go 代码对于使用、阅读和维护它的人来说应该是简单的。 61 | 62 | Go 代码应该以最简单的方式编写,以实现其行为和性能方面的目标。在 Google Go 代码库中,简单的代码: 63 | 64 | - 从头至尾都易于阅读 65 | - 不预设你已经知道它在做什么 66 | - 不预设你能记住前面所有的代码 67 | - 不含非必要的抽象层次 68 | - 不含过于通用的命名 69 | - 让读者清楚地了解到传值与决定的传播情况 70 | - 有注释,解释为什么,而不是代码正在做什么,以避免未来的歧义 71 | - 有独立的文档 72 | - 包含有用的错误与失败用例测试 73 | - 往往不是看起来“聪明”的代码 74 | 75 | 在代码的简单性和 API 使用的简单性之间可能会需要权衡。例如,让代码更复杂可能是值得的,这样 API 的终端用户可以更容易地正确调用 API。相反,把一些额外的工作留给 API 的终端用户也是值得的,这样代码就会保持简单和容易理解。 76 | 77 | 当代码需要复杂性时,应该有意地增加复杂性。如果需要额外的性能,或者一个特定的库或服务有多个不同的客户,这通常是必要的。复杂性可能是合理的,但它应该有相应的文档,以便客户和未来的维护者能够理解和驾驭这种复杂性。这应该用测试和例子来补充,以证明其正确的用法,特别是有“简单”和“复杂”两种方式来使用代码的情况下。 78 | 79 | 这一原则并不意味着复杂的代码不能或不应该用 Go 编写,也不意味着 Go 代码不允许复杂。我们努力使代码库避免不必要的复杂性,因此当复杂性出现时,它表明有关的代码需要仔细理解和维护。理想情况下,应该有相应的注释来解释其中的道理,并指出应该注意的地方。在优化代码以提高性能时,经常会出现这种情况;这样做往往需要更复杂的方法,比如预先分配一个缓冲区并在整个 goroutine 生命周期内重复使用它。当维护者看到这种情况时,应该是一个线索,说明相关的代码是基于性能的关键考虑,这应该影响到未来修改时的谨慎。另一方面,如果不必要地使用,这种复杂性会给那些需要在未来阅读或修改代码的人带来负担。 80 | 81 | 如果代码在其目的应该很简单时却变得非常复杂,这往往是一个我们可以重新审视代码实现的信号,看看是否有更简单的方法来完成同样的事情。 82 | 83 | #### 最小化机制 84 | 85 | 如果有几种方法来表达同一个想法,最好选择使用最标准工具的方法。复杂的机制经常存在,但不应该无缘无故地使用。根据需要增加代码的复杂性是很容易的,而在发现没有必要的情况下删除现有的复杂性则要难得多。 86 | 87 | 1. 当能够满足你的使用情况时,使用核心的语言结构(例如 channel、slice、map、loop 或 struct) 88 | 2. 如果没有,就在标准库中寻找一个工具(如 HTTP 客户端或模板引擎) 89 | 3. 最后,在引入新的依赖或创建自己的依赖之前,考虑 Google 代码库中是否有一个能够满足的核心库 90 | 91 | 例如,考虑生产代码包含一个绑定在变量上的标志,它的默认值必须在测试中被覆盖。除非打算测试程序的命令行界面本身(例如,用`os/exec`),否则直接覆盖绑定的值比使用 `flag.Set` 更简单,因此更可取。 92 | 93 | 同样,如果一段代码需要检查集合成员的资格,一个布尔值的映射(例如,`map[string]bool`)通常就足够了。只有在需要更复杂的操作,不能使用 map 或过于复杂时,才应使用提供类似集合类型和功能的库。 94 | 95 | ### 简洁 96 | 97 | 简洁的 Go 代码具有很高的信噪比。它很容易分辨出相关的细节,而命名和结构则引导读者了解这些细节。 98 | 99 | 而有很多东西会常常会阻碍这些最突出的细节: 100 | 101 | - 重复代码 102 | - 无关的语法 103 | - [含义不明的名称](https://gocn.github.io/styleguide/docs/02-guide/#命名) 104 | - 不必要的抽象 105 | - 空白 106 | 107 | 重复代码尤其容易掩盖每个相似代码之间的差异,需要读者直观地比较相似的代码行来发现变化。[表驱动测试](https://github.com/golang/go/wiki/TableDrivenTests)是一个很好的例子,这种机制可以简明地从每个重复的重要细节中找出共同的代码,但是选择哪些部分囊括在表中,会对表格的易懂程度产生影响。 108 | 109 | 在考虑多种结构代码的方式时,值得考虑哪种方式能使重要的细节最显著。 110 | 111 | 理解和使用常见的代码结构和规范对于保持高信噪比也很重要。例如,下面的代码块在[错误处理](https://go.dev/blog/errors-are-values)中非常常见,读者可以很快理解这个代码块的目的。 112 | 113 | ```go 114 | // Good: 115 | if err := doSomething(); err != nil { 116 | // ... 117 | } 118 | ``` 119 | 120 | 如果代码看起来非常相似但却有细微的不同,读者可能不会注意到这种变化。在这样的情况下,值得故意[“提高”](https://gocn.github.io/styleguide/docs/04-best-practices/#信号增强)错误检查的信号,增加一个注释以引起关注。 121 | 122 | ```go 123 | // Good: 124 | if err := doSomething(); err == nil { // if NO error 125 | // ... 126 | } 127 | ``` 128 | 129 | ### 可维护性 130 | 131 | 代码被编辑的次数比它写它的次数多得多。可读的代码不仅对试图了解其工作原理的读者有意义,而且对需要改写它的程序员也有意义,清晰性很关键。 132 | 133 | 可维护的代码: 134 | 135 | - 容易让未来的程序员正确进行修改 136 | - 拥有结构化的 API,使其能够优雅地增加 137 | - 清楚代码预设条件,并选择映射到问题结构而不是代码结构的抽象 138 | - 避免不必要的耦合,不包括不使用的功能 139 | - 有一个全面的测试套件,以确保预期行为可控、重要逻辑正确,并且测试在失败的情况下提供清晰、可操作的诊断 140 | 141 | 当使用像接口和类型这样的抽象时,根据定义,它们会从使用的上下文中移除信息,因此必须确保它们提供足够的好处。当使用具体类型时,编辑器和 IDE 可以直接连接到方法定义并显示相应的文档,但在其他情况下只能参考接口定义。接口是一个强大的工具,但也是有代价的,因为维护者可能需要了解底层实现的具体细节才能正确使用接口,这必须在接口文档中或在调用现场进行解释。 142 | 143 | 可维护的代码还可以避免在容易忽视的地方隐藏重要的细节。例如,在下面的每一行代码中,是否有 `:` 字符对于理解这一行至关重要。 144 | 145 | ```go 146 | // Bad: 147 | // 使用 = 而不是 := 可以完全改变这一行的含义 148 | if user, err = db.UserByID(userID); err != nil { 149 | // ... 150 | } 151 | ``` 152 | 153 | ```go 154 | // Bad: 155 | // 这行中间的 ! 很容易错过 156 | leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0)) 157 | ``` 158 | 159 | 这两种写法不能说错误,但都可以写得更明确,或者可以有一个附带的评论,提醒注意重要的行为。 160 | 161 | ```go 162 | // Good: 163 | u, err := db.UserByID(userID) 164 | if err != nil { 165 | return fmt.Errorf("invalid origin user: %s", err) 166 | } 167 | user = u 168 | ``` 169 | 170 | ```go 171 | // Good: 172 | // 公历闰年不仅仅是 year%4 == 0 173 | // 查看 https://en.wikipedia.org/wiki/Leap_year#Algorithm. 174 | var ( 175 | leap4 = year%4 == 0 176 | leap100 = year%100 == 0 177 | leap400 = year%400 == 0 178 | ) 179 | leap := leap4 && (!leap100 || leap400) 180 | ``` 181 | 182 | 同样地,一个隐藏了关键逻辑或重要边界情况的辅助函数,可能会使未来的变化很容易地被误解。 183 | 184 | 易联想的名字是可维护代码的另一个特点。一个包的用户或一段代码的维护者应该能够联想到一个变量、方法或函数在特定情况下的名称。相同概念的函数参数和接收器名称通常应该共享相同的名称,这既是为了保持文档的可理解性,也是为了方便以最小的开销重构代码。 185 | 186 | 可维护的代码尽量减少其依赖性(包括隐性和显性)。对更少包的依赖意味着更少的代码行可以影响其行为。避免对内部或未记录的行为的依赖,使得代码在未来这些行为发生变化时,不太容易造成维护负担。 187 | 188 | 在考虑如何构造或编写代码时,值得花时间去思考代码可能随着时间的推移而演变的方式。如果一个给定的方法更有利于未来更容易和更安全的变化,这往往是一个很好的权衡,即使它意味着一个稍微复杂的设计。 189 | 190 | ### 一致 191 | 192 | 一致性的代码是指在更广泛的代码库中,在一个团队或包的范围内,甚至在一个文件中,看起来、感觉和行为都是类似的代码。 193 | 194 | 一致性的问题并不凌驾于上述的任何原则之上,但如果必须有所取舍,那往往有利于一致性的实现。 195 | 196 | 一个包内的一致性通常是最直接重要的一致性水平。如果同一个问题在一个包里有多种处理方式,或者同一个概念在一个文件里有很多名字,那就会非常不优雅。然而,即使这样,也不应该凌驾于文件的风格原则或全局一致性之上。 197 | 198 | ## 核心准则 199 | 200 | 这些准则收集了所有 Go 代码都应遵循的 Go 风格的最重要方面。我们希望这些原则在你被授予可读性认证的时候就已经学会并遵循了。这些准则不会经常改变,新增加内容也有较高准入门槛。 201 | 202 | 下面的准则是对 [Effective Go](https://go.dev/doc/effective_go) 中建议的扩展,它为整个社区的 Go 代码提供了一个共同的基准线。 203 | 204 | ### 格式化 205 | 206 | 所有 Go 源文件必须符合 `gofmt` 工具所输出的格式。这个格式是由 Google 代码库中的预提交检查强制执行的。[生成的代码](https://docs.bazel.build/versions/main/be/general.html#genrule)通常也应该被格式化(例如,通过使用`format.Source`),因为它也可以在代码搜索中浏览。 207 | 208 | ### 大小写混合 209 | 210 | Go 源代码在编写包含多个词的名称时使用`MixedCaps`或`mixedCaps`(驼峰大写)而不是下划线(蛇形大写)。 211 | 212 | 即使该准则打破了其他语言的命名惯例,这也适用。例如,一个常量如果被导出,则为`MaxLength`(而不是`MAX_LENGTH`),如果未被导出,则为`maxLength`(而不是`max_length`)。 213 | 214 | 基于首字母大小写的考量,局部变量被认为是 [不可导出的](https://go.dev/ref/spec#Exported_identifiers)。 215 | 216 | ### 行长度 217 | 218 | Go 源代码没有固定的行长度。如果觉得某一行太长,就应该对其进行重构而不是换行。如果该行已经尽可能短了,那么应该允许其保持很长。 219 | 220 | 不要在以下情况进行分行: 221 | 222 | - 在[缩进变化](https://gocn.github.io/styleguide/docs/03-decisions/#缩进的混乱)之前(例如,函数声明、条件) 223 | - 要使一个长的字符串(例如,一个 URL)适合于多个较短的行 224 | 225 | ### 命名 226 | 227 | 命名与其说是科学,不如说是艺术。在 Go 中,名字往往比许多其他语言的名字短一些,但同样的[一般准则](https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html)也适用,名称应: 228 | 229 | - 使用时不感到[重复](https://gocn.github.io/styleguide/docs/03-decisions/#重复repetition) 230 | - 将上下文考虑在内 231 | - 不重复已经明确的概念 232 | 233 | 你可以在[决定](https://gocn.github.io/styleguide/docs/02-guide/#命名)中找到关于命名的更具体的指导。 234 | 235 | ### 局部一致性 236 | 237 | 如果风格指南对某一特定的风格点没有说明,作者可以自由选择他们喜欢的风格,除非相近的代码(通常在同一个文件或包内,但有时在一个团队或项目目录内)对这个问题采取了一致的立场。 238 | 239 | **有效的**局部风格考虑的例子: 240 | 241 | - 使用 `%s` 或 `%v` 对错误进行格式化打印 242 | - 使用 buffered channels 来代替 mutexes 243 | 244 | **无效的**局部风格考虑的例子: 245 | 246 | - 代码行长度的限制 247 | - 使用基于断言的测试库 248 | 249 | 如果局部风格与风格指南不一致,但对可读性的影响仅限于一个文件,它通常会在代码审查中浮出水面,而一致的修复将超出有关 CL 的范围。在这一点上,提交一个 bug 来跟踪修复是合适的。 250 | 251 | 如果一个变更会使现有的风格偏差变大,在更多的 API 表面暴露出来,扩大存在偏差的文件数量,或者引入一个实际的错误,那么局部一致性就不再是违反新代码风格指南的有效理由。在这些情况下,作者应该在同一 CL 中清理现有的代码库,在当前 CL 之前进行重构,或者找到一个至少不会使本地化问题恶化的替代方案。 252 | 253 | {{< button relref="./01-overview.md" >}}上一章{{< /button >}} 254 | {{< button relref="./03-decisions.md" >}}下一章{{< /button >}} 255 | -------------------------------------------------------------------------------- /content/zh/docs/03-decisions.md: -------------------------------------------------------------------------------- 1 | # Go 风格决策 2 | 3 | 原文:[https://google.github.io/styleguide/go](https://google.github.io/styleguide/go) 4 | 5 | [概述](https://gocn.github.io/styleguide/docs/01-overview/) | [风格指南](https://gocn.github.io/styleguide/docs/02-guide/) | [风格决策](https://gocn.github.io/styleguide/docs/03-decisions/) | [最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/) 6 | 7 | **注意:** 本文是 Google [Go 风格](https://gocn.github.io/styleguide/docs/01-overview/) 系列文档的一部分。本文档是 **[规范性(normative)](https://gocn.github.io/styleguide/docs/01-overview/#标准normative) 但不是[强制规范(canonical)](https://gocn.github.io/styleguide/docs/01-overview/#规范canonical)**,并且从属于[Google 风格指南](https://gocn.github.io/styleguide/docs/02-guide/)。请参阅[概述](https://gocn.github.io/styleguide/docs/01-overview/#关于)获取更多详细信息。 8 | 9 | ## 关于 10 | 11 | 本文包含旨在统一和为 Go 可读性导师给出的建议提供标准指导、解释和示例的风格决策。 12 | 13 | 本文档**并不详尽**,且会随着时间的推移而增加。如果[风格指南](https://gocn.github.io/styleguide/docs/02-guide/) 与此处给出的建议相矛盾,**风格指南优先**,并且本文档应相应更新。 14 | 15 | 参见 [关于](https://gocn.github.io/styleguide/docs/01-overview/#关于) 获取 Go 风格的全套文档。 16 | 17 | 以下部分已从样式决策移至指南的一部分: 18 | 19 | - **混合大写字母MixedCaps**: 参见 https://gocn.github.io/styleguide/docs/02-guide/#大小写混合 20 | - **格式化Formatting**: 参见 https://gocn.github.io/styleguide/docs/02-guide/#格式化 21 | - **行长度Line Length**: 参见 https://gocn.github.io/styleguide/docs/02-guide/#行长度 22 | 23 | ## 命名Naming 24 | 25 | 有关命名的总体指导,请参阅[核心风格指南](https://gocn.github.io/styleguide/docs/02-guide/#命名) 中的命名部分,以下部分对命名中的特定区域提供进一步的说明。 26 | 27 | ### 下划线Underscores 28 | 29 | Go 中的命名通常不应包含下划线。这个原则有三个例外: 30 | 31 | 1. 仅由生成代码导入的包名称可能包含下划线。有关如何选择多词包名称的更多详细信息,请参阅[包名称](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names)。 32 | 2. `*_test.go` 文件中的测试、基准和示例函数名称可能包含下划线。 33 | 3. 与操作系统或 cgo 互操作的低级库可能会重用标识符,如 [`syscall`](https://pkg.go.dev/syscall#pkg-constants) 中所做的那样。在大多数代码库中,这预计是非常罕见的。 34 | ### 包名称Package names 35 | 36 | Go 包名称应该简短并且只包含小写字母。由多个单词组成的包名称应全部小写。例如,包 [`tabwriter`](https://pkg.go.dev/text/tabwriter) 不应该命名为 `tabWriter`、`TabWriter` 或 `tab_writer`。 37 | 38 | 避免选择可能被常用局部变量[遮蔽覆盖](https://gocn.github.io/styleguide/docs/04-best-practices/#阴影) 的包名称。例如,`usercount` 是比 `count` 更好的包名,因为 `count` 是常用变量名。 39 | 40 | Go 包名称不应该有下划线。如果你需要导入名称中确实有一个包(通常来自生成的或第三方代码),则必须在导入时将其重命名为适合在 Go 代码中使用的名称。 41 | 42 | 一个例外是仅由生成的代码导入的包名称可能包含下划线。具体例子包括: 43 | 44 | - 对外部测试包使用 _test 后缀,例如集成测试 45 | - 使用 `_test` 后缀作为 [包级文档示例](https://go.dev/blog/examples) 46 | 47 | 避免使用无意义的包名称,例如 `util`、`utility`、`common`、`helper` 等。查看更多关于[所谓的“实用程序包”](https://gocn.github.io/styleguide/docs/04-best-practices/#工具包)。 48 | 49 | 当导入的包被重命名时(例如 `import foob "path/to/foo_go_proto"`),包的本地名称必须符合上述规则,因为本地名称决定了包中的符号在文件中的引用方式.如果给定的导入在多个文件中重命名,特别是在相同或附近的包中,则应尽可能使用相同的本地名称以保持一致性。 50 | 51 | 另请参阅:https://go.dev/blog/package-names 52 | 53 | ### 接收者命名Receiver names 54 | 55 | [接收者](https://golang.org/ref/spec#Method_declarations) 变量名必须满足: 56 | 57 | - 短(通常是一两个字母的长度) 58 | - 类型本身的缩写 59 | - 始终如一地应用于该类型的每个接收者 60 | 61 | | 长名称 | 更好命名 | 62 | | --------------------------- | ------------------------- | 63 | | `func (tray Tray)` | `func (t Tray)` | 64 | | `func (info *ResearchInfo)` | `func (ri *ResearchInfo)` | 65 | | `func (this *ReportWriter)` | `func (w *ReportWriter)` | 66 | | `func (self *Scanner)` | `func (s *Scanner)` | 67 | 68 | ### 常量命名Constant names 69 | 70 | 常量名称必须像 Go 中的所有其他名称一样使用 [混合大写字母MixedCaps](https://gocn.github.io/styleguide/docs/02-guide#大小写混合)。([导出](https://tour.golang.org/basics/3) 常量以大写字母开头,而未导出的常量以小写字母开头。)即使打破了其他语言的约定,这也是适用的。常量名称不应是其值的派生词,而应该解释值所表示的含义。 71 | 72 | ``` 73 | // Good: 74 | const MaxPacketSize = 512 75 | 76 | const ( 77 | ExecuteBit = 1 << iota 78 | WriteBit 79 | ReadBit 80 | ) 81 | ``` 82 | 83 | 不要使用非混合大写常量名称或带有 `K` 前缀的常量。 84 | 85 | ``` 86 | // Bad: 87 | const MAX_PACKET_SIZE = 512 88 | const kMaxBufferSize = 1024 89 | const KMaxUsersPergroup = 500 90 | ``` 91 | 92 | 根据它们的角色而不是它们的值来命名常量。如果一个常量除了它的值之外没有其他作用,那么就没有必要将它定义为一个常量。 93 | 94 | ``` 95 | // Bad: 96 | const Twelve = 12 97 | 98 | const ( 99 | UserNameColumn = "username" 100 | GroupColumn = "group" 101 | ) 102 | ``` 103 | 104 | ### 缩写词Initialisms 105 | 106 | 名称中的首字母缩略词或单独的首字母缩略词(例如,“URL”和“NATO”)应该具有相同的大小写。`URL` 应显示为 `URL` 或 `url`(如 `urlPony` 或 `URLPony`),绝不能显示为 `Url`。这也适用于 `ID` 是“identifier”的缩写; 写 `appID` 而不是 `appId`。 107 | 108 | - 在具有多个首字母缩写的名称中(例如 `XMLAPI` 因为它包含 `XML` 和 `API`),给定首字母缩写中的每个字母都应该具有相同的大小写,但名称中的每个首字母缩写不需要具有相同的大小写。 109 | - 在带有包含小写字母的首字母缩写的名称中(例如`DDoS`、`iOS`、`gRPC`),首字母缩写应该像在标准中一样出现,除非你需要为了满足 [导出](https://golang.org/ref/spec#Exported_identifiers) 而更改第一个字母。在这些情况下,整个缩写词应该是相同的情况(例如 `ddos`、`IOS`、`GRPC`)。 110 | 111 | | 缩写词 | 范围 | 正确 | 错误 | 112 | |---------|------------|----------|----------------------------------------| 113 | | XML API | Exported | `XMLAPI` | `XmlApi`, `XMLApi`, `XmlAPI`, `XMLapi` | 114 | | XML API | Unexported | `xmlAPI` | `xmlapi`, `xmlApi` | 115 | | iOS | Exported | `IOS` | `Ios`, `IoS` | 116 | | iOS | Unexported | `iOS` | `ios` | 117 | | gRPC | Exported | `GRPC` | `Grpc` | 118 | | gRPC | Unexported | `gRPC` | `grpc` | 119 | | DDoS | Exported | `DDoS` | `DDOS`, `Ddos` | 120 | | DDoS | Unexported | `ddos` | `dDoS`, `dDOS` | 121 | 122 | ### Get方法Getters 123 | 124 | 函数和方法名称不应使用 `Get` 或 `get` 前缀,除非底层概念使用单词“get”(例如 HTTP GET)。此时,更应该直接以名词开头的名称,例如使用 `Counts` 而不是 `GetCounts`。 125 | 126 | 如果该函数涉及执行复杂的计算或执行远程调用,则可以使用`Compute` 或 `Fetch`等不同的词代替`Get`,以使读者清楚函数调用可能需要时间,并有可能会阻塞或失败。 127 | 128 | ### 变量名Variable names 129 | 130 | 一般的经验法则是,名称的长度应与其范围的大小成正比,并与其在该范围内使用的次数成反比。在文件范围内创建的变量可能需要多个单词,而单个内部块作用域内的变量可能是单个单词甚至只是一两个字符,以保持代码清晰并避免无关信息。 131 | 132 | 这是一条粗略的基础原则。这些数字准则不是严格的规则。要根据上下文、[清晰](https://gocn.github.io/styleguide/docs/02-guide/#清晰) 和[简洁](https://gocn.github.io/styleguide/docs/02-guide/#简洁)来进行判断。 133 | 134 | - 小范围是执行一两个小操作的范围,比如 1-7 行。 135 | - 中等范围是一些小的或一个大的操作,比如 8-15 行。 136 | - 大范围是一个或几个大操作,比如 15-25 行。 137 | - 非常大的范围是指超过一页(例如,超过 25 行)的任何内容。 138 | 139 | 在小范围内可能非常清楚的名称(例如,`c` 表示计数器)在较大范围内可能不够用,并且需要澄清以提示进一步了解其在代码中的用途。一个作用域中有很多变量,或者表示相似值或概念的变量,那就可能需要比作用域建议的采用更长的变量名称。 140 | 141 | 概念的特殊性也有助于保持变量名称的简洁。例如,假设只有一个数据库在使用,像`db`这样的短变量名通常可能保留给非常小的范围,即使范围非常大,也可能保持完全清晰。在这种情况下,根据范围的大小,单个词`database`可能是可接受的,但不是必需的,因为`db`是该词的一种非常常见的缩写,几乎没有其他解释。 142 | 143 | 局部变量的名称应该反映它包含的内容以及它在当前上下文中的使用方式,而不是值的来源。例如,通常情况下最佳局部变量名称与结构或协议缓冲区字段名称不同。 144 | 145 | 一般来说: 146 | 147 | - 像 `count` 或 `options` 这样的单字名称是一个很好的起点。 148 | 149 | - 可以添加其他词来消除相似名称的歧义,例如 `userCount` 和 `projectCount`。 150 | 151 | - 不要简单地省略字母来节省打字时间。例如,`Sandbox` 优于 `Sbx`,特别是对于导出的名称。 152 | 153 | - 大多数变量名可省略 [类型和类似类型的词](https://gocn.github.io/styleguide/docs/03-decisions/#变量名-vs-类型variable-name-vs-type) 154 | 155 | - 对于数字,`userCount` 是比 `numUsers` 或 `usersInt` 更好的名称。 156 | - 对于切片,`users` 是一个比 `userSlice` 更好的名字。 157 | - 如果范围内有两个版本的值,则包含类似类型的限定符是可以接受的,例如,你可能将输入存储在 `ageString` 中,并使用 `age` 作为解析值。 158 | 159 | - 省略[上下文](https://gocn.github.io/styleguide/docs/03-decisions/#外部上下文-vs-本地名称external-context-vs-local-names) 中清楚的单词。例如,在 UserCount 方法的实现中,名为 userCount 的局部变量可能是多余的; `count`、`users` 甚至 `c` 都具有可读性。 160 | 161 | #### 单字母变量名Single-letter variable names 162 | 163 | 单字母变量名是可以减少[重复](https://gocn.github.io/styleguide/docs/03-decisions/#重复repetition) 的有用工具,但也可能使代码变得不透明。将它们的使用限制在完整单词很明显以及它会重复出现以代替单字母变量的情况。 164 | 165 | 一般来说: 166 | 167 | - 对于[方法接收者变量](https://gocn.github.io/styleguide/docs/03-decisions/#方法接收者变量receiver-names),最好使用一个字母或两个字母的名称。 168 | - 对常见类型使用熟悉的变量名通常很有帮助: 169 | - `r` 用于 `io.Reader` 或 `*http.Request` 170 | - `w` 用于 `io.Writer` 或 `http.ResponseWriter` 171 | - 单字母标识符作为整数循环变量是可接受的,特别是对于索引(例如,`i`)和坐标(例如,`x` 和 `y`)。 172 | - 当范围很短时,循环标识符使用缩写是可接受的,例如`for _, n := range nodes { ... }`。 173 | 174 | ### 重复Repetition 175 | 176 | 一段 Go 源代码应该避免不必要的重复。一个常见的情形是重复名称,其中通常包含不必要的单词或重复其上下文或类型。如果相同或相似的代码段在很近的地方多次出现,代码本身也可能是不必要的重复。 177 | 178 | 重复命名可以有多种形式,包括: 179 | 180 | #### 包名 vs 可导出符号名Package vs. exported symbol name 181 | 182 | 当命名导出的符号时,包的名称始终在包外可见,因此应减少或消除两者之间的冗余信息。如果一个包如果需要仅导出一种类型并且以包本身命名,则构造函数的规范名称是`New`(如果需要的话)。 183 | 184 | > **实例:** 重复的名称 -> 更好的名称 185 | > 186 | > - `widget.NewWidget` -> `widget.New` 187 | > - `widget.NewWidgetWithName` -> `widget.NewWithName` 188 | > - `db.LoadFromDatabase` -> `db.Load` 189 | > - `goatteleportutil.CountGoatsTeleported` -> `gtutil.CountGoatsTeleported` or `goatteleport.Count` 190 | > - `myteampb.MyTeamMethodRequest` -> `mtpb.MyTeamMethodRequest` or `myteampb.MethodRequest` 191 | 192 | #### 变量名 vs 类型Variable name vs. type 193 | 194 | 编译器总是知道变量的类型,并且在大多数情况下,阅读者也可以通过变量的使用方式清楚地知道变量是什么类型。只有当一个变量的值在同一范围内出现两次时,才有需要明确变量的类型。 195 | 196 | | 重复的名称 | 更好的名称 | 197 | |-------------------------------|------------------------| 198 | | `var numUsers int` | `var users int` | 199 | | `var nameString string` | `var name string` | 200 | | `var primaryProject *Project` | `var primary *Project` | 201 | 202 | 如果该值以多种形式出现,这可以通过额外的词(如`raw`和`parsed`)或底层表示来澄清: 203 | 204 | ``` 205 | // Good: 206 | limitStr := r.FormValue("limit") 207 | limit, err := strconv.Atoi(limitStr) 208 | // Good: 209 | limitRaw := r.FormValue("limit") 210 | limit, err := strconv.Atoi(limitRaw) 211 | ``` 212 | 213 | #### 外部上下文 vs 本地名称External context vs. local names 214 | 215 | 包含来自周围上下文信息的名称通常会产生额外的噪音,而没有任何好处。包名、方法名、类型名、函数名、导入路径,包含来自其上下文信息的名称。 216 | 217 | ``` 218 | // Bad: 219 | // In package "ads/targeting/revenue/reporting" 220 | type AdsTargetingRevenueReport struct{} 221 | 222 | func (p *Project) ProjectName() string 223 | // Good: 224 | // In package "ads/targeting/revenue/reporting" 225 | type Report struct{} 226 | 227 | func (p *Project) Name() string 228 | // Bad: 229 | // In package "sqldb" 230 | type DBConnection struct{} 231 | // Good: 232 | // In package "sqldb" 233 | type Connection struct{} 234 | // Bad: 235 | // In package "ads/targeting" 236 | func Process(in *pb.FooProto) *Report { 237 | adsTargetingID := in.GetAdsTargetingID() 238 | } 239 | // Good: 240 | // In package "ads/targeting" 241 | func Process(in *pb.FooProto) *Report { 242 | id := in.GetAdsTargetingID() 243 | } 244 | ``` 245 | 246 | 重复通常应该在符号使用者的上下文中进行评估,而不是孤立地进行评估。例如,下面的代码有很多名称,在某些情况下可能没问题,但在上下文中是多余的: 247 | 248 | ``` 249 | // Bad: 250 | func (db *DB) UserCount() (userCount int, err error) { 251 | var userCountInt64 int64 252 | if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil { 253 | return 0, fmt.Errorf("failed to load user count: %s", dbLoadError) 254 | } 255 | userCount = int(userCountInt64) 256 | return userCount, nil 257 | } 258 | ``` 259 | 260 | 相反,在上下文和使用上信息是清楚的情况下,常常可以忽略: 261 | 262 | ``` 263 | // Good: 264 | func (db *DB) UserCount() (int, error) { 265 | var count int64 266 | if err := db.Load("count(distinct users)", &count); err != nil { 267 | return 0, fmt.Errorf("failed to load user count: %s", err) 268 | } 269 | return int(count), nil 270 | } 271 | ``` 272 | 273 | ## 评论Commentary 274 | 275 | 关于评论的约定(包括评论什么、使用什么风格、如何提供可运行的示例等)旨在支持阅读公共 API 文档的体验。有关详细信息,请参阅 [Effective Go](http://golang.org/doc/effective_go.html#commentary)。 276 | 277 | 最佳实践文档关于 [文档约定](https://gocn.github.io/styleguide/docs/04-best-practices/#公约) 的部分进一步讨论了这一点。 278 | 279 | **最佳实践:** 在开发和代码审查期间使用[文档预览](https://gocn.github.io/styleguide/docs/04-best-practices/#预览) 查看文档和可运行示例是否有用并以你期望的方式呈现。 280 | 281 | **提示:** Godoc 使用很少的特殊格式; 列表和代码片段通常应该缩进以避免换行。除缩进外,通常应避免装饰。 282 | 283 | ### 注释行长度Comment line length 284 | 285 | 确保注释在即使在较窄的屏幕上的可读性。 286 | 287 | 当评论变得太长时,建议将其包装成多个单行评论。在可能的情况下,争取在 80 列宽的终端上阅读良好的注释,但这并不是硬性限制; Go 中的注释没有固定的行长度限制。例如,标准库经常选择根据标点符号来打断注释,这有时会使个别行更接近 60-70 个字符标记。 288 | 289 | 有很多现有代码的注释长度超过 80 个字符。本指南不应作为更改此类代码作为可读性审查的一部分的理由(请参阅[一致](https://gocn.github.io/styleguide/docs/02-guide/#一致)),但鼓励团队作为其他重构的一部分,有机会时更新注释以遵循此指南。本指南的主要目标是确保所有 Go 可读性导师在提出建议时以及是否提出相同的建议。 290 | 291 | 有关评论的更多信息,请参阅此 [来自 The Go Blog 的帖子](https://blog.golang.org/godoc-documenting-go-code)。 292 | 293 | ``` 294 | # Good: 295 | // This is a comment paragraph. 296 | // The length of individual lines doesn't matter in Godoc; 297 | // but the choice of wrapping makes it easy to read on narrow screens. 298 | // 299 | // Don't worry too much about the long URL: 300 | // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ 301 | // 302 | // Similarly, if you have other information that is made awkward 303 | // by too many line breaks, use your judgment and include a long line 304 | // if it helps rather than hinders. 305 | ``` 306 | 307 | 避免注释在小屏幕上重复换行,这是一种糟糕的阅读体验。 308 | 309 | ``` 310 | # Bad: 311 | // This is a comment paragraph. The length of individual lines doesn't matter in 312 | Godoc; 313 | // but the choice of wrapping causes jagged lines on narrow screens or in 314 | Critique, 315 | // which can be annoying, especially when in a comment block that will wrap 316 | repeatedly. 317 | // 318 | // Don't worry too much about the long URL: 319 | // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ 320 | ``` 321 | 322 | ### 文档注释Doc comments 323 | 324 | 所有顶级导出名称都必须有文档注释,具有不明显行为或含义的未导出类型或函数声明也应如此。这些注释应该是[完整句子](https://gocn.github.io/styleguide/docs/03-decisions/#注释语句comment-sentences),以所描述对象的名称开头。冠词(“a”、“an”、“the”)可以放在名字前面,使其读起来更自然。 325 | 326 | ``` 327 | // Good: 328 | // A Request represents a request to run a command. 329 | type Request struct { ... 330 | 331 | // Encode writes the JSON encoding of req to w. 332 | func Encode(w io.Writer, req *Request) { ... 333 | ``` 334 | 335 | 文档注释出现在 [Godoc](https://pkg.go.dev/) 中,并通过 IDE 显示,因此应该为使用该包的任何人编写文档注释。 336 | 337 | 如果出现在结构中,文档注释适用于以下符号或字段组: 338 | 339 | ``` 340 | // Good: 341 | // Options configure the group management service. 342 | type Options struct { 343 | // General setup: 344 | Name string 345 | Group *FooGroup 346 | 347 | // Dependencies: 348 | DB *sql.DB 349 | 350 | // Customization: 351 | LargeGroupThreshold int // optional; default: 10 352 | MinimumMembers int // optional; default: 2 353 | } 354 | ``` 355 | 356 | **最佳实践:** 如果你对未导出的代码有文档注释,请遵循与导出代码相同的习惯(即,以未导出的名称开始注释)。这使得以后导出它变得容易,只需在注释和代码中用新导出的名称替换未导出的名称即可。 357 | 358 | ### 注释语句Comment sentences 359 | 360 | 完整的注释应该像标准英语句子一样包含大写和标点符号。(作为一个例外,如果在其他方面很清楚,可以以非大写的标识符名称开始一个句子。这种情况最好只在段落的开头进行。) 361 | 362 | 作为句子片段的注释对标点符号或大小写没有此类要求。 363 | 364 | [文档注释](https://gocn.github.io/styleguide/docs/03-decisions/#文档注释doc-comments) 应始终是完整的句子,因此应始终大写和标点符号。简单的行尾注释(特别是对于结构字段)可以为假设字段名称是主语的简单短语。 365 | 366 | ``` 367 | // Good: 368 | // A Server handles serving quotes from the collected works of Shakespeare. 369 | type Server struct { 370 | // BaseDir points to the base directory under which Shakespeare's works are stored. 371 | // 372 | // The directory structure is expected to be the following: 373 | // {BaseDir}/manifest.json 374 | // {BaseDir}/{name}/{name}-part{number}.txt 375 | BaseDir string 376 | 377 | WelcomeMessage string // displayed when user logs in 378 | ProtocolVersion string // checked against incoming requests 379 | PageLength int // lines per page when printing (optional; default: 20) 380 | } 381 | ``` 382 | 383 | ### 示例Examples 384 | 385 | 包应该清楚地记录它们的预期用途。尝试提供一个[可运行的例子](http://blog.golang.org/examples); 示例出现在 Godoc 中。可运行示例属于测试文件,而不是生产源文件。请参阅此示例([Godoc](https://pkg.go.dev/time#example-Duration),[source](https://cs.opensource.google/go/go/+/HEAD:src/time /example_test.go))。 386 | 387 | 如果无法提供可运行的示例,可以在代码注释中提供示例代码。与注释中的其他代码和命令行片段一样,它应该遵循标准格式约定。 388 | 389 | ### 命名的结果参数Named result parameters 390 | 391 | 当有命名参数时,请考虑函数签名在 Godoc 中的显示方式。函数本身的名称和结果参数的类型通常要足够清楚。 392 | 393 | ``` 394 | // Good: 395 | func (n *Node) Parent1() *Node 396 | func (n *Node) Parent2() (*Node, error) 397 | ``` 398 | 399 | 如果一个函数返回两个或多个相同类型的参数,添加名称会很有用。 400 | 401 | ``` 402 | // Good: 403 | func (n *Node) Children() (left, right *Node, err error) 404 | ``` 405 | 406 | 如果调用者必须对特定的结果参数采取行动,命名它们可以帮助暗示行动是什么: 407 | 408 | ``` 409 | // Good: 410 | // WithTimeout returns a context that will be canceled no later than d duration 411 | // from now. 412 | // 413 | // The caller must arrange for the returned cancel function to be called when 414 | // the context is no longer needed to prevent a resource leak. 415 | func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func()) 416 | ``` 417 | 418 | 在上面的代码中,取消是调用者必须执行的特定操作。但是,如果将结果参数单独写为`(Context, func())`,“取消函数”的含义就不清楚了。 419 | 420 | 当名称产生 [不必要的重复](https://gocn.github.io/styleguide/docs/03-decisions/#变量名-vs-类型variable-name-vs-type) 时,不要使用命名结果参数。 421 | 422 | ``` 423 | // Bad: 424 | func (n *Node) Parent1() (node *Node) 425 | func (n *Node) Parent2() (node *Node, err error) 426 | ``` 427 | 428 | 不要为了避免在函数内声明变量而使用命名结果参数。这种做法会导致不必要的冗长API,但收益只是微小的简洁性。 429 | 430 | [裸返回](https://tour.golang.org/basics/7) 仅在小函数中是可接受的。一旦它是一个中等大小的函数,就需要明确你的返回值。同样,不要仅仅因为可以裸返回就使用命名结果参数。[清晰](https://gocn.github.io/styleguide/docs/02-guide/#清晰) 总是比在你的函数中节省几行更重要。 431 | 432 | 如果必须在延迟闭包中更改结果参数的值,则命名结果参数始终是可以接受的。 433 | 434 | > **提示:** 类型通常比函数签名中的名称更清晰。[GoTip #38:作为命名类型的函数](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 演示了这一点。 435 | > 436 | > 在上面的 [`WithTimeout`](https://pkg.go.dev/context#WithTimeout) 中,代码使用了一个 [`CancelFunc`](https://pkg.go.dev/context#CancelFunc) 而不是结果参数列表中的原始`func()`,并且几乎不需要做任何记录工作。 437 | 438 | ### 包注释 439 | 440 | 包注释必须出现在包内语句的上方,注释和包名称之间没有空行。例子: 441 | 442 | ``` 443 | // Good: 444 | // Package math provides basic constants and mathematical functions. 445 | // 446 | // This package does not guarantee bit-identical results across architectures. 447 | package math 448 | ``` 449 | 450 | 每个包必须有一个包注释。如果一个包由多个文件组成,那么其中一个文件应该有包注释。 451 | 452 | `main` 包的注释形式略有不同,其中 BUILD 文件中的 `go_binary` 规则的名称代替了包名。 453 | 454 | ``` 455 | // Good: 456 | // The seed_generator command is a utility that generates a Finch seed file 457 | // from a set of JSON study configs. 458 | package main 459 | ``` 460 | 461 | 只要二进制文件的名称与 BUILD 文件中所写的完全一致,其他风格的注释也是可以了。当二进制名称是第一个单词时,即使它与命令行调用的拼写不严格匹配,也需要将其大写。 462 | 463 | ``` 464 | // Good: 465 | // Binary seed_generator ... 466 | // Command seed_generator ... 467 | // Program seed_generator ... 468 | // The seed_generator command ... 469 | // The seed_generator program ... 470 | // Seed_generator ... 471 | ``` 472 | 473 | 提示: 474 | 475 | - 命令行调用示例和 API 用法可以是有用的文档。对于 Godoc 格式,缩进包含代码的注释行。 476 | 477 | - 如果没有明显的main文件或者包注释特别长,可以将文档注释放在名为 doc.go 的文件中,只有注释和包子句。 478 | 479 | - 可以使用多行注释代替多个单行注释。如果文档包含可能对从源文件复制和粘贴有用的部分,如示例命令行(用于二进制文件)和模板示例,这将非常有用。 480 | 481 | ``` 482 | // Good: 483 | /* 484 | The seed_generator command is a utility that generates a Finch seed file 485 | from a set of JSON study configs. 486 | 487 | seed_generator *.json | base64 > finch-seed.base64 488 | */ 489 | package template 490 | ``` 491 | 492 | - 供维护者使用且适用于整个文件的注释通常放在导入声明之后。这些不会出现在 Godoc 中,也不受上述包注释规则的约束。 493 | 494 | ## 导入 495 | 496 | ### 导入重命名 497 | 498 | 只有在为了避免与其他导入的名称冲突时,才使用重命名导入。(由此推论,[好的包名称](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names) 不需要重命名。)如果发生名称冲突,最好重命名 最本地或特定于项目的导入。包的本地别名必须遵循[包命名指南](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names),包括禁止使用下划线和大写字母。 499 | 500 | 生成的 protocol buffer 包必须重命名以从其名称中删除下划线,并且它们的别名必须具有 `pb` 后缀。有关详细信息,请参阅[ proto 和 stub 最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#protos-and-stubs)。 501 | 502 | ``` 503 | // Good: 504 | import ( 505 | fspb "path/to/package/foo_service_go_proto" 506 | ) 507 | ``` 508 | 509 | 导入的包名称没有有用的识别信息时(例如 `package v1`),应该重命名以包括以前的路径组件。重命名必须与导入相同包的其他本地文件一致,并且可以包括版本号。 510 | 511 | **注意:** 最好重命名包以符合 [好的包命名规则](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names),但在vendor目录下的包通常是不可行的。 512 | 513 | 514 | ``` 515 | // Good: 516 | import ( 517 | core "github.com/kubernetes/api/core/v1" 518 | meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1" 519 | ) 520 | ``` 521 | 522 | 如果你需要导入一个名称与你要使用的公共局部变量名称(例如 `url`、`ssh`)冲突的包,并且你希望重命名该包,首选方法是使用 `pkg` 后缀(例如 `urlpkg`)。请注意,可以使用局部变量隐藏包; 仅当此类变量在范围内时仍需要使用此包时,才需要重命名。 523 | 524 | ### 导入分组 525 | 526 | 导入应分为两组: 527 | 528 | - 标准库包 529 | - 其他(项目和vendor)包 530 | 531 | ``` 532 | // Good: 533 | package main 534 | 535 | import ( 536 | "fmt" 537 | "hash/adler32" 538 | "os" 539 | 540 | "github.com/dsnet/compress/flate" 541 | "golang.org/x/text/encoding" 542 | "google.golang.org/protobuf/proto" 543 | foopb "myproj/foo/proto/proto" 544 | _ "myproj/rpc/protocols/dial" 545 | _ "myproj/security/auth/authhooks" 546 | ) 547 | ``` 548 | 549 | 将导入项分成多个组是可以接受的,例如,如果你想要一个单独的组来重命名、导入仅为了特殊效果 或另一个特殊的导入组。 550 | 551 | ``` 552 | // Good: 553 | package main 554 | 555 | import ( 556 | "fmt" 557 | "hash/adler32" 558 | "os" 559 | 560 | "github.com/dsnet/compress/flate" 561 | "golang.org/x/text/encoding" 562 | "google.golang.org/protobuf/proto" 563 | 564 | foopb "myproj/foo/proto/proto" 565 | 566 | _ "myproj/rpc/protocols/dial" 567 | _ "myproj/security/auth/authhooks" 568 | ) 569 | ``` 570 | 571 | **注意:** [goimports](https://google.github.io/styleguide/go/golang.org/x/tools/cmd/goimports) 不支持维护可选组 - 超出标准库和 Google 导入之间强制分离所需的拆分。为了保持符合状态,额外的导入子组需要作者和审阅人的注意。 572 | 573 | Google 程序有时也是 AppEngine 应用程序,应该有一个单独的组用于 AppEngine 导入。 574 | 575 | Gofmt 负责按导入路径对每个组进行排序。但是,它不会自动将导入分成组。流行的 [goimports](https://google.github.io/styleguide/go/golang.org/x/tools/cmd/goimports) 工具结合了 Gofmt 和导入管理,根据上述规则将导入进行分组。通过 [goimports](https://google.github.io/styleguide/go/golang.org/x/tools/cmd/goimports) 来管理导入顺序是可行的,但随着文件的修改,其导入列表必须保持内部一致。 576 | 577 | ### 导入"blank" (`import _`) 578 | 579 | 使用语法 `import _ "package"`导入的包,称为副作用导入,只能在主包或需要它们的测试中导入。 580 | 581 | 此类包的一些示例包括: 582 | 583 | - [time/tzdata](https://pkg.go.dev/time/tzdata) 584 | - [image/jpeg](https://pkg.go.dev/image/jpeg) 在图像处理中的代码 585 | 586 | 避免在工具包中导入空白,即使工具包间接依赖于它们。将副作用导入限制到主包有助于控制依赖性,并使得编写依赖于不同导入的测试成为可能,而不会发生冲突或浪费构建成本。 587 | 588 | 以下是此规则的唯一例外情况: 589 | 590 | - 你可以使用空白导入来绕过 [nogo 静态检查器](https://github.com/bazelbuild/rules_go/blob/master/go/nogo.rst) 中对不允许导入的检查。 591 | - 你可以在使用 `//go:embed` 编译器指令的源文件中使用 [embed](https://pkg.go.dev/embed) 包的空白导入。 592 | 593 | **提示:** 如果生产环境中你创建的工具包间接依赖于副作用导入,请记录这里的预期用途。 594 | 595 | ### 导入 "dot" (`import .`) 596 | 597 | `import .` 形式是一种语言特性,它允许将从另一个包导出的标识符无条件地带到当前包中。有关更多信息,请参阅[语言规范](https://go.dev/ref/spec#Import_declarations)。 598 | 599 | **不要**在 Google 代码库中使用此功能; 这使得更难判断功能来自何处。 600 | 601 | ``` 602 | // Bad: 603 | package foo_test 604 | 605 | import ( 606 | "bar/testutil" // also imports "foo" 607 | . "foo" 608 | ) 609 | 610 | var myThing = Bar() // Bar defined in package foo; no qualification needed. 611 | // Good: 612 | package foo_test 613 | 614 | import ( 615 | "bar/testutil" // also imports "foo" 616 | "foo" 617 | ) 618 | 619 | var myThing = foo.Bar() 620 | ``` 621 | 622 | ## 错误 623 | 624 | ### 返回错误 625 | 626 | 使用 `error` 表示函数可能会失败。按照惯例,`error` 是最后一个结果参数。 627 | 628 | ``` 629 | // Good: 630 | func Good() error { /* ... */ } 631 | ``` 632 | 633 | 返回 `nil` 错误是表示操作成功的惯用方式,否则表示可能会失败。如果函数返回错误,除非另有明确说明,否则调用者必须将所有非错误返回值视为未确定。通常来说,非错误返回值是它们的零值,但也不能直接这么假设。 634 | 635 | ``` 636 | // Good: 637 | func GoodLookup() (*Result, error) { 638 | // ... 639 | if err != nil { 640 | return nil, err 641 | } 642 | return res, nil 643 | } 644 | ``` 645 | 646 | 返回错误的导出函数应使用`error`类型返回它们。具体的错误类型容易受到细微错误的影响:一个 `nil` 指针可以包装到接口中,从而就变成非 nil 值(参见 [关于该主题的 Go FAQ 条目](https://golang.org/doc/faq#nil_error))。 647 | 648 | ``` 649 | // Bad: 650 | func Bad() *os.PathError { /*...*/ } 651 | ``` 652 | 653 | **提示**:采用 `context.Context` 参数的函数通常应返回 `error`,以便调用者可以确定上下文是否在函数运行时被取消。 654 | 655 | ### 错误字符串 656 | 657 | 错误字符串不应大写(除非以导出名称、专有名词或首字母缩写词开头)并且不应以标点符号结尾。这是因为错误字符串通常在打印给用户之前出现在其他上下文中。 658 | 659 | ``` 660 | // Bad: 661 | err := fmt.Errorf("Something bad happened.") 662 | // Good: 663 | err := fmt.Errorf("something bad happened") 664 | ``` 665 | 666 | 另一方面,完整显示消息(日志记录、测试失败、API 响应或其他 UI)的样式视情况而定,但通常应大写首字母。 667 | 668 | ``` 669 | // Good: 670 | log.Infof("Operation aborted: %v", err) 671 | log.Errorf("Operation aborted: %v", err) 672 | t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err) 673 | ``` 674 | 675 | ### 错误处理 676 | 677 | 遇到错误的代码应该慎重选择如何处理它。使用 _ 变量丢弃错误通常是不合适的。如果函数返回错误,请执行以下操作之一: 678 | 679 | - 立即处理并解决错误 680 | - 将错误返回给调用者 681 | - 在特殊情况下,调用 [`log.Fatal`](https://pkg.go.dev/github.com/golang/glog#Fatal) 或(如绝对有必要)则调用 `panic` 682 | 683 | **注意:** `log.Fatalf` 不是标准库日志。参见 [#logging]。 684 | 685 | 在极少数情况下适合忽略或丢弃错误(例如调用 [`(*bytes.Buffer).Write`](https://pkg.go.dev/bytes#Buffer.Write) 被记录为永远不会失败),随附的注释应该解释为什么这是安全的。 686 | 687 | ``` 688 | // Good: 689 | var b *bytes.Buffer 690 | 691 | n, _ := b.Write(p) // never returns a non-nil error 692 | ``` 693 | 694 | 关于错误处理的更多讨论和例子,请参见[Effective Go](http://golang.org/doc/effective_go.html#errors)和[最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#错误处理)。 695 | 696 | ### In-band 错误 697 | 698 | 在C和类似语言中,函数通常会返回-1、null或空字符串等值,以示错误或丢失结果。这就是所谓的`In-band`处理。 699 | 700 | ``` 701 | // Bad: 702 | // Lookup returns the value for key or -1 if there is no mapping for key. 703 | func Lookup(key string) int 704 | ``` 705 | 706 | 未能检查`In-band`错误值会导致错误,并可能将 error 归于错误的功能。 707 | 708 | ``` 709 | // Bad: 710 | // The following line returns an error that Parse failed for the input value, 711 | // whereas the failure was that there is no mapping for missingKey. 712 | return Parse(Lookup(missingKey)) 713 | ``` 714 | 715 | Go对多重返回值的支持提供了一个更好的解决方案(见[Effective Go关于多重返回的部分](http://golang.org/doc/effective_go.html#multiple-returns))。与其要求调用方检查`In-band`的错误值,函数更应该返回一个额外的值来表明返回值是否有效。这个返回值可以是一个错误,或者在不需要解释时是一个布尔值,并且应该是最终的返回值。 716 | 717 | ``` 718 | // Good: 719 | // Lookup returns the value for key or ok=false if there is no mapping for key. 720 | func Lookup(key string) (value string, ok bool) 721 | ``` 722 | 723 | 这个 API 可以防止调用者错误地编写`Parse(Lookup(key))`,从而导致编译时错误,因为`Lookup(key)`有两个返回值。 724 | 725 | 以这种方式返回错误,来构筑更强大和明确的错误处理。 726 | 727 | ``` 728 | // Good: 729 | value, ok := Lookup(key) 730 | if !ok { 731 | return fmt.Errorf("no value for %q", key) 732 | } 733 | return Parse(value) 734 | ``` 735 | 736 | 一些标准库函数,如包`strings`中的函数,返回`In-band`错误值。这大大简化了字符串处理的代码,但代价是要求程序员更加勤奋。一般来说,Google 代码库中的 Go 代码应该为错误返回额外的值 737 | 738 | ### 缩进错误流程 739 | 740 | 在继续代码的其余部分之前处理错误。这提高了代码的可读性,使读者能够快速找到正常路径。这个逻辑同样适用于任何测试条件并以终端条件结束的代码块(例如,`return`、`panic`、`log.Fatal`)。 741 | 742 | 如果终止条件没有得到满足,运行的代码应该出现在`if`块之后,而不应该缩进到`else`子句中。 743 | 744 | ``` 745 | // Good: 746 | if err != nil { 747 | // error handling 748 | return // or continue, etc. 749 | } 750 | // normal code 751 | // Bad: 752 | if err != nil { 753 | // error handling 754 | } else { 755 | // normal code that looks abnormal due to indentation 756 | } 757 | ``` 758 | 759 | > **提示:** 如果你使用一个变量超过几行代码,通常不值得使用`带有初始化的 if `风格。在这种情况下,通常最好将声明移出,使用标准的`if`语句。 760 | > 761 | > ``` 762 | > // Good: 763 | > x, err := f() 764 | > if err != nil { 765 | > // error handling 766 | > return 767 | > } 768 | > // lots of code that uses x 769 | > // across multiple lines 770 | > // Bad: 771 | > if x, err := f(); err != nil { 772 | > // error handling 773 | > return 774 | > } else { 775 | > // lots of code that uses x 776 | > // across multiple lines 777 | > } 778 | > ``` 779 | 780 | 更多细节见[Go Tip #1:视线](https://gocn.github.io/styleguide/docs/01-overview/#gotip)和[TotT:通过减少嵌套降低代码的复杂性](https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html)。 781 | 782 | ## 语言 783 | 784 | ### 字面格式化 785 | 786 | Go 有一个非常强大的[复合字面语法](https://golang.org/ref/spec#Composite_literals),用它可以在一个表达式中表达深度嵌套的复杂值。在可能的情况下,应该使用这种字面语法,而不是逐字段建值。字面意义的 `gofmt`格式一般都很好,但有一些额外的规则可以使这些字面意义保持可读和可维护。 787 | 788 | #### 字段名称 789 | 790 | 对于在当前包之外定义的类型,结构体字面量通常应该指定**字段名**。 791 | 792 | - 包括来自其他包的类型的字段名。 793 | 794 | ``` 795 | // Good: 796 | good := otherpkg.Type{A: 42} 797 | ``` 798 | 799 | 结构中字段的位置和字段的完整集合(当字段名被省略时,这两者都是有必要搞清楚的)通常不被认为是结构的公共 API 的一部分;需要指定字段名以避免不必要的耦合。 800 | 801 | ``` 802 | // Bad: 803 | // https://pkg.go.dev/encoding/csv#Reader 804 | r := csv.Reader{',', '#', 4, false, false, false, false} 805 | ``` 806 | 807 | 在小型、简单的结构中可以省略字段名,这些结构的组成和顺序都有文档证明是稳定的。 808 | 809 | ``` 810 | // Good: 811 | okay := image.Point{42, 54} 812 | also := image.Point{X: 42, Y: 54} 813 | ``` 814 | 815 | - 对于包内类型,字段名是可选的。 816 | 817 | ``` 818 | // Good: 819 | okay := Type{42} 820 | also := internalType{4, 2} 821 | ``` 822 | 如果能使代码更清晰,还是应该使用字段名,而且这样做是很常见的。例如,一个有大量字段的结构几乎都应该用字段名来初始化。 823 | 824 | ``` 825 | // Good: 826 | okay := StructWithLotsOfFields{ 827 | field1: 1, 828 | field2: "two", 829 | field3: 3.14, 830 | field4: true, 831 | } 832 | ``` 833 | 834 | #### 匹配的大括号 835 | 836 | 一对大括号的最后一半应该总是出现在一行中,其缩进量与开头的大括号相同。单行字词必然具有这个属性。当字面意义跨越多行时,保持这一属性可以使字面意义的括号匹配与函数和`if`语句等常见 Go 语法结构的括号匹配相同。 837 | 838 | 这方面最常见的错误是在多行结构字中把收尾括号与值放在同一行。在这种情况下,该行应以逗号结束,收尾括号应出现在下一行。 839 | 840 | ``` 841 | // Good: 842 | good := []*Type{{Key: "value"}} 843 | // Good: 844 | good := []*Type{ 845 | {Key: "multi"}, 846 | {Key: "line"}, 847 | } 848 | // Bad: 849 | bad := []*Type{ 850 | {Key: "multi"}, 851 | {Key: "line"}} 852 | // Bad: 853 | bad := []*Type{ 854 | { 855 | Key: "value"}, 856 | } 857 | ``` 858 | 859 | #### Cuddled 大括号 860 | 861 | 只有在以下两种情况下,才允许在大括号之间为切片和数组丢弃空格(又称 "“cuddling”)。 862 | 863 | - [缩进匹配](https://gocn.github.io/styleguide/docs/03-decisions/#匹配的大括号) 864 | - 内部值也是字面意义或原语构建者(即不是变量或其他表达式) 865 | 866 | ``` 867 | // Good: 868 | good := []*Type{ 869 | { // Not cuddled 870 | Field: "value", 871 | }, 872 | { 873 | Field: "value", 874 | }, 875 | } 876 | // Good: 877 | good := []*Type{{ // Cuddled correctly 878 | Field: "value", 879 | }, { 880 | Field: "value", 881 | }} 882 | // Good: 883 | good := []*Type{ 884 | first, // Can't be cuddled 885 | {Field: "second"}, 886 | } 887 | // Good: 888 | okay := []*pb.Type{pb.Type_builder{ 889 | Field: "first", // Proto Builders may be cuddled to save vertical space 890 | }.Build(), pb.Type_builder{ 891 | Field: "second", 892 | }.Build()} 893 | // Bad: 894 | bad := []*Type{ 895 | first, 896 | { 897 | Field: "second", 898 | }} 899 | ``` 900 | 901 | #### 重复的类型名称 902 | 903 | 重复的类型名称可以从 slice 和 map 字面上省略,这对减少杂乱是有帮助的。明确重复类型名称的一个合理场合,当在你的项目中处理一个不常见的复杂类型时,当重复的类型名称在一行上却相隔很远的时候,可以提醒读者的上下文。 904 | 905 | ``` 906 | // Good: 907 | good := []*Type{ 908 | {A: 42}, 909 | {A: 43}, 910 | } 911 | // Bad: 912 | repetitive := []*Type{ 913 | &Type{A: 42}, 914 | &Type{A: 43}, 915 | } 916 | // Good: 917 | good := map[Type1]*Type2{ 918 | {A: 1}: {B: 2}, 919 | {A: 3}: {B: 4}, 920 | } 921 | // Bad: 922 | repetitive := map[Type1]*Type2{ 923 | Type1{A: 1}: &Type2{B: 2}, 924 | Type1{A: 3}: &Type2{B: 4}, 925 | } 926 | ``` 927 | 928 | **提示:** 如果你想删除结构字中重复的类型名称,可以运行`gofmt -s`。 929 | 930 | #### 零值字段 931 | 932 | [零值](https://golang.org/ref/spec#The_zero_value)字段可以从结构字段中省略,但不能因此而失去`清晰`这个风格原则。 933 | 934 | 设计良好的 API 经常采用零值结构来提高可读性。例如,从下面的结构中省略三个零值字段,可以使人们注意到正在指定的唯一选项。 935 | 936 | ``` 937 | // Bad: 938 | import ( 939 | "github.com/golang/leveldb" 940 | "github.com/golang/leveldb/db" 941 | ) 942 | 943 | ldb := leveldb.Open("/my/table", &db.Options{ 944 | BlockSize int: 1<<16, 945 | ErrorIfDBExists: true, 946 | 947 | // These fields all have their zero values. 948 | BlockRestartInterval: 0, 949 | Comparer: nil, 950 | Compression: nil, 951 | FileSystem: nil, 952 | FilterPolicy: nil, 953 | MaxOpenFiles: 0, 954 | WriteBufferSize: 0, 955 | VerifyChecksums: false, 956 | }) 957 | // Good: 958 | import ( 959 | "github.com/golang/leveldb" 960 | "github.com/golang/leveldb/db" 961 | ) 962 | 963 | ldb := leveldb.Open("/my/table", &db.Options{ 964 | BlockSize int: 1<<16, 965 | ErrorIfDBExists: true, 966 | }) 967 | ``` 968 | 969 | 表驱动的测试中的结构经常受益于[显式字段名](https://gocn.github.io/styleguide/docs/03-decisions/#字段名称),特别是当测试结构不是琐碎的时候。这允许作者在有关字段与测试用例无关时完全省略零值字段。例如,成功的测试案例应该省略任何与错误或失败相关的字段。在零值对于理解测试用例是必要的情况下,例如测试零或 `nil` 输入,应该指定字段名。 970 | 971 | **简明** 972 | 973 | ``` 974 | tests := []struct { 975 | input string 976 | wantPieces []string 977 | wantErr error 978 | }{ 979 | { 980 | input: "1.2.3.4", 981 | wantPieces: []string{"1", "2", "3", "4"}, 982 | }, 983 | { 984 | input: "hostname", 985 | wantErr: ErrBadHostname, 986 | }, 987 | } 988 | ``` 989 | 990 | **明确** 991 | 992 | ``` 993 | tests := []struct { 994 | input string 995 | wantIPv4 bool 996 | wantIPv6 bool 997 | wantErr bool 998 | }{ 999 | { 1000 | input: "1.2.3.4", 1001 | wantIPv4: true, 1002 | wantIPv6: false, 1003 | }, 1004 | { 1005 | input: "1:2::3:4", 1006 | wantIPv4: false, 1007 | wantIPv6: true, 1008 | }, 1009 | { 1010 | input: "hostname", 1011 | wantIPv4: false, 1012 | wantIPv6: false, 1013 | wantErr: true, 1014 | }, 1015 | } 1016 | ``` 1017 | 1018 | ### Nil 切片 1019 | 1020 | 在大多数情况下,`nil`和空切片之间没有功能上的区别。像`len`和`cap`这样的内置函数在`nil`片上的表现与预期相同。 1021 | 1022 | ``` 1023 | // Good: 1024 | import "fmt" 1025 | 1026 | var s []int // nil 1027 | 1028 | fmt.Println(s) // [] 1029 | fmt.Println(len(s)) // 0 1030 | fmt.Println(cap(s)) // 0 1031 | for range s {...} // no-op 1032 | 1033 | s = append(s, 42) 1034 | fmt.Println(s) // [42] 1035 | ``` 1036 | 1037 | 如果你声明一个空切片作为局部变量(特别是如果它可以成为返回值的来源),最好选择 nil 初始化,以减少调用者的错误风险。 1038 | 1039 | ``` 1040 | // Good: 1041 | var t []string 1042 | // Bad: 1043 | t := []string{} 1044 | ``` 1045 | 1046 | 不要创建强迫调用者区分 nil 和空切片的 API。 1047 | 1048 | ``` 1049 | // Good: 1050 | // Ping pings its targets. 1051 | // Returns hosts that successfully responded. 1052 | func Ping(hosts []string) ([]string, error) { ... } 1053 | // Bad: 1054 | // Ping pings its targets and returns a list of hosts 1055 | // that successfully responded. Can be empty if the input was empty. 1056 | // nil signifies that a system error occurred. 1057 | func Ping(hosts []string) []string { ... } 1058 | ``` 1059 | 1060 | 在设计接口时,避免区分 `nil` 切片和非 `nil` 的零长度切片,因为这可能导致微妙的编程错误。这通常是通过使用`len`来检查是否为空,而不是`==nil`来实现的。 1061 | 1062 | 这个实现同时将`nil`和零长度的切片视为 "空"。 1063 | 1064 | ``` 1065 | // Good: 1066 | // describeInts describes s with the given prefix, unless s is empty. 1067 | func describeInts(prefix string, s []int) { 1068 | if len(s) == 0 { 1069 | return 1070 | } 1071 | fmt.Println(prefix, s) 1072 | } 1073 | ``` 1074 | 1075 | 而不是依靠二者的区别作为API的一部分: 1076 | ``` 1077 | // Bad: 1078 | func maybeInts() []int { /* ... */ } 1079 | 1080 | // describeInts describes s with the given prefix; pass nil to skip completely. 1081 | func describeInts(prefix string, s []int) { 1082 | // The behavior of this function unintentionally changes depending on what 1083 | // maybeInts() returns in 'empty' cases (nil or []int{}). 1084 | if s == nil { 1085 | return 1086 | } 1087 | fmt.Println(prefix, s) 1088 | } 1089 | 1090 | describeInts("Here are some ints:", maybeInts()) 1091 | ``` 1092 | 1093 | 详见 [in-band 错误](https://gocn.github.io/styleguide/docs/03-decisions/#in-band-错误). 1094 | 1095 | ### 缩进的混乱 1096 | 1097 | 如果断行会使其余的行与缩进的代码块对齐,则应避免引入断行。如果这是不可避免的,请留下一个空间,将代码块中的代码与包线分开。 1098 | 1099 | ``` 1100 | // Bad: 1101 | if longCondition1 && longCondition2 && 1102 | // Conditions 3 and 4 have the same indentation as the code within the if. 1103 | longCondition3 && longCondition4 { 1104 | log.Info("all conditions met") 1105 | } 1106 | ``` 1107 | 1108 | 具体准则和例子见以下章节: 1109 | 1110 | - [函数格式化](https://gocn.github.io/styleguide/docs/03-decisions/#函数格式化) 1111 | - [Conditionals and loops](https://gocn.github.io/styleguide/docs/03-decisions/#条件和循环) 1112 | - [Literal formatting](https://gocn.github.io/styleguide/docs/03-decisions/#字面格式化) 1113 | 1114 | ### 函数格式化 1115 | 1116 | 函数定义或方法声明的签名应该保持在一行,以避免[缩进的混乱](https://gocn.github.io/styleguide/docs/03-decisions/#缩进的混乱)。 1117 | 1118 | 函数参数列表可以成为Go源文件中最长的几行。然而,它们在缩进的变化之前,因此很难以不使后续行看起来像函数体的一部分的混乱方式来断行。 1119 | 1120 | ``` 1121 | // Bad: 1122 | func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string, 1123 | foo4, foo5, foo6 int) { 1124 | foo7 := bar(foo1) 1125 | // ... 1126 | } 1127 | ``` 1128 | 1129 | 参见[最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#函数参数列表),了解一些缩短函数调用的选择,否则这些函数会有很多参数。 1130 | 1131 | ``` 1132 | // Good: 1133 | good := foo.Call(long, CallOptions{ 1134 | Names: list, 1135 | Of: of, 1136 | The: parameters, 1137 | Func: all, 1138 | Args: on, 1139 | Now: separate, 1140 | Visible: lines, 1141 | }) 1142 | // Bad: 1143 | bad := foo.Call( 1144 | long, 1145 | list, 1146 | of, 1147 | parameters, 1148 | all, 1149 | on, 1150 | separate, 1151 | lines, 1152 | ) 1153 | ``` 1154 | 1155 | 通过分解局部变量,通常可以缩短行数。 1156 | 1157 | ``` 1158 | // Good: 1159 | local := helper(some, parameters, here) 1160 | good := foo.Call(list, of, parameters, local) 1161 | ``` 1162 | 1163 | 类似地,函数和方法调用不应该仅仅由于行的长度而进行换行。 1164 | 1165 | ``` 1166 | // Good: 1167 | good := foo.Call(long, list, of, parameters, all, on, one, line) 1168 | // Bad: 1169 | bad := foo.Call(long, list, of, parameters, 1170 | with, arbitrary, line, breaks) 1171 | ``` 1172 | 1173 | 不要为特定的函数参数添加注释。相反,使用 [option struct](https://gocn.github.io/styleguide/docs/04-best-practices/#option-模式) 或在函数文档中添加更多细节。 1174 | 1175 | ``` 1176 | // Good: 1177 | good := server.New(ctx, server.Options{Port: 42}) 1178 | // Bad: 1179 | bad := server.New( 1180 | ctx, 1181 | 42, // Port 1182 | ) 1183 | ``` 1184 | 1185 | 如果调用参数确实长得令人很难受,那么就应该考虑重构: 1186 | 1187 | ``` 1188 | // Good: 1189 | // Sometimes variadic arguments can be factored out 1190 | replacements := []string{ 1191 | "from", "to", // related values can be formatted adjacent to one another 1192 | "source", "dest", 1193 | "original", "new", 1194 | } 1195 | 1196 | // Use the replacement struct as inputs to NewReplacer. 1197 | replacer := strings.NewReplacer(replacements...) 1198 | ``` 1199 | 1200 | 当 API 无法更改或本地调用是不频繁的(无论调用是否太长),在有助于理解本次调用的前提下,那么是始终允许添加换行符的。 1201 | 1202 | ``` 1203 | // Good: 1204 | canvas.RenderCube(cube, 1205 | x0, y0, z0, 1206 | x0, y0, z1, 1207 | x0, y1, z0, 1208 | x0, y1, z1, 1209 | x1, y0, z0, 1210 | x1, y0, z1, 1211 | x1, y1, z0, 1212 | x1, y1, z1, 1213 | ) 1214 | ``` 1215 | 1216 | 请注意,上面示例中的行没有在特定的列边界处换行,而是根据坐标三元组进行分组。 1217 | 1218 | 函数中的长字符串不应该因为行的长度而被破坏。对于包含此类字符串的函数,可以在字符串格式之后添加换行符,并且可以在下一行或后续行中提供参数。最好根据输入的语义分组来决定换行符应该放在哪里,而不是单纯基于行长。 1219 | 1220 | ``` 1221 | // Good: 1222 | log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)", 1223 | currentCustomer, currentOffset, currentKey, 1224 | txCustomer, txOffset, txKey) 1225 | // Bad: 1226 | log.Warningf("Database key (%q, %d, %q) incompatible in"+ 1227 | " transaction started by (%q, %d, %q)", 1228 | currentCustomer, currentOffset, currentKey, txCustomer, 1229 | txOffset, txKey) 1230 | ``` 1231 | 1232 | ### 条件和循环 1233 | 1234 | `if` 语句不应换行; 多行 `if` 子句的形式会出现 [缩进混乱带来的困扰](https://gocn.github.io/styleguide/docs/03-decisions/#缩进的混乱)。 1235 | 1236 | ``` 1237 | // Bad: 1238 | // The second if statement is aligned with the code within the if block, causing 1239 | // indentation confusion. 1240 | if db.CurrentStatusIs(db.InTransaction) && 1241 | db.ValuesEqual(db.TransactionKey(), row.Key()) { 1242 | return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) 1243 | } 1244 | ``` 1245 | 1246 | 如果不需要短路(short-circuit)行为,可以直接提取布尔操作数: 1247 | 1248 | ``` 1249 | // Good: 1250 | inTransaction := db.CurrentStatusIs(db.InTransaction) 1251 | keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) 1252 | if inTransaction && keysMatch { 1253 | return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) 1254 | } 1255 | ``` 1256 | 1257 | 尤其注意,在条件已经重复的情况下,很可能还是有可以提取的局部变量: 1258 | 1259 | ``` 1260 | // Good: 1261 | uid := user.GetUniqueUserID() 1262 | if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) { 1263 | // ... 1264 | } 1265 | // Bad: 1266 | if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) { 1267 | // ... 1268 | } 1269 | ``` 1270 | 1271 | 包含闭包或多行结构文字的 `if` 语句应确保 [大括号匹配](https://gocn.github.io/styleguide/docs/03-decisions/#匹配的大括号) 以避免 [缩进混淆](https://gocn.github.io/styleguide/docs/03-decisions/#缩进的混乱)。 1272 | 1273 | ``` 1274 | // Good: 1275 | if err := db.RunInTransaction(func(tx *db.TX) error { 1276 | return tx.Execute(userUpdate, x, y, z) 1277 | }); err != nil { 1278 | return fmt.Errorf("user update failed: %s", err) 1279 | } 1280 | // Good: 1281 | if _, err := client.Update(ctx, &upb.UserUpdateRequest{ 1282 | ID: userID, 1283 | User: user, 1284 | }); err != nil { 1285 | return fmt.Errorf("user update failed: %s", err) 1286 | } 1287 | ``` 1288 | 1289 | 同样,不要尝试在 `for` 语句中人为的插入换行符。如果没有优雅的重构方式,是可以允许单纯的较长的行: 1290 | 1291 | ``` 1292 | // Good: 1293 | for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ { 1294 | // ... 1295 | } 1296 | ``` 1297 | 1298 | 但是,通常可以优化为: 1299 | 1300 | ``` 1301 | // Good: 1302 | for i, max := 0, collection.Size(); i < max; i++ { 1303 | if collection.HasPendingWriters() { 1304 | break 1305 | } 1306 | // ... 1307 | } 1308 | ``` 1309 | 1310 | `switch` 和 `case` 语句都应始终保持在一行: 1311 | 1312 | ``` 1313 | // Good: 1314 | switch good := db.TransactionStatus(); good { 1315 | case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: 1316 | // ... 1317 | case db.TransactionCommitted, db.NoTransaction: 1318 | // ... 1319 | default: 1320 | // ... 1321 | } 1322 | // Bad: 1323 | switch bad := db.TransactionStatus(); bad { 1324 | case db.TransactionStarting, 1325 | db.TransactionActive, 1326 | db.TransactionWaiting: 1327 | // ... 1328 | case db.TransactionCommitted, 1329 | db.NoTransaction: 1330 | // ... 1331 | default: 1332 | // ... 1333 | } 1334 | ``` 1335 | 1336 | 如果行太长,将所有大小写缩进并用空行分隔以避免[缩进混淆](https://gocn.github.io/styleguide/docs/03-decisions/#缩进的混乱): 1337 | 1338 | ``` 1339 | // Good: 1340 | switch db.TransactionStatus() { 1341 | case 1342 | db.TransactionStarting, 1343 | db.TransactionActive, 1344 | db.TransactionWaiting, 1345 | db.TransactionCommitted: 1346 | 1347 | // ... 1348 | case db.NoTransaction: 1349 | // ... 1350 | default: 1351 | // ... 1352 | } 1353 | ``` 1354 | 1355 | 在将变量比较的条件中,变量值放在等号运算符的左侧: 1356 | 1357 | ``` 1358 | // Good: 1359 | if result == "foo" { 1360 | // ... 1361 | } 1362 | ``` 1363 | 1364 | 不要采用常量在前的表达含糊的写法([尤达条件式](https://en.wikipedia.org/wiki/Yoda_conditions)) 1365 | 1366 | ``` 1367 | // Bad: 1368 | if "foo" == result { 1369 | // ... 1370 | } 1371 | ``` 1372 | 1373 | ### 复制 1374 | 1375 | 为了避免意外的别名和类似的错误,从另一个包复制结构时要小心。例如 `sync.Mutex` 是不能复制的同步对象。 1376 | 1377 | `bytes.Buffer` 类型包含一个 `[]byte` 切片和切片可以引用的小数组,这是为了对小字符串的优化。如果你复制一个 `Buffer`,复制的切片会指向原始切片中的数组,从而在后续方法调用产生意外的效果。 1378 | 1379 | 一般来说,如果类型的方法与指针类型`*T`相关联,不要复制类型为`T`的值。 1380 | 1381 | 1382 | ``` 1383 | // Bad: 1384 | b1 := bytes.Buffer{} 1385 | b2 := b1 1386 | ``` 1387 | 1388 | 调用值接收者的方法可以隐藏拷贝。当你编写 API 时,如果你的结构包含不应复制的字段,你通常应该采用并返回指针类型。 1389 | 1390 | 如此是可接受的: 1391 | 1392 | ``` 1393 | // Good: 1394 | type Record struct { 1395 | buf bytes.Buffer 1396 | // other fields omitted 1397 | } 1398 | 1399 | func New() *Record {...} 1400 | 1401 | func (r *Record) Process(...) {...} 1402 | 1403 | func Consumer(r *Record) {...} 1404 | ``` 1405 | 1406 | 但下面这种通常是错误的: 1407 | 1408 | ``` 1409 | // Bad: 1410 | type Record struct { 1411 | buf bytes.Buffer 1412 | // other fields omitted 1413 | } 1414 | 1415 | func (r Record) Process(...) {...} // Makes a copy of r.buf 1416 | 1417 | func Consumer(r Record) {...} // Makes a copy of r.buf 1418 | ``` 1419 | 1420 | 这一指南同样也适用于 `sync.Mutex` 复制的情况。 1421 | 1422 | ### 不要 panic 1423 | 1424 | 不要使用 `panic` 进行正常的错误处理。相反,使用 `error` 和多个返回值。请参阅 [关于错误的有效 Go 部分](http://golang.org/doc/effective_go.html#errors)。 1425 | 1426 | 在 `package main` 和初始化代码中,考虑 [`log.Exit`](https://pkg.go.dev/github.com/golang/glog#Exit) 中应该终止程序的错误(例如,无效配置 ),因为在许多这些情况下,堆栈跟踪对阅读者没有帮助。请注意 [`log.Exit`](https://pkg.go.dev/github.com/golang/glog#Exit) 中调用了 [`os.Exit`](https://pkg.go.dev/os#Exit) ,此时所有`defer`函数都将不会运行。 1427 | 1428 | 对于那些表示“不可能”出现的条件错误、命名错误,应该在代码评审、测试期间发现,函数应合理地返回错误或调用 [`log.Fatal`](https://pkg.go.dev/github.com/golang/glog#Fatal)。 1429 | 1430 | **注意:** `log.Fatalf` 不是标准库日志。请参阅 [#logging]。 1431 | 1432 | ### Must类函数 1433 | 1434 | 用于在失败时停止程序的辅助函数应遵循命名约定“MustXYZ”(或“mustXYZ”)。一般来说,它们应该只在程序启动的早期被调用,而不是在像用户输入时,此时更应该首选 `error` 处理。 1435 | 1436 | 这类方式,通常只在[包初始化时](https://golang.org/ref/spec#Package_initialization)进行包级变量初始化的函数常见(例如[template.Must](https://golang.org/pkg/text/template/#Must) 和 [regexp.MustCompile](https://golang.org/pkg/regexp/#MustCompile))。 1437 | 1438 | ``` 1439 | // Good: 1440 | func MustParse(version string) *Version { 1441 | v, err := Parse(version) 1442 | if err != nil { 1443 | log.Fatalf("MustParse(%q) = _, %v", version, err) 1444 | } 1445 | return v 1446 | } 1447 | 1448 | // Package level "constant". If we wanted to use `Parse`, we would have had to 1449 | // set the value in `init`. 1450 | var DefaultVersion = MustParse("1.2.3") 1451 | ``` 1452 | 1453 | 相同的约定也可用于仅停止当前测试的情况(使用 `t.Fatal`)。这样在创建测试时通常很方便的,例如在 [表驱动测试](https://gocn.github.io/styleguide/docs/03-decisions/#表驱动测试) 的结构字段中,作为返回错误的函数是不能直接复制给结构字段的。 1454 | 1455 | ``` 1456 | // Good: 1457 | func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any { 1458 | t.Helper() 1459 | any, err := anypb.New(m) 1460 | if err != nil { 1461 | t.Fatalf("MustMarshalAny(t, m) = %v; want %v", err, nil) 1462 | } 1463 | return any 1464 | } 1465 | 1466 | func TestCreateObject(t *testing.T) { 1467 | tests := []struct{ 1468 | desc string 1469 | data *anypb.Any 1470 | }{ 1471 | { 1472 | desc: "my test case", 1473 | // Creating values directly within table driven test cases. 1474 | data: mustMarshalAny(t, mypb.Object{}), 1475 | }, 1476 | // ... 1477 | } 1478 | // ... 1479 | } 1480 | ``` 1481 | 1482 | 在这两种情况下,这种模式的价值在于可以在“值”上下文中调用。不应在难以确保捕获错误的地方或应[检查](https://gocn.github.io/styleguide/docs/03-decisions/#错误处理)错误的上下文中调用这些程序(如,在许多请求处理程序中)。对于常量输入,这允许测试确保“必须”的参数格式正确,对于非常量的输入,它允许测试验证错误是否[正确处理或传播](https://gocn.github.io/styleguide/docs/04-best-practices/#错误处理)。 1483 | 1484 | 在测试中使用 `Must` 函数的地方,通常应该 [标记为测试辅助函数](https://gocn.github.io/styleguide/docs/03-decisions/#测试辅助函数) 并调用 `t.Fatal`(请参阅[测试辅助函数中的错误处理](https://gocn.github.io/styleguide/docs/04-best-practices/#在测试辅助函数中处理错误)来了解使用它的更多注意事项)。 1485 | 1486 | 当有可能通过 [普通错误处理](https://gocn.github.io/styleguide/docs/04-best-practices/#错误处理) 处理时,就不应该使用`Must`类函数: 1487 | 1488 | ``` 1489 | // Bad: 1490 | func Version(o *servicepb.Object) (*version.Version, error) { 1491 | // Return error instead of using Must functions. 1492 | v := version.MustParse(o.GetVersionString()) 1493 | return dealiasVersion(v) 1494 | } 1495 | ``` 1496 | 1497 | ### Goroutine 生命周期 1498 | 1499 | 当你生成 goroutines 时,要明确它们何时或是否退出。 1500 | 1501 | Goroutines 可以在阻塞通道发送或接收出现泄漏。垃圾收集器不会终止一个 goroutine,即使它被阻塞的通道已经不可用。 1502 | 1503 | 即使 goroutine 没有泄漏,在不再需要时仍处于运行状态也会导致其他微妙且难以诊断的问题。向已关闭的通道上发送会导致panic。 1504 | 1505 | 1506 | ``` 1507 | // Bad: 1508 | ch := make(chan int) 1509 | ch <- 42 1510 | close(ch) 1511 | ch <- 13 // panic 1512 | ``` 1513 | 1514 | “在结果已经不需要之后”修改仍在使用的入参可能会导致数据竞争。运行任意长时间的 goroutine 会导致不可预测的内存占用。 1515 | 1516 | 并发代码的编写应该让 goroutine 生命周期非常明显。通常,这意味着在与同步相关的代码限制的函数范围内,将逻辑分解为 [同步函数](https://gocn.github.io/styleguide/docs/03-decisions/#同步函数)。如果并发性仍然不明显,那么文档说明 goroutine 在何时、为何退出就很重要。 1517 | 1518 | 遵循上下文使用最佳实践的代码通常有助于明确这一点,其通常使用 `context.Context` 进行管理: 1519 | 1520 | ``` 1521 | // Good: 1522 | func (w *Worker) Run(ctx context.Context) error { 1523 | // ... 1524 | for item := range w.q { 1525 | // process 至少在ctx取消时会返回 1526 | go process(ctx, item) 1527 | } 1528 | // ... 1529 | } 1530 | ``` 1531 | 1532 | 上面还有其他使用通道的情况,例如 `chan struct{}`、同步变量、[条件变量](https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view) 等等。重要的部分是 goroutine 的 结束对于后续维护者来说是显而易见的。 1533 | 1534 | 相比之下,以下代码不关心其衍生的 goroutine 何时完成: 1535 | 1536 | ``` 1537 | // Bad: 1538 | func (w *Worker) Run() { 1539 | // ... 1540 | for item := range w.q { 1541 | // process returns when it finishes, if ever, possibly not cleanly 1542 | // handling a state transition or termination of the Go program itself. 1543 | go process(item) 1544 | } 1545 | // ... 1546 | } 1547 | ``` 1548 | 1549 | 这段代码看起来还行,但有几个潜在的问题: 1550 | 1551 | - 该代码在生产中可能有未定义的行为,即使操作系统已经释放资源,程序也可能没有完全干净地结束 1552 | - 由于代码的不确定生命周期,代码难以进行有效的测试 1553 | - 代码可能会出现资源泄漏,如上所述 1554 | 1555 | 更多可阅读: 1556 | 1557 | - [永远不要在不知道它将如何停止的情况下启动 goroutine](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop) 1558 | - 重新思考经典并发模式:[幻灯片](https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view),[视频](https://www.youtube.com/watch?v=5zXAHh5tJqQ) 1559 | - [Go 程序何时结束](https://changelog.com/gotime/165) 1560 | 1561 | ### 接口 1562 | 1563 | Go 接口通常属于*使用*接口类型值的包,而不是*实现*接口类型的包。实现包应该返回具体的(通常是指针或结构)类型。这样就可以将新方法添加到实现中,而无需进行大量重构。有关详细信息,请参阅 [GoTip #49:接受接口、返回具体类型](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。 1564 | 1565 | 不要从使用 API 导出接口的 [test double](https://abseil.io/resources/swe-book/html/ch13.html#techniques_for_using_test_doubles) 实现。相反,应设计可以使用 [实际实现](https://abseil.io/resources/swe-book/html/ch12.html#test_via_public_apis) 的[公共API]((https://abseil.io/resources/swe-book/html/ch12.html#test_via_public_apis))进行测试。有关详细信息,请参阅 [GoTip #42:为测试编写存根](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。即使在使用实现不可行的情况下,也没有必要引入一个完全覆盖类型所有方法的接口;消费者可以创建一个只包含它需要的方法的接口,如 [GoTip #78: Minimal Viable Interfaces](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 中所示。 1566 | 1567 | 要测试使用 Stubby RPC 客户端的包,请使用真实的客户端连接。如果无法在测试中运行真实服务器,Google 的内部做法是使用内部 rpctest 包(即将推出!)获得与本地 [test double] 的真实客户端连接。 1568 | 1569 | 在使用之前不要定义接口(参见 [TotT: Code Health: Eliminate YAGNI Smells](https://testing.googleblog.com/2017/08/code-health-eliminate-yagni-smells.html))。如果没有实际的使用示例,就很难判断一个接口是否必要,更不用说它应该包含哪些方法了。 1570 | 1571 | 如果不需要传递不同的类型,则不要使用接口类型作为参数。 1572 | 1573 | 不要导出不需要开放的接口。 1574 | 1575 | **TODO:** 写一个关于接口的更深入的文档并在这里链接到它。 1576 | 1577 | ``` 1578 | // Good: 1579 | package consumer // consumer.go 1580 | 1581 | type Thinger interface { Thing() bool } 1582 | 1583 | func Foo(t Thinger) string { ... } 1584 | // Good: 1585 | package consumer // consumer_test.go 1586 | 1587 | type fakeThinger struct{ ... } 1588 | func (t fakeThinger) Thing() bool { ... } 1589 | ... 1590 | if Foo(fakeThinger{...}) == "x" { ... } 1591 | // Bad: 1592 | package producer 1593 | 1594 | type Thinger interface { Thing() bool } 1595 | 1596 | type defaultThinger struct{ ... } 1597 | func (t defaultThinger) Thing() bool { ... } 1598 | 1599 | func NewThinger() Thinger { return defaultThinger{ ... } } 1600 | // Good: 1601 | package producer 1602 | 1603 | type Thinger struct{ ... } 1604 | func (t Thinger) Thing() bool { ... } 1605 | 1606 | func NewThinger() Thinger { return Thinger{ ... } } 1607 | ``` 1608 | 1609 | ### 泛型 1610 | 1611 | 在满足业务需求时,泛型(正式名称为“[类型参数](https://go.dev/design/43651-type-parameters)”)才应该被使用。在许多应用程序中,使用现有语言特性中传统方式(切片、映射、接口等)也可以正常工作,而不会增加复杂性,因此请注意不要过早使用。请参阅关于 [最小机制](https://gocn.github.io/styleguide/docs/02-guide/#最小化机制) 的讨论。 1612 | 1613 | 在引入使用泛型的导出 API 时,请确保对其进行适当的记录。强烈鼓励包含可运行的 [示例](https://gocn.github.io/styleguide/docs/03-decisions/#示例examples)。 1614 | 1615 | 不要仅仅因为你正在实现一个算法或不关心元素类型的数据结构而使用泛型。如果在实践中只有一种类型可以使用,那么首先让您的代码在该类型上工作,而不使用泛型。与其删除不必要的抽象,稍后为其添加多态将更简单。 1616 | 1617 | 不要使用泛型来发明领域特定语言 (DSL)。特别是,不要引入可能会给阅读者带来沉重负担的错误处理框架。相反,更应该使用 [错误处理](https://gocn.github.io/styleguide/docs/03-decisions/#错误) 做法。对于测试,要特别小心引入 [断言库](https://gocn.github.io/styleguide/docs/03-decisions/#断言库) 或框架,尤其是很少发现[失败case](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败)的。 1618 | 1619 | 一般来说: 1620 | 1621 | - [写代码,不要去设计类型](https://www.youtube.com/watch?v=Pa_e9EeCdy8&t=1250s)。来自 Robert Griesemer 和 Ian Lance Taylor 的 GopherCon 演讲。 1622 | - 如果你有几种类型共享一个统一接口,请考虑使用该接口对解决方案进行建模。这种情况可能不需要泛型。 1623 | - 否则,不要依赖 `any` 类型和过多的 [类型断言](https://tour.golang.org/methods/16)的情况,应考虑泛型。 1624 | 1625 | 更多也可以参考: 1626 | 1627 | - [在 Go 中使用泛型](https://www.youtube.com/watch?v=nr8EpUO9jhw),Ian Lance Taylor 的演讲 1628 | - Go 网页上的[泛型教程](https://go.dev/doc/tutorial/generics) 1629 | 1630 | ### 参数值传递 1631 | 1632 | 不要为了节省几个字节而将指针作为函数参数传递。如果一个函数在整个过程中只将参数`x`处理为`*x`,那么不应该采用指针。常见的例子包括传递一个指向字符串的指针(`*string`)或一个指向接口值的指针(`*io.Reader`)。在这两种情况下,值本身都是固定大小的,可以直接传递。 1633 | 1634 | 此建议不适用于大型结构体,甚至可能会增加大小的小型结构。特别是,`pb`消息通常应该通过指针而不是值来处理。指针类型满足 `proto.Message` 接口(被 `proto.Marshal`、`protocmp.Transform` 等接受),并且协议缓冲区消息可能非常大,并且随着时间的推移通常会变得更大。 1635 | 1636 | ### 接收者类型 1637 | 1638 | [方法接收者](https://golang.org/ref/spec#Method_declarations) 和常规函数参数一样,也可以使用值或指针传递。选择哪个应该基于该方法应该属于哪个[方法集](https://golang.org/ref/spec#Method_sets)。 1639 | 1640 | **正确性胜过速度或简单性。** 在某些情况下是必须使用指针的。在其他情况下,如果你对代码的增长方式没有很好的了解,请为大类型或考虑未来适用性上选择指针,并为[简单的的数据]((https://en.wikipedia.org/wiki/Passive_data_structure))使用值。 1641 | 1642 | 下面的列表更详细地说明了每个案例: 1643 | 1644 | - 如果接收者是一个切片并且该方法没有重新切片或重新分配切片,应使用值而不是指针。 1645 | 1646 | ``` 1647 | // Good: 1648 | type Buffer []byte 1649 | 1650 | func (b Buffer) Len() int { return len(b) } 1651 | ``` 1652 | 1653 | - 如果方法需要修改接收者,应使用指针。 1654 | 1655 | ``` 1656 | // Good: 1657 | type Counter int 1658 | 1659 | func (c *Counter) Inc() { *c++ } 1660 | 1661 | // See https://pkg.go.dev/container/heap. 1662 | type Queue []Item 1663 | 1664 | func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) } 1665 | ``` 1666 | 1667 | - 如果接收者包含 [不能被安全复制的](https://gocn.github.io/styleguide/docs/03-decisions/#复制) 字段, 应使用指针接收者。常见的例子是 [`sync.Mutex`](https://pkg.go.dev/sync#Mutex) 和其他同步类型。 1668 | 1669 | ``` 1670 | // Good: 1671 | type Counter struct { 1672 | mu sync.Mutex 1673 | total int 1674 | } 1675 | 1676 | func (c *Counter) Inc() { 1677 | c.mu.Lock() 1678 | defer c.mu.Unlock() 1679 | c.total++ 1680 | } 1681 | ``` 1682 | 1683 | **提示:** 检查类型是否可被安全复制的相关信息可参考 [Godoc](https://pkg.go.dev/time#example-Duration)。 1684 | 1685 | - 如果接收者是“大”结构或数组,则指针接收者可能更有效。传递结构相当于将其所有字段或元素作为参数传递给方法。如果这看起来太大而无法[按值传递](https://gocn.github.io/styleguide/docs/03-decisions/#参数值传递),那么指针是一个不错的选择。 1686 | 1687 | - 对于将调用修改接收者的其他函数,而这些修改对此方法不可见,请使用值类型; 否则使用指针。 1688 | 1689 | - 如果接收者是一个结构或数组,其元素中的任何一个都是指向可能发生变化的东西的指针,那么更应该指针接收者以使阅读者清楚地了解可变性的意图。 1690 | 1691 | ``` 1692 | // Good: 1693 | type Counter struct { 1694 | m *Metric 1695 | } 1696 | 1697 | func (c *Counter) Inc() { 1698 | c.m.Add(1) 1699 | } 1700 | ``` 1701 | 1702 | - 如果接收者是[内置类型](https://pkg.go.dev/builtin),例如整数或字符串,不需要修改,使用值。 1703 | 1704 | ``` 1705 | // Good: 1706 | type User string 1707 | 1708 | func (u User) String() { return string(u) } 1709 | ``` 1710 | 1711 | - 接收者是`map`, `function` 或 `channel`,使用值类型,而不是指针。 1712 | 1713 | ``` 1714 | // Good: 1715 | // See https://pkg.go.dev/net/http#Header. 1716 | type Header map[string][]string 1717 | 1718 | func (h Header) Add(key, value string) { /* omitted */ } 1719 | ``` 1720 | 1721 | - 如果接收器是一个“小”数组或结构,它自然是一个没有可变字段和指针,那么值接收者通常是正确的选择。 1722 | 1723 | ``` 1724 | // Good: 1725 | // See https://pkg.go.dev/time#Time. 1726 | type Time struct { /* omitted */ } 1727 | 1728 | func (t Time) Add(d Duration) Time { /* omitted */ } 1729 | ``` 1730 | 1731 | - 如有疑问,请使用指针接收者。 1732 | 1733 | 作为一般准则,最好将类型的方法设为全部指针方法或全部值方法。 1734 | 1735 | **注意:** 关于是否值或指针的函数是否会影响性能,存在很多错误信息。编译器可以选择将指针传递到堆栈上的值以及复制堆栈上的值,但在大多数情况下,这些考虑不应超过代码的可读性和正确性。当性能确实很重要时,重要的是在确定一种方法优于另一种方法之前,用一个实际的基准来描述这两种方法。 1736 | 1737 | ### `switch` 和 `break` 1738 | 1739 | 不要在`switch`子句末尾使用没有目标标签的`break`语句; 它们是多余的。与 C 和 Java 不同,Go 中的 `switch` 子句会自动中断,并且需要 `fallthrough` 语句来实现 C 风格的行为。如果你想阐明空子句的目的,请使用注释而不是 `break`。 1740 | 1741 | ``` 1742 | // Good: 1743 | switch x { 1744 | case "A", "B": 1745 | buf.WriteString(x) 1746 | case "C": 1747 | // handled outside of the switch statement 1748 | default: 1749 | return fmt.Errorf("unknown value: %q", x) 1750 | } 1751 | // Bad: 1752 | switch x { 1753 | case "A", "B": 1754 | buf.WriteString(x) 1755 | break // this break is redundant 1756 | case "C": 1757 | break // this break is redundant 1758 | default: 1759 | return fmt.Errorf("unknown value: %q", x) 1760 | } 1761 | ``` 1762 | 1763 | > **Note:** If a `switch` clause is within a `for` loop, using `break` within `switch` does not exit the enclosing `for` loop. 1764 | > 1765 | > ``` 1766 | > for { 1767 | > switch x { 1768 | > case "A": 1769 | > break // exits the switch, not the loop 1770 | > } 1771 | > } 1772 | > ``` 1773 | > 1774 | > To escape the enclosing loop, use a label on the `for` statement: 1775 | > 1776 | > ``` 1777 | > loop: 1778 | > for { 1779 | > switch x { 1780 | > case "A": 1781 | > break loop // exits the loop 1782 | > } 1783 | > } 1784 | > ``` 1785 | 1786 | ### 同步函数 1787 | 1788 | 同步函数直接返回它们的结果,并在返回之前完成所有回调或通道操作。首选同步函数而不是异步函数。 1789 | 1790 | 同步函数使 goroutine 在调用中保持本地化。这有助于推理它们的生命周期,并避免泄漏和数据竞争。同步函数也更容易测试,因为调用者可以传递输入并检查输出,而无需轮询或同步。 1791 | 1792 | 如有必要,调用者可以通过在单独的 goroutine 中调用函数来添加并发性。然而,在调用方移除不必要的并发是相当困难的(有时是不可能的)。 1793 | 1794 | 也可以看看: 1795 | 1796 | - “重新思考经典并发模式”,Bryan Mills 的演讲:[幻灯片](https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view),[视频](https://www.youtube.com/ 看?v=5zXAHh5tJqQ) 1797 | 1798 | ### 类型别名 1799 | 1800 | 使用*类型定义*,`type T1 T2`,定义一个新类型。 1801 | 使用 [*类型别名*](http://golang.org/ref/spec#Type_declarations), `type T1 = T2` 来引用现有类型而不定义新类型。 1802 | 类型别名很少见; 它们的主要用途是帮助将包迁移到新的源代码位置。不要在不需要时使用类型别名。 1803 | 1804 | ### 使用 %q 1805 | 1806 | Go 的格式函数(`fmt.Printf` 等)有一个 `%q` 动词,它在双引号内打印字符串。 1807 | 1808 | ``` 1809 | // Good: 1810 | fmt.Printf("value %q looks like English text", someText) 1811 | ``` 1812 | 1813 | 更应该使用 `%q` 而不是使用 `%s` 手动执行等效操作: 1814 | 1815 | ``` 1816 | // Bad: 1817 | fmt.Printf("value \"%s\" looks like English text", someText) 1818 | // Avoid manually wrapping strings with single-quotes too: 1819 | fmt.Printf("value '%s' looks like English text", someText) 1820 | ``` 1821 | 1822 | 建议在供人使用的输出中使用 `%q`,其输入值可能为空或包含控制字符。可能很难注意到一个无声的空字符串,但是 `""` 就这样清楚地突出了。 1823 | ### 使用 any 1824 | 1825 | Go 1.18 将 `any` 类型作为 [别名](https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md) 引入到 `interface{}`。因为它是一个别名,所以 `any` 在许多情况下等同于 `interface{}`,而在其他情况下,它可以通过显式转换轻松互换。在新代码中应使用 `any`。 1826 | ## 通用库 1827 | 1828 | ### Flags 1829 | 1830 | Google 代码库中的 Go 程序使用 [标准 `flag` 包](https://golang.org/pkg/flag/) 的内部变体。它具有类似的接口,但与 Google 内部系统的互操作性很好。Go 二进制文件中的标志名称应该更应该使用下划线来分隔单词,尽管包含标志值的变量应该遵循标准的 Go 名称样式([混合大写字母](https://gocn.github.io/styleguide/docs/02-guide#大小写混合))。具体来说,标志名称应该是蛇形命名,变量名称应该是驼峰命名。 1831 | 1832 | ``` 1833 | // Good: 1834 | var ( 1835 | pollInterval = flag.Duration("poll_interval", time.Minute, "Interval to use for polling.") 1836 | ) 1837 | // Bad: 1838 | var ( 1839 | poll_interval = flag.Int("pollIntervalSeconds", 60, "Interval to use for polling in seconds.") 1840 | ) 1841 | ``` 1842 | 1843 | Flags只能在 `package main` 或等效项中定义。 1844 | 1845 | 通用包应该使用 Go API 进行配置,而不是通过命令行界面进行配置;不要让导入库导出新标志作为副作用。也就是说,更倾向于显式的函数参数或结构字段分配,或者低频和严格审查的全局变量导出。在需要打破此规则的极少数情况下,标志名称必须清楚地表明它配置的包。 1846 | 1847 | 如果你的标志是全局变量,在导入部分之后,将它们放在 `var` 组中。 1848 | 1849 | 关于使用子命令创建 [complex CLI](https://gocn.github.io/styleguide/docs/04-best-practices/#复杂的命令行界面) (https://gocn.github.io/styleguide/docs/04-best-practices/#自定义日志级别) (https://gocn.github.io/styleguide/docs/04-best-practices/#自定义日志级别) 的最佳实践还有其他讨论。 1850 | 1851 | 也可以看看: 1852 | 1853 | - [本周提示 #45:避免标记,尤其是在库代码中](https://abseil.io/tips/45) 1854 | - [Go Tip #10:配置结构和标志](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 1855 | - [Go Tip #80:依赖注入原则](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 1856 | 1857 | ### 日志 1858 | 1859 | Google 代码库中的 Go 程序使用 [标准 `log` 包](https://pkg.go.dev/log) 的变体。它具有类似但功能更强大的interface,并且可以与 Google 内部系统进行良好的互操作。该库的开源版本可通过 [package `glog`](https://pkg.go.dev/github.com/golang/glog) 获得,开源 Google 项目可能会使用它,但本指南指的是它始终作为“日志”。 1860 | 1861 | **注意:** 对于异常的程序退出,这个库使用 `log.Fatal` 通过堆栈跟踪中止,使用 `log.Exit` 在没有堆栈跟踪的情况下停止。标准库中没有 `log.Panic` 函数。 1862 | 1863 | **提示:** `log.Info(v)` 等价于 `log.Infof("%v", v)`,其他日志级别也是如此。当你没有格式化要做时,首选非格式化版本。 1864 | 1865 | 也可以看看: 1866 | 1867 | - [记录错误](https://gocn.github.io/styleguide/docs/04-best-practices/#错误日志) 和 [自定义详细日志级别](https://gocn.github.io/styleguide/docs/04-best-practices/#自定义日志级别) 1868 | - 何时以及如何使用日志包[停止程序](https://gocn.github.io/styleguide/docs/04-best-practices/#程序检查和-panic) 1869 | 1870 | ### 上下文 1871 | 1872 | [`context.Context`](https://pkg.go.dev/context) 类型的值携带跨 API 和进程边界的安全凭证、跟踪信息、截止日期和取消信号。与 Google 代码库中使用线程本地存储的 C++ 和 Java 不同,Go 程序在整个函数调用链中显式地传递上下文,从传入的 RPC 和 HTTP 请求到传出请求。 1873 | 1874 | 当传递给函数或方法时,`context.Context` 始终是第一个参数。 1875 | 1876 | ``` 1877 | func F(ctx context.Context /* other arguments */) {} 1878 | ``` 1879 | 1880 | 例外情况是: 1881 | 1882 | - 在 HTTP 处理程序中,上下文来自 [`req.Context()`](https://pkg.go.dev/net/http#Request.Context)。 1883 | 1884 | - 在流式 RPC 方法中,上下文来自流。 1885 | 1886 | 使用 gRPC 流的代码从生成的服务器类型中的 `Context()` 方法访问上下文,该方法实现了 `grpc.ServerStream`。请参阅 https://grpc.io/docs/languages/go/generated-code/。 1887 | 1888 | - 在入口函数(此类函数的示例见下文)中,使用 [`context.Background()`](https://pkg.go.dev/context/#Background)。 1889 | 1890 | - 在二进制目标中:`main` 1891 | - 在通用代码和库中:`init` 1892 | - 在测试中:`TestXXX`、`BenchmarkXXX`、`FuzzXXX` 1893 | 1894 | > **注意**:调用链中间的代码很少需要使用 `context.Background()` 创建自己的基本上下文。更应该从调用者那里获取上下文,除非它是错误的上下文。 1895 | > 1896 | > 你可能会遇到服务库(在 Google 的 Go 服务框架中实现 Stubby、gRPC 或 HTTP),它们为每个请求构建一个新的上下文对象。这些上下文立即填充来自传入请求的信息,因此当传递给请求处理程序时,上下文的附加值已从客户端调用者通过网络边界传播给它。此外,这些上下文的生命周期仅限于请求的生命周期:当请求完成时,上下文将被取消。 1897 | > 1898 | > 除非你正在实现一个服务器框架,否则你不应该在库代码中使用 `context.Background()` 创建上下文。相反,如果有可用的现有上下文,则更应该使用下面提到的上下文分离。如果你认为在入口点函数之外确实需要`context.Background()`,请在提交实现之前与 Google Go 风格的邮件列表讨论它。 1899 | 1900 | `context.Context` 在函数中首先出现的约定也适用于测试辅助函数。 1901 | 1902 | ``` 1903 | // Good: 1904 | func readTestFile(ctx context.Context, t *testing.T, path string) string {} 1905 | ``` 1906 | 1907 | 不要将上下文成员添加到结构类型。相反,为需要传递它的类型的每个方法添加一个上下文参数。一个例外是其签名必须与标准库或 Google 无法控制的第三方库中的接口匹配的方法。这种情况非常罕见,应该在实施和可读性审查之前与 Google Go 风格的邮件列表讨论。 1908 | 1909 | Google 代码库中必须产生可以在取消父上下文后运行的后台操作的代码可以使用内部包进行分离。关注 https://github.com/golang/go/issues/40221 讨论开源替代方案。 1910 | 1911 | 由于上下文是不可变的,因此可以将相同的上下文传递给共享相同截止日期、取消信号、凭据、父跟踪等的多个调用。 1912 | 1913 | 更多参见: 1914 | 1915 | - [上下文和结构](https://go.dev/blog/context-and-structs) 1916 | 1917 | #### 自定义上下文 1918 | 1919 | 不要在函数签名中创建自定义上下文类型或使用上下文以外的接口。这条规定没有例外。 1920 | 1921 | 想象一下,如果每个团队都有一个自定义上下文。对于包 P 和 Q 的所有对,从包 P 到包 Q 的每个函数调用都必须确定如何将“PContext”转换为“QContext”。这对开发者来说是不切实际且容易出错的,并且它会进行自动重构 添加上下文参数几乎是不可能的。 1922 | 1923 | 如果你要传递应用程序数据,请将其放入参数、接收器、全局变量中,或者如果它确实属于那里,则放入 Context 值中。创建自己的 Context 类型是不可接受的,因为它破坏了 Go 团队使 Go 程序在生产中正常工作的能力。 1924 | 1925 | ### crypto/rand 1926 | 1927 | 不要使用包 `math/rand` 来生成密钥,即使是一次性的。如果未生成随机种子,则生成器是完全可预测的。用`time.Nanoseconds()`生成种子,也只有几位熵。相反,请使用 `crypto/rand` ,如果需要文本,请打印为十六进制或 base64。 1928 | 1929 | ``` 1930 | // Good: 1931 | import ( 1932 | "crypto/rand" 1933 | // "encoding/base64" 1934 | // "encoding/hex" 1935 | "fmt" 1936 | 1937 | // ... 1938 | ) 1939 | 1940 | func Key() string { 1941 | buf := make([]byte, 16) 1942 | if _, err := rand.Read(buf); err != nil { 1943 | log.Fatalf("Out of randomness, should never happen: %v", err) 1944 | } 1945 | return fmt.Sprintf("%x", buf) 1946 | // or hex.EncodeToString(buf) 1947 | // or base64.StdEncoding.EncodeToString(buf) 1948 | } 1949 | ``` 1950 | 1951 | ## 有用的测试失败 1952 | 1953 | 应该可以在不读取测试代码的情况下诊断测试失败。测试失败应当显示详细有用的消息说明: 1954 | 1955 | - 是什么导致了失败 1956 | - 哪些输入导致错误 1957 | - 实际结果 1958 | - 预期的结果 1959 | 1960 | 下面概述了实现这一目标的具体约定。 1961 | 1962 | ### 断言库 1963 | 1964 | 不要创建“断言库”作为测试辅助函数。 1965 | 1966 | 断言库是试图在测试中结合验证和生成失败消息的库(尽管同样的陷阱也可能适用于其他测试辅助函数)。有关测试辅助函数和断言库之间区别的更多信息,请参阅 [最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数)。 1967 | 1968 | ``` 1969 | // Bad: 1970 | var obj BlogPost 1971 | 1972 | assert.IsNotNil(t, "obj", obj) 1973 | assert.StringEq(t, "obj.Type", obj.Type, "blogPost") 1974 | assert.IntEq(t, "obj.Comments", obj.Comments, 2) 1975 | assert.StringNotEq(t, "obj.Body", obj.Body, "") 1976 | ``` 1977 | 1978 | 断言库倾向于提前停止测试(如果 `assert` 调用 `t.Fatalf` 或 `panic`)或省略有关测试正确的相关信息: 1979 | 1980 | ``` 1981 | // Bad: 1982 | package assert 1983 | 1984 | func IsNotNil(t *testing.T, name string, val interface{}) { 1985 | if val == nil { 1986 | t.Fatalf("data %s = nil, want not nil", name) 1987 | } 1988 | } 1989 | 1990 | func StringEq(t *testing.T, name, got, want string) { 1991 | if got != want { 1992 | t.Fatalf("data %s = %q, want %q", name, got, want) 1993 | } 1994 | } 1995 | ``` 1996 | 1997 | 复杂的断言函数通常不提供 [有用的失败消息](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败) 和存在于测试函数中的上下文。太多的断言函数和库会导致开发人员体验支离破碎:我应该使用哪个断言库,它应该发出什么样的输出格式等问题? 碎片化会产生不必要的混乱,特别是对于负责修复潜在下游破坏的库维护者和大规模更改的作者。与其创建用于测试的特定领域语言,不如使用 Go 本身。 1998 | 1999 | 断言库通常会排除比较和相等检查。更应该使用标准库,例如 [`cmp`](https://pkg.go.dev/github.com/google/go-cmp/cmp) 和 [`fmt`](https://golang.org/pkg/fmt/) 修改为: 2000 | 2001 | ``` 2002 | // Good: 2003 | var got BlogPost 2004 | 2005 | want := BlogPost{ 2006 | Comments: 2, 2007 | Body: "Hello, world!", 2008 | } 2009 | 2010 | if !cmp.Equal(got, want) { 2011 | t.Errorf("blog post = %v, want = %v", got, want) 2012 | } 2013 | ``` 2014 | 2015 | 对于更多特定于域的比较助手,更应该返回一个可以在测试失败消息中使用的值或错误,而不是传递 `*testing.T` 并调用其错误报告方法: 2016 | 2017 | ``` 2018 | // Good: 2019 | func postLength(p BlogPost) int { return len(p.Body) } 2020 | 2021 | func TestBlogPost_VeritableRant(t *testing.T) { 2022 | post := BlogPost{Body: "I am Gunnery Sergeant Hartman, your senior drill instructor."} 2023 | 2024 | if got, want := postLength(post), 60; got != want { 2025 | t.Errorf("length of post = %v, want %v", got, want) 2026 | } 2027 | } 2028 | ``` 2029 | 2030 | **最佳实践:** 如果 `postLength` 很重要,直接测试它是有意义的,独立于调用它的其他函数测试。 2031 | 2032 | 也可以看看: 2033 | 2034 | - [等值比较和差异](https://gocn.github.io/styleguide/docs/03-decisions/#等值比较和差异) 2035 | - [打印差异](https://gocn.github.io/styleguide/docs/03-decisions/#打印差异) 2036 | - 有关测试辅助函数和断言助手之间区别的更多信息,请参阅[最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数) 2037 | 2038 | ### 标识出函数 2039 | 2040 | 在大多数测试中,失败消息应该包括失败的函数的名称,即使从测试函数的名称中看起来很明显。具体来说,你的失败信息应该是 `YourFunc(%v) = %v, want %v` 而不仅仅是 `got %v, want %v`。 2041 | 2042 | ### 标识出输入 2043 | 2044 | 在大多数测试中,失败消息应该包括功能输入(如果它们很短)。如果输入的相关属性不明显(例如,因为输入很大或不透明),你应该使用对正在测试的内容的描述来命名测试用例,并将描述作为错误消息的一部分打印出来。 2045 | 2046 | ### Got before want 2047 | 2048 | 测试输出应包括函数在打印预期值之前返回的实际值。打印测试输出的标准格式是 `YourFunc(%v) = %v, want %v`。在你会写“实际”和“预期”的地方,更应该分别使用“get”和“want”这两个词。 2049 | 2050 | 对于差异,方向性不太明显,因此包含一个有助于解释失败的关键是很重要的。请参阅 [关于打印差异的部分](https://gocn.github.io/styleguide/docs/03-decisions/#打印差异)。无论你在失败消息中使用哪种 diff 顺序,都应将其明确指示为失败消息的一部分,因为现有代码的顺序不一致。 2051 | 2052 | ### 全结构比较 2053 | 2054 | 如果你的函数返回一个结构体(或任何具有多个字段的数据类型,例如切片、数组和映射),请避免编写执行手动编码的结构体逐个字段比较的测试代码。相反,构建期望函数返回的数据,并使用 [深度比较](https://gocn.github.io/styleguide/docs/03-decisions/#等值比较和差异) 直接进行比较。 2055 | 2056 | **注意:** 如果你的数据包含模糊测试意图的不相关字段,则这不适用。 2057 | 2058 | 如果你的结构比较时需要近似相等(或等效类型的语义),或者它包含无法比较相等的字段(例如,如果其中一个字段是 `io.Reader`),请调整 [`cmp. Diff`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmp#Diff) 或 [`cmp.Equal`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmp#Equal) 与 [`cmpopts`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts) 选项比较,例如[`cmpopts.IgnoreInterfaces`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#IgnoreInterfaces) 可能满足你的需求([示例](https://play.golang.org/p/vrCUNVfxsvF))。 2059 | 2060 | 如果你的函数返回多个返回值,则无需在比较它们之前将它们包装在结构中。只需单独比较返回值并打印它们。 2061 | 2062 | ``` 2063 | // Good: 2064 | val, multi, tail, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"') 2065 | if err != nil { 2066 | t.Fatalf(...) 2067 | } 2068 | if val != `"` { 2069 | t.Errorf(...) 2070 | } 2071 | if multi { 2072 | t.Errorf(...) 2073 | } 2074 | if tail != `Fran & Freddie's Diner"` { 2075 | t.Errorf(...) 2076 | } 2077 | ``` 2078 | 2079 | ### 比较稳定的结果 2080 | 2081 | 避免比较那些可能依赖于非自有包输出稳定性的结果。相反,测试应该在语义相关的信息上进行比较,这些信息是稳定的,并能抵抗依赖关系的变化。对于返回格式化字符串或序列化字节的功能,一般来说,假设输出是稳定的是不安全的。 2082 | 2083 | 例如,[`json.Marshal`](https://golang.org/pkg/encoding/json/#Marshal)可以改变(并且在过去已经改变)它所输出的特定字节。如果`json`包改变了它序列化字节的方式,在JSON字符串上执行字符串相等的测试可能会中断。相反,一个更强大的测试将解析JSON字符串的内容,并确保它在语义上等同于一些预期的数据结构。 2084 | 2085 | ### 测试继续进行 2086 | 2087 | 测试应该尽可能地持续下去,即使是在失败之后,以便在一次运行中打印出所有的失败检查。这样一来,正在修复失败测试的开发人员就不必在修复每个错误后重新运行测试来寻找下一个错误。 2088 | 2089 | 更倾向于调用`t.Error`而不是`t.Fatal`来报告不匹配。当比较一个函数输出的几个不同属性时,对每一个比较都使用`t.Error`。 2090 | 2091 | 调用`t.Fatal`主要用于报告一个意外的错误情况,当后续的比较失败是没有意义的。 2092 | 2093 | 对于表驱动的测试,考虑使用子测试,使用`t.Fatal`而不是`t.Error`和`continue`。参见[GoTip #25: Subtests: Making Your Tests Lean](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。 2094 | 2095 | **最佳实践:**关于何时应使用`t.Fatal`的更多讨论,见[最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#terror-vs-tfatal)。 2096 | 2097 | ### 等值比较和差异 2098 | 2099 | `==`操作符使用[语言定义的比较](http://golang.org/ref/spec#Comparison_operators)来评估相等性。标量值(数字、布尔运算等)根据其值进行比较, 但只有一些结构和接口可以用这种方式进行比较。指针的比较是基于它们是否指向同一个变量,而不是基于它们所指向的值是否相等。 2100 | 2101 | 对于类似切片这种情况下,`==`是不能正确处理比较的,[`cmp`](https://pkg.go.dev/github.com/google/go-cmp/cmp)包则可以用于比较更复杂的数据结构。使用[`cmp.Equal`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmp#Equal)进行等价比较,使用[`cmp.Diff`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmp#Diff)获得对象之间可供人类阅读的差异。 2102 | 2103 | ``` 2104 | // Good: 2105 | want := &Doc{ 2106 | Type: "blogPost", 2107 | Comments: 2, 2108 | Body: "This is the post body.", 2109 | Authors: []string{"isaac", "albert", "emmy"}, 2110 | } 2111 | if !cmp.Equal(got, want) { 2112 | t.Errorf("AddPost() = %+v, want %+v", got, want) 2113 | } 2114 | ``` 2115 | 2116 | 作为一个通用的比较库,`cmp`可能不知道如何比较某些类型。例如,它只能在传递[`protocmp.Transform`](https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp#Transform)选项时比较`protobuf`的信息。 2117 | 2118 | ``` 2119 | // Good: 2120 | if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { 2121 | t.Errorf("Foo() returned unexpected difference in protobuf messages (-want +got):\n%s", diff) 2122 | } 2123 | ``` 2124 | 2125 | 虽然`cmp`包不是Go标准库的一部分,但它是由Go团队维护的,随着时间的推移应该会产生稳定的相等结果。它是用户可配置的,应该可以满足大多数的比较需求。 2126 | 2127 | 现有的代码可能会使用以下旧的库,为了保持一致性,可以继续使用它们。 2128 | 2129 | - [`pretty`](https://pkg.go.dev/github.com/kylelemons/godebug/pretty)产生美观的差异报告。然而,它非常谨慎地认为具有相同视觉表现的数值是相等的。特别注意,`pretty`不区分nil切片和空切片之间的差异,对具有相同字段的不同接口实现也不敏感,而且有可能使用嵌套图作为与结构值比较的基础。在产生差异之前,它还会将整个值序列化为一个字符串,因此对于比较大的值来说不是一个好的选择。默认情况下,它比较的是未导出的字段,这使得它对你的依赖关系中实现细节的变化很敏感。由于这个原因,在protobuf信息上使用`pretty`是不合适的。 2130 | 2131 | 在新的代码中更倾向于使用`cmp`,值得考虑更新旧的代码,在实际可行的情况下使用`cmp`。 2132 | 2133 | 旧的代码可以使用标准库中的`reflect.DeepEqual`函数来比较复杂的结构。`reflect.DeepEqual`不应该被用来检查等值比较,因为它对未导出的字段和其他实现细节的变化很敏感。使用`reflect.DeepEqual`的代码应该更新为上述库之一。 2134 | 2135 | **注意:** `cmp`包是为测试而设计的,而不是用于生产。因此,当它怀疑一个比较被错误地执行时,它可能会 panic ,以向用户提供如何改进测试的指导,使其不那么脆弱。鉴于cmp的恐慌倾向,它不适合在生产中使用的代码,因为虚假的panic可能是致命的。 2136 | 2137 | ### 详细程度 2138 | 2139 | 传统的失败信息,适用于大多数Go测试,是`YourFunc(%v) = %v, want %v`。然而,有些情况可能需要更多或更少的细节。 2140 | 2141 | - 进行复杂交互的测试也应该描述交互。例如,如果同一个`YourFunc`被调用了好几次,那么要确定哪个调用未通过测试。如果知道系统的任何额外状态是很重要的,那么在失败输出中应包括这些(或者至少在日志中)。 2142 | - 如果数据是一个复杂的结构,在消息中只描述重要的部分是可以接受的,但不要过分掩盖数据。 2143 | - 设置失败不需要同样水平的细节。如果一个测试辅助函数填充了一个Spanner表,但Spanner却坏了,你可能不需要包括你要存储在数据库中的测试输入。`t.Fatalf("Setup: Failed to set up test database: %s", err)`通常足以解决这个问题。 2144 | 2145 | **提示:**应该在开发过程中触发失败。审查失败信息是什么样子的,维护者是否能有效地处理失败。 2146 | 2147 | 有一些技术可以清晰地再现测试输入和输出: 2148 | 2149 | - 当打印字符串数据时,[`%q`通常是有用的](https://gocn.github.io/styleguide/docs/03-decisions/#使用-q)以强调该值的重要性,并更容易发现坏值。 2150 | - 当打印(小)结构时,`%+v`可能比`%v`更有用。 2151 | - 当验证较大的值失败时,[打印差异](https://gocn.github.io/styleguide/docs/03-decisions/#打印差异)可以使人们更容易理解失败的原因。 2152 | 2153 | ### 打印差异 2154 | 2155 | 如果你的函数返回较大的输出,那么当你的测试失败时,阅读失败信息的人很难发现其中的差异。与其同时打印返回值和想要的值,不如做一个差异。 2156 | 2157 | 为了计算这些值的差异,`cmp.Diff`是首选,特别是对于新的测试和新的代码,但也可以使用其他工具。关于每个函数的优点和缺点的指导,见[类型的等值](https://gocn.github.io/styleguide/docs/03-decisions/#等值比较和差异)。 2158 | 2159 | - [`cmp.Diff`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmp#Diff) 2160 | - [`pretty.Compare`](https://pkg.go.dev/github.com/kylelemons/godebug/pretty#Compare) 2161 | 2162 | 你可以使用[`diff`](https://pkg.go.dev/github.com/kylelemons/godebug/diff)包来比较多行字符串或字符串的列表。你可以把它作为其他类型的比较的构建块。 2163 | 2164 | 在失败信息中添加一些文字,解释差异的方向。 2165 | 2166 | - 当你使用`cmp`,`pretty`和`diff`包时,类似`diff (-want +got)`的东西很好(如果把`(want, got)`传递给函数),因为你添加到格式字符串中的`-`和`+`将与实际出现在diff行开头的`-`和`+`匹配。如果把`(got, want)`传给函数,正确的键将是`(-got +want)`。 2167 | - `messagediff`包使用不同的输出格式,所以当你使用它时,`diff (want -> got)`的信息是合适的(如果把`(want, got)`传给函数),因为箭头的方向将与 "修改 "行中箭头的方向一致。 2168 | 2169 | 差异将跨越多行,所以应该在打印差异之前打印一个新行。 2170 | 2171 | ### 测试错误语义 2172 | 2173 | 当一个单元测试执行字符串比较或使用 `cmp` 来检查特定的输入是否返回特定种类的错误时,你可能会发现,如果这些错误信息在将来被重新修改,你的测试就会很脆弱。因为这有可能将你的单元测试变成一个变化检测器(参见[TotT: Change-Detector Tests Considered Harmful](https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html)),不要使用字符串比较来检查你的函数返回什么类型的错误。然而,允许使用字符串比较来检查来自被测包的错误信息是否满足某些属性,例如,它是否包括参数名称。 2174 | 2175 | Go中的错误值通常有一个用于人眼的部分和一个用于语义控制流的部分。测试应该力求只测试可以可靠观察到的语义信息,而不是显示用于人类调试的信息,因为这往往会在未来发生变化。关于构建具有语义的错误的指导,请参见[关于错误的最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#错误处理)。如果语义信息不充分的错误来自于你无法控制的依赖关系,请考虑针对所有者提交一个错误,以帮助改进API,而不是依靠解析错误信息。 2176 | 2177 | 在单元测试中,通常只关心错误是否发生。如果是这样,那么在你预期发生错误时,只测试错误是否为非零就足够了。如果你想测试错误在语义上与其他错误相匹配,那么可以考虑使用`cmp`与[`cmpopts.EquateErrors`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateErrors)。 2178 | 2179 | > **注意:**如果一个测试使用了[`cmpopts.EquateErrors`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateErrors),但是它所有的`wantErr`值都是`nil`或者`cmpopts.AnyError`,那么使用`cmp`是[不必要的](https://gocn.github.io/styleguide/docs/02-guide/#最小化机制)。简化代码,使want字段改为`bool`类型,然后就可以用`!=`进行简单的比较。 2180 | > 2181 | > ``` 2182 | > // Good: 2183 | > gotErr := f(test.input) != nil 2184 | > if gotErr != test.wantErr { 2185 | > t.Errorf("f(%q) returned err = %v, want error presence = %v", test.input, gotErr, test.wantErr) 2186 | > } 2187 | > ``` 2188 | 2189 | 另请参阅 [GoTip #13:设计用于检查的错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。 2190 | 2191 | ## 测试结构 2192 | 2193 | ### 子测试 2194 | 2195 | 标准的 Go 测试库提供了一种工具来 [定义子测试](https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks)。这允许在设置和清理、控制并行性和测试过滤方面具有灵活性。子测试可能很有用(特别是对于表驱动测试),但使用它们不是强制性的。另请参阅 https://blog.golang.org/subtests。 2196 | 2197 | 子测试不应该依赖于其他case的执行来获得成功或初始状态,因为子测试应该能够使用 `go test -run` 标志或使用 Bazel [测试过滤器](https://bazel.build/docs/user-manual#test-filter) 表达式。 2198 | 2199 | #### 子测试名称 2200 | 2201 | 命名子测试,使其在测试输出中可读,并且在命令行上对测试过滤的用户有用。当你使用“t.Run”创建子测试时,第一个参数用作测试的描述性名称。为了确保测试结果对于阅读日志的人来说是清晰的,请选择在转义后仍然有用且可读的子测试名称。将子测试名称视为函数标识符而不是散文描述。测试运行程序用下划线替换空格,并转义非打印字符。如果你的测试数据受益于更长的描述,请考虑将描述放在单独的字段中(可能使用“t.Log”或与失败消息一起打印)。 2202 | 2203 | 可以使用 [Go 测试运行器](https://golang.org/cmd/go/#hdr-Testing_flags) 或 Bazel [测试过滤器](https://bazel.build/docs/user-manual#test-filter) 的标志单独运行子测试,选择易于输入的描述性名称。 2204 | 2205 | > **警告:**斜杠字符在子测试名称中特别不友好,因为它们具有[测试过滤器的特殊含义](https://blog.golang.org/subtests#:~:text=Perhaps 一位匹配任何测试)。 2206 | > 2207 | > > ``` 2208 | > > # Bad: 2209 | > > # Assuming TestTime and t.Run("America/New_York", ...) 2210 | > > bazel test :mytest --test_filter="Time/New_York" # Runs nothing! 2211 | > > bazel test :mytest --test_filter="Time//New_York" # Correct, but awkward. 2212 | > > ``` 2213 | 2214 | 要[识别函数的输入](https://gocn.github.io/styleguide/docs/03-decisions/#标识出输入),将它们包含在测试的失败消息中,它们不会被测试执行者所忽略。 2215 | 2216 | ``` 2217 | // Good: 2218 | func TestTranslate(t *testing.T) { 2219 | data := []struct { 2220 | name, desc, srcLang, dstLang, srcText, wantDstText string 2221 | }{ 2222 | { 2223 | name: "hu=en_bug-1234", 2224 | desc: "regression test following bug 1234. contact: cleese", 2225 | srcLang: "hu", 2226 | srcText: "cigarettát és egy öngyújtót kérek", 2227 | dstLang: "en", 2228 | wantDstText: "cigarettes and a lighter please", 2229 | }, // ... 2230 | } 2231 | for _, d := range data { 2232 | t.Run(d.name, func(t *testing.T) { 2233 | got := Translate(d.srcLang, d.dstLang, d.srcText) 2234 | if got != d.wantDstText { 2235 | t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q", 2236 | d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText) 2237 | } 2238 | }) 2239 | } 2240 | } 2241 | ``` 2242 | 2243 | 以下是一些要避免的事情的例子: 2244 | 2245 | ``` 2246 | // Bad: 2247 | // Too wordy. 2248 | t.Run("check that there is no mention of scratched records or hovercrafts", ...) 2249 | // Slashes cause problems on the command line. 2250 | t.Run("AM/PM confusion", ...) 2251 | ``` 2252 | 2253 | ### 表驱动测试 2254 | 2255 | 当许多不同的测试用例可以使用相似的测试逻辑进行测试时,使用表驱动测试。 2256 | 2257 | - 测试函数的实际输出是否等于预期输出时。例如,许多 [`fmt.Sprintf` 的测试](https://cs.opensource.google/go/go/+/master:src/fmt/fmt_test.go) 或下面的最小片段。 2258 | - 测试函数的输出是否始终符合同一组不变量时。例如,[测试 `net.Dial`](https://cs.opensource.google/go/go/+/master:src/net/dial_test.go;l=318;drc=5b606a9d2b7649532fe25794fa6b99bd24e7697c)。 2259 | 2260 | 这是从标准“字符串”库复制的表驱动测试的最小结构。如果需要,你可以使用不同的名称,将测试切片移动到测试函数中,或者添加额外的工具,例如子测试或设置和清理函数。始终牢记[有用的测试失败](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败)。 2261 | 2262 | ``` 2263 | // Good: 2264 | var compareTests = []struct { 2265 | a, b string 2266 | i int 2267 | }{ 2268 | {"", "", 0}, 2269 | {"a", "", 1}, 2270 | {"", "a", -1}, 2271 | {"abc", "abc", 0}, 2272 | {"ab", "abc", -1}, 2273 | {"abc", "ab", 1}, 2274 | {"x", "ab", 1}, 2275 | {"ab", "x", -1}, 2276 | {"x", "a", 1}, 2277 | {"b", "x", -1}, 2278 | // test runtime·memeq's chunked implementation 2279 | {"abcdefgh", "abcdefgh", 0}, 2280 | {"abcdefghi", "abcdefghi", 0}, 2281 | {"abcdefghi", "abcdefghj", -1}, 2282 | } 2283 | 2284 | func TestCompare(t *testing.T) { 2285 | for _, tt := range compareTests { 2286 | cmp := Compare(tt.a, tt.b) 2287 | if cmp != tt.i { 2288 | t.Errorf(`Compare(%q, %q) = %v`, tt.a, tt.b, cmp) 2289 | } 2290 | } 2291 | } 2292 | ``` 2293 | 2294 | **注意**:上面这个例子中的失败消息满足了[识别函数](https://gocn.github.io/styleguide/docs/03-decisions/#标识出函数)和[识别输入](https://gocn.github.io/styleguide/docs/03-decisions/#标识出输入)。无需[用数字标识行](https://gocn.github.io/styleguide/docs/03-decisions/#标识行)。 2295 | 2296 | 当需要使用与其他测试用例不同的逻辑来检查某些测试用例时,编写多个测试函数更为合适,如 [GoTip #50: Disjoint Table Tests](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。当表中的每个条目都有自己不同的条件逻辑来检查每个输出的输入时,测试代码的逻辑可能会变得难以理解。如果测试用例具有不同的逻辑但设置相同,则单个测试函数中的一系列[子测试](https://gocn.github.io/styleguide/docs/03-decisions/#子测试) 可能有意义。 2297 | 2298 | 你可以将表驱动测试与多个测试函数结合起来。例如,当测试函数的输出与预期输出完全匹配并且函数为无效输入返回非零错误时,编写两个单独的表驱动测试函数是最好的方法:一个用于正常的非错误输出,一个用于错误输出。 2299 | 2300 | #### 数据驱动的测试用例 2301 | 2302 | 表测试行有时会变得复杂,行值指示测试用例内的条件行为。测试用例之间重复的额外清晰度对于可读性是必要的。 2303 | ``` 2304 | // Good: 2305 | type decodeCase struct { 2306 | name string 2307 | input string 2308 | output string 2309 | err error 2310 | } 2311 | 2312 | func TestDecode(t *testing.T) { 2313 | // setupCodex is slow as it creates a real Codex for the test. 2314 | codex := setupCodex(t) 2315 | 2316 | var tests []decodeCase // rows omitted for brevity 2317 | 2318 | for _, test := range tests { 2319 | t.Run(test.name, func(t *testing.T) { 2320 | output, err := Decode(test.input, codex) 2321 | if got, want := output, test.output; got != want { 2322 | t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) 2323 | } 2324 | if got, want := err, test.err; !cmp.Equal(got, want) { 2325 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2326 | } 2327 | }) 2328 | } 2329 | } 2330 | 2331 | func TestDecodeWithFake(t *testing.T) { 2332 | // A fakeCodex is a fast approximation of a real Codex. 2333 | codex := newFakeCodex() 2334 | 2335 | var tests []decodeCase // rows omitted for brevity 2336 | 2337 | for _, test := range tests { 2338 | t.Run(test.name, func(t *testing.T) { 2339 | output, err := Decode(test.input, codex) 2340 | if got, want := output, test.output; got != want { 2341 | t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) 2342 | } 2343 | if got, want := err, test.err; !cmp.Equal(got, want) { 2344 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2345 | } 2346 | }) 2347 | } 2348 | } 2349 | ``` 2350 | 2351 | 在下面的反例中,请注意在case设置中区分每个测试案例使用哪种类型的 `Codex` 是多么困难。(突出显示的部分与 [TotT:数据驱动陷阱!](https://testing.googleblog.com/2008/09/tott-data-driven-traps.html) 的建议相冲突。) 2352 | 2353 | ``` 2354 | // Bad: 2355 | type decodeCase struct { 2356 | name string 2357 | input string 2358 | codex testCodex 2359 | output string 2360 | err error 2361 | } 2362 | 2363 | type testCodex int 2364 | 2365 | const ( 2366 | fake testCodex = iota 2367 | prod 2368 | ) 2369 | 2370 | func TestDecode(t *testing.T) { 2371 | var tests []decodeCase // rows omitted for brevity 2372 | 2373 | for _, test := tests { 2374 | t.Run(test.name, func(t *testing.T) { 2375 | var codex Codex 2376 | switch test.codex { 2377 | case fake: 2378 | codex = newFakeCodex() 2379 | case prod: 2380 | codex = setupCodex(t) 2381 | default: 2382 | t.Fatalf("unknown codex type: %v", codex) 2383 | } 2384 | output, err := Decode(test.input, codex) 2385 | if got, want := output, test.output; got != want { 2386 | t.Errorf("Decode(%q) = %q, want %q", test.input, got, want) 2387 | } 2388 | if got, want := err, test.err; !cmp.Equal(got, want) { 2389 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2390 | } 2391 | }) 2392 | } 2393 | } 2394 | ``` 2395 | 2396 | #### 标识行 2397 | 2398 | 不要使用测试表中的测试索引来代替命名测试或打印输入。没有人愿意通过你的测试表并计算条目以找出哪个测试用例失败。 2399 | 2400 | ``` 2401 | // Bad: 2402 | tests := []struct { 2403 | input, want string 2404 | }{ 2405 | {"hello", "HELLO"}, 2406 | {"wORld", "WORLD"}, 2407 | } 2408 | for i, d := range tests { 2409 | if strings.ToUpper(d.input) != d.want { 2410 | t.Errorf("failed on case #%d", i) 2411 | } 2412 | } 2413 | ``` 2414 | 2415 | 在你的测试结构中添加测试描述,并将其与失败信息一起打印。当使用子测试时,你的子测试名称应能有效识别行。 2416 | 2417 | **重要的是:**即使`t.Run`对输出和执行有一定的范围,你必须始终[识别输入](https://gocn.github.io/styleguide/docs/03-decisions/#标识出输入)。表的测试行名称必须遵循[子测试命名](https://gocn.github.io/styleguide/docs/03-decisions/#子测试名称)的指导。 2418 | 2419 | ### 测试辅助函数 2420 | 2421 | 一个测试辅助函数是一个执行设置或清理任务的函数。所有发生在测试辅助函数中的故障都被认为是环境的故障(而不是被测代码的故障)--例如,当一个测试数据库不能被启动,因为在这台机器上没有更多的空闲端口。 2422 | 2423 | 如果你传递一个`*testing.T`,调用[`t.Helper`](https://pkg.go.dev/testing#T.Helper),将测试辅助函数中的故障归结到调用助手的那一行。这个参数应该在[context](https://gocn.github.io/styleguide/docs/03-decisions/#上下文)参数之后,如果有的话,在任何其他参数之前。 2424 | 2425 | ``` 2426 | // Good: 2427 | func TestSomeFunction(t *testing.T) { 2428 | golden := readFile(t, "testdata/golden-result.txt") 2429 | // ... tests against golden ... 2430 | } 2431 | 2432 | // readFile returns the contents of a data file. 2433 | // It must only be called from the same goroutine as started the test. 2434 | func readFile(t *testing.T, filename string) string { 2435 | t.Helper() 2436 | contents, err := runfiles.ReadFile(filename) 2437 | if err != nil { 2438 | t.Fatal(err) 2439 | } 2440 | return string(contents) 2441 | } 2442 | ``` 2443 | 2444 | 当这种模式掩盖了测试失败和导致失败的条件之间的联系时,请不要使用这种模式。具体来说,关于[断言库](https://gocn.github.io/styleguide/docs/03-decisions/#断言库)的指导仍然适用,[`t.Helper`](https://pkg.go.dev/testing#T.Helper)不应该被用来实现这种库。 2445 | 2446 | **提示:**更多关于测试辅助函数和断言助手的区别,请参见[最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数)。 2447 | 2448 | 虽然上面提到的是`*testing.T`,但大部分建议对基准和模糊帮助器来说都是一样的。 2449 | 2450 | ### 测试包 2451 | 2452 | #### 同一包内的测试 2453 | 2454 | 测试可以和被测试的代码定义在同一个包里。 2455 | 2456 | 要在同一个包中编写测试 2457 | 2458 | - 将测试放在一个`foo_test.go`文件中 2459 | - 在测试文件中使用`package foo`。 2460 | - 不要明确地导入要测试的包 2461 | 2462 | ```build 2463 | # Good: 2464 | go_library( 2465 | name = "foo", 2466 | srcs = ["foo.go"], 2467 | deps = [ 2468 | ... 2469 | ], 2470 | ) 2471 | 2472 | go_test( 2473 | name = "foo_test", 2474 | size = "small", 2475 | srcs = ["foo_test.go"], 2476 | library = ":foo", 2477 | deps = [ 2478 | ... 2479 | ], 2480 | ) 2481 | ``` 2482 | 2483 | 同一个包中的测试可以访问包中未导出的标识符。这可以实现更好的测试覆盖率和更简洁的测试。注意在测试中声明的任何[examples](https://gocn.github.io/styleguide/docs/03-decisions/#示例examples)都不会有用户在他们的代码中需要的包名。 2484 | 2485 | #### 不同包中的测试 2486 | 2487 | 将测试定义在与被测代码相同的包中并不总是合适的,甚至不可能。在这种情况下,使用一个带有`_test`后缀的包名。这是对[包名](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names)的 "不使用下划线"规则的一个例外。比如说。 2488 | 2489 | - 如果一个集成测试没有一个它明显属于的库 2490 | 2491 | ``` 2492 | // Good: 2493 | package gmailintegration_test 2494 | 2495 | import "testing" 2496 | ``` 2497 | 2498 | - 如果在同一包中定义测试会导致循环依赖性 2499 | 2500 | ``` 2501 | // Good: 2502 | package fireworks_test 2503 | 2504 | import ( 2505 | "fireworks" 2506 | "fireworkstestutil" // fireworkstestutil also imports fireworks 2507 | ) 2508 | ``` 2509 | 2510 | ### 使用`testing`包 2511 | 2512 | Go标准库提供了[`testing`包](https://pkg.go.dev/testing)。这是Google代码库中唯一允许用于Go代码的测试框架。特别是[断言库](https://gocn.github.io/styleguide/docs/03-decisions/#断言库)和第三方测试框架是不允许的。 2513 | 2514 | `testing`包为编写好的测试提供了最小但完整的功能集。 2515 | 2516 | - 顶层测试 2517 | - 基准 2518 | - [可运行的例子](https://blog.golang.org/examples) 2519 | - 子测试 2520 | - 记录 2521 | - 失败和致命的失败 2522 | 2523 | 这些设计是为了与核心语言特性如[复合字面](https://go.dev/ref/spec#Composite_literals)和[带有初始化的if语句](https://go.dev/ref/spec#If_statements)语法协同工作,使测试作者能够编写[清晰、可读、可维护的测试]。 2524 | 2525 | ## 非决策性的 2526 | 2527 | 风格指南不能列举所有事项的正面规定,也不能列举所有它不提供意见的事项。也就是说,这里有几件可读性社区以前争论过但没有达成共识的事情。 2528 | 2529 | - **零值的局部变量初始化**。`var i int`和`i := 0`是等同的。参见[初始化最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#初始化)。 2530 | - **空的复合字面与`new`或`make`**。`&File{}`和`new(File)`是等同的。`map[string]bool{}`和`make(map[string]bool)`也是如此。参见[复合声明最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/#复合字面量)。 2531 | - **got/want参数在cmp.Diff调用中的排序**。要有本地一致性,并在你的失败信息中[包括一个图例](https://gocn.github.io/styleguide/docs/03-decisions/#打印差异)。 2532 | - **`errors.New`与`fmt.Errorf`在非格式化字符串上的对比**。`errors.New("foo")`和`fmt.Errorf("foo")`可以互换使用。 2533 | 2534 | 如果有特殊情况,它们又出现了,可读性导师可能会做一个可选的注释,但一般来说,作者可以自由选择他们在特定情况下喜欢的风格。 2535 | 2536 | 当然,如果风格指南中没有涉及的东西确实需要更多的讨论,欢迎在具体的审查中,或者在内部留言板上提出来。 2537 | 2538 | {{< button relref="./02-guide.md" >}}上一章{{< /button >}} 2539 | {{< button relref="./04-best-practices.md" >}}下一章{{< /button >}} 2540 | -------------------------------------------------------------------------------- /content/zh/docs/04-best-practices.md: -------------------------------------------------------------------------------- 1 | # Go 编程风格指南 - 最佳实践 2 | 3 | 原文:[https://google.github.io/styleguide/go](https://google.github.io/styleguide/go) 4 | 5 | [概述](https://gocn.github.io/styleguide/docs/01-overview/) | [风格指南](https://gocn.github.io/styleguide/docs/02-guide/) | [风格决策](https://gocn.github.io/styleguide/docs/03-decisions/) | [最佳实践](https://gocn.github.io/styleguide/docs/04-best-practices/) 6 | 7 | **注意:** 本文是 Google [Go 风格](https://gocn.github.io/styleguide/docs/01-overview/) 系列文档的一部分。本文档是 **[规范性(normative)](https://gocn.github.io/styleguide/docs/01-overview/#标准normative) 但不是[强制规范(canonical)](https://gocn.github.io/styleguide/docs/01-overview/#规范canonical)**,并且从属于[Google 风格指南](https://gocn.github.io/styleguide/docs/02-guide/)。请参阅[概述](https://gocn.github.io/styleguide/docs/01-overview/#关于)获取更多详细信息。 8 | 9 | ## 关于 10 | 11 | 本文件记录了**关于如何更好地应用 Go 风格指南的指导意见**。该指导旨在解决经常出现的通用问题,但不一定适用于所有情况。在可能的情况下,我们讨论了多种替代方法,以及决定何时该用和何时不该用这些方法的考虑因素。 12 | 13 | 查看[概述](https://gocn.github.io/styleguide/docs/01-overview/#关于)来获取完整的风格指导文档 14 | 15 | ## 命名 16 | 17 | ### 函数和方法名称 18 | 19 | #### 避免重复 20 | 21 | 在为一个函数或方法选择名称时,要考虑该名称将被阅读的环境。请考虑以下建议,以避免在调用地点出现过多的[重复](https://gocn.github.io/styleguide/docs/03-decisions/#重复repetition): 22 | 23 | - 以下内容一般可以从函数和方法名称中省略。 24 | 25 | - 输入和输出的类型(当没有冲突的时候) 26 | - 方法的接收器的类型 27 | - 一个输入或输出是否是一个指针 28 | 29 | - 对于函数,不要[重复软件包的名称](https://gocn.github.io/styleguide/docs/03-decisions/#包名-vs-可导出符号名package-vs-exported-symbol-name)。 30 | 31 | ```go 32 | // Bad: 33 | package yamlconfig 34 | 35 | func ParseYAMLConfig(input string) (*Config, error) 36 | ``` 37 | 38 | ```go 39 | // Good: 40 | package yamlconfig 41 | 42 | func Parse(input string) (*Config, error) 43 | ``` 44 | 45 | - 对于方法不要重复方法接收器的名称。 46 | 47 | ```go 48 | // Bad: 49 | func (c *Config) WriteConfigTo(w io.Writer) (int64, error) 50 | ``` 51 | 52 | ```go 53 | // Good: 54 | func (c *Config) WriteTo(w io.Writer) (int64, error) 55 | ``` 56 | 57 | - 不要重复传参的变量名称 58 | 59 | ```go 60 | // Bad: 61 | func OverrideFirstWithSecond(dest, source *Config) error 62 | ``` 63 | 64 | ```go 65 | // Good: 66 | func Override(dest, source *Config) error 67 | ``` 68 | 69 | - 不要重复返回值的名称和类型 70 | 71 | ```go 72 | // Bad: 73 | func TransformYAMLToJSON(input *Config) *jsonconfig.Config 74 | ``` 75 | 76 | ```go 77 | // Good: 78 | func Transform(input *Config) *jsonconfig.Config 79 | ``` 80 | 81 | 当有必要区分类似名称的函数时,包含额外信息是可以接受的。 82 | 83 | ```go 84 | // Good: 85 | func (c *Config) WriteTextTo(w io.Writer) (int64, error) 86 | func (c *Config) WriteBinaryTo(w io.Writer) (int64, error) 87 | ``` 88 | 89 | #### 命名约定 90 | 91 | 在为函数和方法选择名称时,还有一些常见的约定: 92 | 93 | - 有返回结果的函数使用类名词的名称。 94 | 95 | ``` 96 | // Good: 97 | func (c *Config) JobName(key string) (value string, ok bool) 98 | ``` 99 | 100 | 这方面的一个推论是,函数和方法名称应该[避免使用前缀`Get`](https://gocn.github.io/styleguide/docs/03-decisions/#get方法getters)。 101 | 102 | ``` 103 | // Bad: 104 | func (c *Config) GetJobName(key string) (value string, ok bool) 105 | ``` 106 | 107 | - 做事的函数被赋予类似动词的名称。 108 | 109 | ``` 110 | // Good: 111 | func (c *Config) WriteDetail(w io.Writer) (int64, error) 112 | ``` 113 | 114 | - 只因所涉及的类型而不同,而功能相同的函数在名称的末尾带上类型的名称。 115 | 116 | ``` 117 | // Good: 118 | func ParseInt(input string) (int, error) 119 | func ParseInt64(input string) (int64, error) 120 | func AppendInt(buf []byte, value int) []byte 121 | func AppendInt64(buf []byte, value int64) []byte 122 | ``` 123 | 124 | 如果有一个明确的 "主要 "版本,该版本的名称中可以省略类型: 125 | 126 | ``` 127 | // Good: 128 | func (c *Config) Marshal() ([]byte, error) 129 | func (c *Config) MarshalText() (string, error) 130 | ``` 131 | 132 | 133 | ### 测试替身包和类型 134 | 135 | 有几个原则你可以应用于[命名](https://gocn.github.io/styleguide/docs/02-guide/#命名)包和类型,提供测试辅助函数,特别是[测试替身](https://en.wikipedia.org/wiki/Test_double)。一个测试替身可以是一个桩、假的、模拟的或间谍的。 136 | 这些例子大多使用打桩。如果你的代码使用假的或其他类型的测试替身,请相应地更新你的名字。 137 | 假设你有一个重点突出的包,提供与此类似的生产代码: 138 | 139 | ```go 140 | package creditcard 141 | 142 | import ( 143 | "errors" 144 | 145 | "path/to/money" 146 | ) 147 | 148 | // ErrDeclined indicates that the issuer declines the charge. 149 | var ErrDeclined = errors.New("creditcard: declined") 150 | 151 | // Card contains information about a credit card, such as its issuer, 152 | // expiration, and limit. 153 | type Card struct { 154 | // omitted 155 | } 156 | 157 | // Service allows you to perform operations with credit cards against external 158 | // payment processor vendors like charge, authorize, reimburse, and subscribe. 159 | type Service struct { 160 | // omitted 161 | } 162 | 163 | func (s *Service) Charge(c *Card, amount money.Money) error { /* omitted */ } 164 | ``` 165 | 166 | #### 创建测试辅助包 167 | 168 | 假设你想创建一个包,包含另一个包的测试替身。在这个例子中我们将使用`package creditcard`(来自上面)。 169 | 一种方法是在生产包的基础上引入一个新的 Go 包进行测试。一个安全的选择是在原来的包名后面加上`test`这个词("creditcard" + "test")。 170 | 171 | ``` 172 | // Good: 173 | package creditcardtest 174 | ``` 175 | 176 | 除非另有明确说明,以下各节中的所有例子都在 `package creditcardtest` 中。 177 | 178 | #### 简单案例 179 | 180 | 你想为`Service`添加一组测试替身。因为`Card`是一个有效的哑巴数据类型,类似于协议缓冲区的消息,它在测试中不需要特殊处理,所以不需要替身。如果你预计只有一种类型(如`Service')的测试替身,你可以采取一种简洁的方法来命名替身。 181 | 182 | ```go 183 | // Good: 184 | import ( 185 | "path/to/creditcard" 186 | "path/to/money" 187 | ) 188 | 189 | // Stub stubs creditcard.Service and provides no behavior of its own. 190 | type Stub struct{} 191 | 192 | func (Stub) Charge(*creditcard.Card, money.Money) error { return nil } 193 | ``` 194 | 195 | 严格来说,这比像 `StubService` 或非常差的 `StubCreditCardService` 这样的命名选择要好,因为基础包的名字和它的域类型意味着 `creditcardtest.Stub` 是什么。 196 | 最后,如果该包是用 Bazel 构建的,确保该包的新 `go_library` 规则被标记为 `testonly`。 197 | 198 | ```go 199 | # Good: 200 | go_library( 201 | name = "creditcardtest", 202 | srcs = ["creditcardtest.go"], 203 | deps = [ 204 | ":creditcard", 205 | ":money", 206 | ], 207 | testonly = True, 208 | ) 209 | ``` 210 | 211 | 上述方法是常规的,其他工程师也会很容易理解。 212 | 213 | 另见: 214 | 215 | - [Go Tip #42: 为测试便编写桩](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 216 | 217 | #### 多重测试替身行为 218 | 219 | 当一种桩不够用时(例如,你还需要一种总是失败的桩),我们建议根据它们所模拟的行为来命名桩。这里我们把`Stub`改名为`AlwaysCharges`,并引入一个新的桩,叫做`AlwaysDeclines`: 220 | 221 | ```go 222 | // Good: 223 | // AlwaysCharges stubs creditcard.Service and simulates success. 224 | type AlwaysCharges struct{} 225 | 226 | func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil } 227 | 228 | // AlwaysDeclines stubs creditcard.Service and simulates declined charges. 229 | type AlwaysDeclines struct{} 230 | 231 | func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error { 232 | return creditcard.ErrDeclined 233 | } 234 | ``` 235 | 236 | #### 多种类型的多重替身 237 | 238 | 但现在假设 `package creditcard` 包含多个值得创建替身的类型,如下面的 `Service` 和 `StoredValue` : 239 | 240 | ```go 241 | package creditcard 242 | 243 | type Service struct { 244 | // omitted 245 | } 246 | 247 | type Card struct { 248 | // omitted 249 | } 250 | 251 | // StoredValue manages customer credit balances. This applies when returned 252 | // merchandise is credited to a customer's local account instead of processed 253 | // by the credit issuer. For this reason, it is implemented as a separate 254 | // service. 255 | type StoredValue struct { 256 | // omitted 257 | } 258 | 259 | func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* omitted */ } 260 | ``` 261 | 262 | In this case, more explicit test double naming is sensible: 263 | 264 | 在这种情况下,更明确的测试替身命名是明智的: 265 | 266 | ```go 267 | // Good: 268 | type StubService struct{} 269 | 270 | func (StubService) Charge(*creditcard.Card, money.Money) error { return nil } 271 | 272 | type StubStoredValue struct{} 273 | 274 | func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil } 275 | ``` 276 | 277 | #### 测试中的局部变量 278 | 279 | 当你的测试中的变量引用替身时,要根据上下文选择一个能最清楚地区分替身和其他生产类型的名称。考虑一下你要测试的一些生产代码: 280 | 281 | ```go 282 | package payment 283 | 284 | import ( 285 | "path/to/creditcard" 286 | "path/to/money" 287 | ) 288 | 289 | type CreditCard interface { 290 | Charge(*creditcard.Card, money.Money) error 291 | } 292 | 293 | type Processor struct { 294 | CC CreditCard 295 | } 296 | 297 | var ErrBadInstrument = errors.New("payment: instrument is invalid or expired") 298 | 299 | func (p *Processor) Process(c *creditcard.Card, amount money.Money) error { 300 | if c.Expired() { 301 | return ErrBadInstrument 302 | } 303 | return p.CC.Charge(c, amount) 304 | } 305 | ``` 306 | 307 | 在测试中,一个被称为 "间谍 "的 `CreditCard` 的测试替身与生产类型并列,所以给名字加前缀可以提高清晰度。 308 | 309 | ```go 310 | // Good: 311 | package payment 312 | 313 | import "path/to/creditcardtest" 314 | 315 | func TestProcessor(t *testing.T) { 316 | var spyCC creditcardtest.Spy 317 | 318 | proc := &Processor{CC: spyCC} 319 | 320 | // declarations omitted: card and amount 321 | if err := proc.Process(card, amount); err != nil { 322 | t.Errorf("proc.Process(card, amount) = %v, want %v", got, want) 323 | } 324 | 325 | charges := []creditcardtest.Charge{ 326 | {Card: card, Amount: amount}, 327 | } 328 | 329 | if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) { 330 | t.Errorf("spyCC.Charges = %v, want %v", got, want) 331 | } 332 | } 333 | ``` 334 | 335 | 这比没有前缀的名字更清楚。 336 | 337 | ```go 338 | // Bad: 339 | package payment 340 | 341 | import "path/to/creditcardtest" 342 | 343 | func TestProcessor(t *testing.T) { 344 | var cc creditcardtest.Spy 345 | 346 | proc := &Processor{CC: cc} 347 | 348 | // declarations omitted: card and amount 349 | if err := proc.Process(card, amount); err != nil { 350 | t.Errorf("proc.Process(card, amount) = %v, want %v", got, want) 351 | } 352 | 353 | charges := []creditcardtest.Charge{ 354 | {Card: card, Amount: amount}, 355 | } 356 | 357 | if got, want := cc.Charges, charges; !cmp.Equal(got, want) { 358 | t.Errorf("cc.Charges = %v, want %v", got, want) 359 | } 360 | } 361 | ``` 362 | 363 | ### 阴影 364 | 365 | **注意:**本解释使用了两个非正式的术语,_stomping_ 和 _shadowing_。它们不是 Go 语言规范中的正式概念。 366 | 像许多编程语言一样,Go 有可变的变量:对一个变量的赋值会改变其值。 367 | 368 | ```go 369 | // Good: 370 | func abs(i int) int { 371 | if i < 0 { 372 | i *= -1 373 | } 374 | return i 375 | } 376 | ``` 377 | 378 | 当使用[短变量声明](https://go.dev/ref/spec#Short_variable_declarations)与 `:=` 操作符时,在某些情况下不会创建一个新的变量。我们可以把这称为 _stomping_。当不再需要原来的值时,这样做是可以的。 379 | 380 | ```go 381 | // Good: 382 | // innerHandler is a helper for some request handler, which itself issues 383 | // requests to other backends. 384 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 385 | // Unconditionally cap the deadline for this part of request handling. 386 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 387 | defer cancel() 388 | ctxlog.Info("Capped deadline in inner request") 389 | 390 | // Code here no longer has access to the original context. 391 | // This is good style if when first writing this, you anticipate 392 | // that even as the code grows, no operation legitimately should 393 | // use the (possibly unbounded) original context that the caller provided. 394 | 395 | // ... 396 | } 397 | ``` 398 | 399 | 不过要小心在新的作用域中使用短的变量声明 `:` 这将引入一个新的变量。我们可以把这称为对原始变量的 "阴影"。块结束后的代码指的是原来的。这里是一个有条件缩短期限的错误尝试: 400 | 401 | ```go 402 | // Bad: 403 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 404 | // Attempt to conditionally cap the deadline. 405 | if *shortenDeadlines { 406 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 407 | defer cancel() 408 | ctxlog.Info(ctx, "Capped deadline in inner request") 409 | } 410 | 411 | // BUG: "ctx" here again means the context that the caller provided. 412 | // The above buggy code compiled because both ctx and cancel 413 | // were used inside the if statement. 414 | 415 | // ... 416 | } 417 | ``` 418 | 419 | 正确版本的代码应该是: 420 | 421 | ```go 422 | // Good: 423 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 424 | if *shortenDeadlines { 425 | var cancel func() 426 | // Note the use of simple assignment, = and not :=. 427 | ctx, cancel = context.WithTimeout(ctx, 3*time.Second) 428 | defer cancel() 429 | ctxlog.Info(ctx, "Capped deadline in inner request") 430 | } 431 | // ... 432 | } 433 | ``` 434 | 435 | 在我们称之为 stomping 的情况下,因为没有新的变量,所以被分配的类型必须与原始变量的类型相匹配。有了 shadowing,一个全新的实体被引入,所以它可以有不同的类型。有意的影子可以是一种有用的做法,但如果能提高[清晰度](https://gocn.github.io/styleguide/docs/02-guide/#清晰),你总是可以使用一个新的名字。 436 | 437 | 除了非常小的范围之外,使用与标准包同名的变量并不是一个好主意,因为这使得该包的自由函数和值无法访问。相反,当为你的包挑选名字时,要避免使用那些可能需要[导入重命名](https://gocn.github.io/styleguide/docs/03-decisions/#导入重命名)或在客户端造成阴影的其他好的变量名称。 438 | 439 | ```go 440 | // Bad: 441 | func LongFunction() { 442 | url := "https://example.com/" 443 | // Oops, now we can't use net/url in code below. 444 | } 445 | ``` 446 | 447 | ### 工具包 448 | 449 | Go 包在 `package` 声明中指定了一个名称,与导入路径分开。包的名称比路径更重要,因为它的可读性。 450 | 451 | Go 包的名字应该是[与包所提供的内容相关](https://gocn.github.io/styleguide/docs/03-decisions/#包名称package-names)。将包命名为 `util`、`helper`、`common` 或类似的名字通常是一个糟糕的选择(但它可以作为名字的一部分)。没有信息的名字会使代码更难读,而且如果使用的范围太广,很容易造成不必要的[导入冲突](https://gocn.github.io/styleguide/docs/03-decisions/#导入重命名)。 452 | 453 | 相反,考虑一下调用站会是什么样子。 454 | 455 | ```go 456 | // Good: 457 | db := spannertest.NewDatabaseFromFile(...) 458 | 459 | _, err := f.Seek(0, io.SeekStart) 460 | 461 | b := elliptic.Marshal(curve, x, y) 462 | ``` 463 | 464 | 即使不知道导入列表,你也能大致知道这些东西的作用(`cloud.google.com/go/spanner/spannertest`,`io`,和`crypto/elliptic`)。如果名称不那么集中,这些可能是: 465 | 466 | ```go 467 | // Bad: 468 | db := test.NewDatabaseFromFile(...) 469 | 470 | _, err := f.Seek(0, common.SeekStart) 471 | 472 | b := helper.Marshal(curve, x, y) 473 | ``` 474 | 475 | ## 包大小 476 | 477 | 如果你在问自己,你的 Go 包应该有多大,是把相关的类型放在同一个包里,还是把它们分成不同的包,可以从[关于包名的 Go 博文](https://go.dev/blog/package-names)开始。尽管帖子的标题是这样的,但它并不仅仅是关于命名的。它包含了一些有用的提示并引用了一些有用的文章和讲座。 478 | 479 | 这里有一些其他的考虑和说明。 480 | 481 | 用户在一页中看到包的 [godoc](https://pkg.go.dev/),任何由包提供的类型导出的方法都按其类型分组。Godoc 还将构造函数与它们返回的类型一起分组。如果_客户端代码_可能需要两个不同类型的值来相互作用,那么把它们放在同一个包里对用户来说可能很方便。 482 | 483 | 包内的代码可以访问包内未导出的标识符。如果你有几个相关的类型,它们的_实现_是紧密耦合的,把它们放在同一个包里可以让你实现这种耦合,而无需用这些细节污染公共 API。 484 | 485 | 综上所述,把你的整个项目放在一个包里可能会使这个包变得太大。当一个东西在概念上是不同的,给它一个自己的小包可以使它更容易使用。客户端所知道的包的短名称和导出的类型名称一起构成了一个有意义的标识符:例如`bytes.Buffer`, `ring.New`。[博文](https://go.dev/blog/package-names)有更多的例子。 486 | 487 | Go 风格在文件大小方面很灵活,因为维护者可以将包内的代码从一个文件移到另一个文件而不影响调用者。但作为一般准则:通常情况下,一个文件有几千行,或者有许多小文件,都不是一个好主意。没有像其他一些语言那样的 "一个类型,一个文件 "的约定。作为一个经验法则,文件应该足够集中,以便维护者可以知道哪个文件包含了什么东西,而且文件应该足够小,以便一旦有了这些东西,就很容易找到。标准库经常将大型包分割成几个源文件,将相关的代码按文件分组。[`bytes` 包](https://go.dev/src/bytes/) 的源代码就是一个很好的例子。有很长包文档的包可以选择专门的一个文件,称为`doc.go`,其中有[包文档](https://gocn.github.io/styleguide/docs/03-decisions/#包注释),包声明,而没有其他内容,但这不是必须的。 488 | 489 | 在 Google 代码库和使用 Bazel 的项目中,Go 代码的目录布局与开源 Go 项目不同:你可以在一个目录中拥有多个`go_library`目标。如果你期望在未来将你的项目开源,那么给每个包提供自己的目录是一个很好的理由。 490 | 491 | 另见: 492 | 493 | - [测试替身包](https://gocn.github.io/styleguide/docs/04-best-practices/#测试替身包和类型) 494 | 495 | ## 导入 496 | 497 | ### Protos and stubs 498 | 499 | 由于其跨语言的特性,Proto 库导入的处理方式与标准 Go 导入不同。重命名的 proto 导入的约定是基于生成包的规则: 500 | 501 | - `pb`后缀一般用于 `go_proto_library` 规则。 502 | - `grpc`后缀一般用于 `go_grpc_library` 规则。 503 | 504 | 一般来说,使用简短的一个或两个字母的前缀: 505 | 506 | ```go 507 | // Good: 508 | import ( 509 | fspb "path/to/package/foo_service_go_proto" 510 | fsgrpc "path/to/package/foo_service_go_grpc" 511 | ) 512 | ``` 513 | 514 | 如果一个包只使用一个 proto,或者该包与该 proto 紧密相连,那么前缀可以省略: 515 | 516 | import ( pb “path/to/package/foo\_service\_go\_proto” grpc “path/to/package/foo\_service\_go\_grpc” ) 517 | 518 | 如果 proto 中的符号是通用的,或者没有很好的自我描述,或者用首字母缩写来缩短包的名称是不明确的,那么一个简短的词就可以作为前缀: 519 | 520 | ```go 521 | // Good: 522 | import ( 523 | mapspb "path/to/package/maps_go_proto" 524 | ) 525 | ``` 526 | 527 | 在这种情况下,如果有关的代码还没有明确与地图相关,那么 `mapspb.Address` 可能比 `mpb.Address` 更清楚。 528 | 529 | ### 导入顺序 530 | 531 | 导入通常按顺序分为以下两(或更多)块。 532 | 533 | 1. 标准库导入(例如,`"fmt"`)。 534 | 2. 导入(例如,"/path/to/somelib")。 535 | 3. (可选)Protobuf 导入(例如,`fpb "path/to/foo_go_proto"`)。 536 | 4. (可选)无使用导入(例如,`_ "path/to/package"`) 537 | 538 | 如果一个文件没有上述可选类别组,相关的导入将包含在项目倒入组中。 539 | 540 | 任何清晰和容易理解的导入分组通常都是好的。例如,一个团队可以选择将 gRPC 导入与 protobuf 导入分开分组。 541 | 542 | > **注意:** 对于只维护两个强制组的代码 (一个组用于标准库,一个组用于所有其他的导入), `goimports` 工具产生的输出与这个指南一致。 543 | > 然而, `goimports` 并不了解强制性组以外的组;可选组很容易被该工具废止。当使用可选的组别时,作者和审稿人都需要注意,以确保组别符合要求。 544 | > 两种方法都可以,但不要让进口部分处于不一致的、部分分组的状态。 545 | 546 | ## 错误处理 547 | 548 | 在 Go 中,[错误就是价值](https://go.dev/blog/errors-are-values);它们由代码创造,也由代码消耗。错误可以是: 549 | 550 | - 转化为诊断信息,显示给程序员看 551 | - 由维护者使用 552 | - 向终端用户解释 553 | 554 | 错误信息也显示在各种不同的渠道,包括日志信息、错误转储和渲染的 UI。 555 | 556 | 处理(产生或消耗)错误的代码应该刻意这样做。忽略或盲目地传播错误的返回值可能是很诱人的。然而,值得注意的是,调用框架中的当前函数是否被定位为最有效地处理该错误。这是一个很大的话题,很难给出明确的建议。请使用你自己的判断,但要记住以下的考虑。 557 | 558 | - 当创建一个错误值时,决定是否给它任何[结构](https://gocn.github.io/styleguide/docs/04-best-practices/#错误结构)。 559 | - 当处理一个错误时,考虑[添加信息](https://gocn.github.io/styleguide/docs/04-best-practices/#为错误添加信息),这些信息你有,但调用者和/或被调用者可能没有。 560 | - 也请参见关于[错误记录](https://gocn.github.io/styleguide/docs/04-best-practices/#错误日志)的指导。 561 | 562 | 虽然忽略一个错误通常是不合适的,但一个合理的例外是在协调相关操作时,通常只有第一个错误是有用的。包[`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup)为一组操作提供了一个方便的抽象,这些操作都可以作为一个组失败或被取消。 563 | 564 | 也请参见: 565 | 566 | - [Effective Go on errors](https://go.dev/doc/effective_go#errors) 567 | - [Go博客关于错误的文章](https://go.dev/blog/go1.13-errors) 568 | - [`errors` 包](https://pkg.go.dev/errors) 569 | - [`upspin.io/errors`包](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html) 570 | - [GoTip #89: 何时使用规范的状态码作为错误?](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 571 | - [GoTip #48: 错误的哨兵值](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 572 | - [GoTip #13: 设计用于检查的错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 573 | 574 | ### 错误结构 575 | 576 | 如果调用者需要询问错误(例如,区分不同的错误条件),请给出错误值结构,这样可以通过编程完成,而不是让调用者进行字符串匹配。这个建议适用于生产代码,也适用于关心不同错误条件的测试。 577 | 578 | 最简单的结构化错误是无参数的全局值。 579 | 580 | ```go 581 | type Animal string 582 | 583 | var ( 584 | // ErrDuplicate occurs if this animal has already been seen. 585 | ErrDuplicate = errors.New("duplicate") 586 | 587 | // ErrMarsupial occurs because we're allergic to marsupials outside Australia. 588 | // Sorry. 589 | ErrMarsupial = errors.New("marsupials are not supported") 590 | ) 591 | 592 | func pet(animal Animal) error { 593 | switch { 594 | case seen[animal]: 595 | return ErrDuplicate 596 | case marsupial(animal): 597 | return ErrMarsupial 598 | } 599 | seen[animal] = true 600 | // ... 601 | return nil 602 | } 603 | ``` 604 | 605 | 调用者可以简单地将函数返回的错误值与已知的错误值之一进行比较: 606 | 607 | ```go 608 | // Good: 609 | func handlePet(...) { 610 | switch err := process(an); err { 611 | case ErrDuplicate: 612 | return fmt.Errorf("feed %q: %v", an, err) 613 | case ErrMarsupial: 614 | // Try to recover with a friend instead. 615 | alternate = an.BackupAnimal() 616 | return handlePet(..., alternate, ...) 617 | } 618 | } 619 | ``` 620 | 621 | 上面使用了哨兵值,其中误差必须等于(在`==`的意义上)预期值。这在很多情况下是完全足够的。如果`process`返回包装好的错误(下面讨论),你可以使用[`errors.Is`](https://pkg.go.dev/errors#Is)。 622 | 623 | ```go 624 | // Good: 625 | func handlePet(...) { 626 | switch err := process(an); { 627 | case errors.Is(err, ErrDuplicate): 628 | return fmt.Errorf("feed %q: %v", an, err) 629 | case errors.Is(err, ErrMarsupial): 630 | // ... 631 | } 632 | } 633 | ``` 634 | 635 | 不要试图根据字符串的形式来区分错误。(参见[Go Tip #13: 设计用于检查的错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip)以了解更多信息)。 636 | 637 | ```go 638 | // Bad: 639 | func handlePet(...) { 640 | err := process(an) 641 | if regexp.MatchString(`duplicate`, err.Error()) {...} 642 | if regexp.MatchString(`marsupial`, err.Error()) {...} 643 | } 644 | ``` 645 | 646 | 如果错误中有调用者需要的额外信息,最好是以结构化方式呈现。例如,[`os.PathError`](https://pkg.go.dev/os#PathError) 类型的记录是将失败操作的路径名,放在调用者可以轻松访问的结构域中。 647 | 648 | 其他错误结构可以酌情使用,例如一个包含错误代码和细节字符串的项目结构。[Package `status`](https://pkg.go.dev/google.golang.org/grpc/status) 是一种常见的封装方式;如果你选择这种方式(你没有义务这么做),请使用 [规范错误码](https://pkg.go.dev/google.golang.org/grpc/codes)。参见 [Go Tip #89: 何时使用规范的状态码作为错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 以了解使用状态码是否是正确的选择。 649 | 650 | ### 为错误添加信息 651 | 652 | 任何返回错误的函数都应该努力使错误值变得有用。通常情况下,该函数处于一个调用链的中间,并且只是在传播它所调用的其他函数的错误(甚至可能来自另一个包)。这里有机会用额外的信息来注解错误,但程序员应该确保错误中有足够的信息,而不添加重复的或不相关的细节。如果你不确定,可以尝试在开发过程中触发错误条件:这是一个很好的方法来评估错误的观察者(无论是人类还是代码)最终会得到什么。 653 | 654 | 习惯和良好的文档有帮助。例如,标准包`os`宣传其错误包含路径信息,当它可用时。这是一种有用的风格,因为得到错误的调用者不需要用他们已经提供了失败的函数的信息来注释它。 655 | 656 | ```go 657 | // Good: 658 | if err := os.Open("settings.txt"); err != nil { 659 | return err 660 | } 661 | 662 | // Output: 663 | // 664 | // open settings.txt: no such file or directory 665 | ``` 666 | 667 | 如果对错误的_意义_有什么有趣的说法,当然可以加入。只需考虑调用链的哪一层最适合理解这个含义。 668 | 669 | ```go 670 | // Good: 671 | if err := os.Open("settings.txt"); err != nil { 672 | // We convey the significance of this error to us. Note that the current 673 | // function might perform more than one file operation that can fail, so 674 | // these annotations can also serve to disambiguate to the caller what went 675 | // wrong. 676 | return fmt.Errorf("launch codes unavailable: %v", err) 677 | } 678 | 679 | // Output: 680 | // 681 | // launch codes unavailable: open settings.txt: no such file or directory 682 | ``` 683 | 684 | 与这里的冗余信息形成鲜明对比: 685 | 686 | ```go 687 | // Bad: 688 | if err := os.Open("settings.txt"); err != nil { 689 | return fmt.Errorf("could not open settings.txt: %w", err) 690 | } 691 | 692 | // Output: 693 | // 694 | // could not open settings.txt: open settings.txt: no such file or directory 695 | ``` 696 | 697 | 当添加信息到一个传播的错误时,你可以包裹错误或提出一个新的错误。用`fmt.Errorf`中的`%w`动词来包装错误,允许调用者访问原始错误的数据。这在某些时候是非常有用的,但在其他情况下,这些细节对调用者来说是误导或不感兴趣的。更多信息请参见[关于错误包装的博文](https://blog.golang.org/go1.13-errors)。包裹错误也以一种不明显的方式扩展了你的包的 API 表面,如果你改变了你的包的实现细节,这可能会导致破坏。 698 | 699 | 最好避免使用`%w`,除非你也记录(并有测试来验证)你所暴露的基本错误。如果你不期望你的调用者调用`errors.Unwrap`, `errors.Is`等等,就不要费心使用`%w`。 700 | 701 | 同样的概念适用于[结构化错误](https://gocn.github.io/styleguide/docs/04-best-practices/#错误结构),如[`*status.Status`](https://pkg.go.dev/google.golang.org/grpc/status)(见[规范错误码](https://pkg.go.dev/google.golang.org/grpc/codes))。例如,如果你的服务器向后端发送畸形的请求,并收到一个`InvalidArgument` 错误码,这个代码不应该传播给客户端,假设客户端没有做错。相反,应该向客户端返回一个`内部`的规范码。 702 | 703 | 然而,注解错误有助于自动日志系统保留错误的状态有效载荷。例如,在一个内部函数中注释错误是合适的: 704 | 705 | ```go 706 | // Good: 707 | func (s *Server) internalFunction(ctx context.Context) error { 708 | // ... 709 | if err != nil { 710 | return fmt.Errorf("couldn't find remote file: %w", err) 711 | } 712 | } 713 | ``` 714 | 715 | 直接位于系统边界的代码(通常是RPC、IPC、存储等之类的)应该使用规范的错误空间报告错误。这里的代码有责任处理特定领域的错误,并以规范的方式表示它们。比如说: 716 | 717 | ```go 718 | // Bad: 719 | func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { 720 | // ... 721 | if err != nil { 722 | return nil, fmt.Errorf("couldn't find remote file: %w", err) 723 | } 724 | } 725 | ``` 726 | 727 | ```go 728 | // Good: 729 | import ( 730 | "google.golang.org/grpc/codes" 731 | "google.golang.org/grpc/status" 732 | ) 733 | func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { 734 | // ... 735 | if err != nil { 736 | // Or use fmt.Errorf with the %w verb if deliberately wrapping an 737 | // error which the caller is meant to unwrap. 738 | return nil, status.Errorf(codes.Internal, "couldn't find fortune database", status.ErrInternal) 739 | } 740 | } 741 | ``` 742 | 743 | ### 错误中的 %w 的位置 744 | 745 | 倾向于将`%w`放在错误字符串的末尾。 746 | 747 | 错误可以用[`%w`动词](https://blog.golang.org/go1.13-errors)来包装,或者把它们放在一个实现`Unwrap()错误`的[结构化错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip)中(例如:[`fs.PathError`](https://pkg.go.dev/io/fs#PathError))。 748 | 749 | 被包裹的错误形成错误链:每一层新的包裹都会在错误链的前面增加一个新的条目。错误链可以用`Unwrap()error`方法进行遍历。比如说: 750 | 751 | ``` 752 | err1 := fmt.Errorf("err1") 753 | err2 := fmt.Errorf("err2: %w", err1) 754 | err3 := fmt.Errorf("err3: %w", err2) 755 | ``` 756 | 757 | 这就形成了一个错误链的形式, 758 | 759 | ``` 760 | flowchart LR 761 | err3 == err3 wraps err2 ==> err2; 762 | err2 == err2 wraps err1 ==> err1; 763 | ``` 764 | 765 | 不管`%w`动词放在哪里,返回的错误总是代表错误链的前面,而`%w`是下一个子节点。同样,`Unwrap()error`总是从最新的错误到最旧的错误穿越错误链。 766 | 767 | 然而,`%w`动词的位置会影响错误链是从最新到最旧,从最旧到最新,还是两者都不影响: 768 | 769 | ```go 770 | // Good: 771 | err1 := fmt.Errorf("err1") 772 | err2 := fmt.Errorf("err2: %w", err1) 773 | err3 := fmt.Errorf("err3: %w", err2) 774 | fmt.Println(err3) // err3: err2: err1 775 | // err3 is a newest-to-oldest error chain, that prints newest-to-oldest. 776 | ``` 777 | 778 | ```go 779 | // Bad: 780 | err1 := fmt.Errorf("err1") 781 | err2 := fmt.Errorf("%w: err2", err1) 782 | err3 := fmt.Errorf("%w: err3", err2) 783 | fmt.Println(err3) // err1: err2: err3 784 | // err3 is a newest-to-oldest error chain, that prints oldest-to-newest. 785 | ``` 786 | 787 | ```go 788 | // Bad: 789 | err1 := fmt.Errorf("err1") 790 | err2 := fmt.Errorf("err2-1 %w err2-2", err1) 791 | err3 := fmt.Errorf("err3-1 %w err3-2", err2) 792 | fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2 793 | // err3 is a newest-to-oldest error chain, that neither prints newest-to-oldest 794 | // nor oldest-to-newest. 795 | ``` 796 | 797 | 因此,为了使错误文本反映错误链结构,最好将`%w`动词放在最后,形式为`[...]: %w`。 798 | 799 | ### 错误日志 800 | 801 | 函数有时需要告诉外部系统一个错误,而不把它传播给其调用者。在这里,日志是一个明显的选择;但要注意记录错误的内容和方式。 802 | 803 | - 像[好的测试失败信息](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败)一样,日志信息应该清楚地表达出错的原因,并通过包括相关信息来帮助维护者诊断问题。 804 | - 避免重复。如果你返回一个错误,通常最好不要自己记录,而是让调用者处理。调用者可以选择记录错误,也可以使用[`rate.sometimes`](https://pkg.go.dev/golang.org/x/time/rate#Sometimes)限制记录的速度。其他选择包括尝试恢复,甚至是[停止程序](https://gocn.github.io/styleguide/docs/04-best-practices/#程序检查和-panic)。在任何情况下,让调用者控制有助于避免日志垃圾。 805 | 然而,这种方法的缺点是,任何日志都是用调用者的行座标写的。 806 | - 对[PII](https://en.wikipedia.org/wiki/Personal_data)要小心。许多日志汇不是敏感的终端用户信息的合适目的地。 807 | - 尽量少使用`log.Error`。ERROR级别的日志会导致刷新,比低级别的日志更昂贵。这可能会对你的代码产生严重的性能影响。当决定错误级别还是警告级别时,考虑最佳实践,即错误级别的消息应该是可操作的,而不是比警告 "更严重"。 808 | - 在谷歌内部,我们有监控系统,可以设置更有效的警报,而不是写到日志文件,希望有人注意到它。这与标准库[` expvar` 包](https://pkg.go.dev/expvar)类似,但不完全相同。 809 | 810 | #### 自定义日志级别 811 | 812 | 使用日志分级([`log.V`](https://pkg.go.dev/github.com/golang/glog#V))对你有利。分级的日志对开发和追踪很有用。建立一个关于粗略程度的约定是有帮助的。比如说。 813 | 814 | - 在 `V(1) `写少量的额外信息 815 | - 在 `V(2)`中跟踪更多信息 816 | - 在 `V(3)`中倾倒大量的内部状态。 817 | 818 | 为了尽量减少粗略记录的成本,你应该确保即使在 `log.V` 关闭的情况下也不要意外地调用昂贵的函数。`log.V`提供两个API。更方便的那个带有这种意外支出的风险。如有疑问,请使用稍显粗略的风格。 819 | 820 | ```go 821 | // Good: 822 | for _, sql := range queries { 823 | log.V(1).Infof("Handling %v", sql) 824 | if log.V(2) { 825 | log.Infof("Handling %v", sql.Explain()) 826 | } 827 | sql.Run(...) 828 | } 829 | ``` 830 | 831 | ```go 832 | // Bad: 833 | // sql.Explain called even when this log is not printed. 834 | log.V(2).Infof("Handling %v", sql.Explain()) 835 | ``` 836 | 837 | ### 程序初始化 838 | 839 | 程序的初始化错误(如坏的标志和配置)应该向上传播到`main`,它应该调用`log.Exit`,并说明如何修复错误。在这些情况下,一般不应使用`log.Fatal`,因为指向检查的堆栈跟踪不可能像人工生成的可操作信息那样有用。 840 | 841 | ### 程序检查和 panic 842 | 843 | 正如[反对 panic 的决定](https://gocn.github.io/styleguide/docs/03-decisions/#不要-panic)中所述,标准的错误处理应该围绕错误返回值进行结构化。库应该倾向于向调用者返回错误,而不是中止程序,特别是对于瞬时错误。 844 | 845 | 偶尔有必要对一个不变量进行一致性检查,如果违反了这个不变量,就终止程序。一般来说,只有当不变量检查失败意味着内部状态已经无法恢复时,才会这样做。在谷歌代码库中,最可靠的方法是调用`log.Fatal`。在这种情况下使用`panic`是不可靠的,因为延迟函数有可能会出现死锁或进一步破坏内部或外部状态。 846 | 847 | 同样,抵制恢复 panic 以避免崩溃的诱惑,因为这样做可能导致传播损坏的状态。你离 panic 越远,你对程序的状态就越不了解,它可能持有锁或其他资源。然后,程序可以发展出其他意想不到的故障模式,使问题更加难以诊断。与其试图在代码中处理意外的 panic,不如使用监控工具来浮现出意外的故障,并高度优先修复相关的错误。 848 | 849 | **注意:**标准的[`net/http`服务器](https://pkg.go.dev/net/http#Server)违反了这个建议,从请求处理程序中恢复 panic。有经验的 Go 工程师的共识是,这是一个历史性的错误。如果你对其他语言的应用服务器的日志进行采样,通常会发现有大量的堆栈轨迹没有被处理。在你的服务器中应避免这种陷阱。 850 | 851 | ### 何时 panic 852 | 853 | 标准库对 API 的误用感到 panic。例如,[`reflect`](https://pkg.go.dev/reflect)在许多情况下,如果一个值的访问方式表明它被误读,就会发出 panic。这类似于对核心语言错误的 panic,如访问一个越界的 slice 元素。代码审查和测试应该发现这样的错误,这些错误预计不会出现在生产代码中。这些 panic 作为不依赖库的不变性检查,因为标准库不能访问谷歌代码库使用的[levelled `log`](https://gocn.github.io/styleguide/docs/03-decisions/#日志) 包。 854 | 855 | 另一种情况下,panic 可能是有用的,尽管不常见,是作为一个包的内部实现细节,在调用链中总是有一个匹配的恢复。解析器和类似的深度嵌套、紧密耦合的内部函数组可以从这种设计中受益,其中管道错误返回增加了复杂性而没有价值。这种设计的关键属性是,这些 panic 永远不允许跨越包的边界,不构成包的API的一部分。这通常是通过一个顶层的延迟恢复来实现的,它将传播的 panic 转化为公共API表面的返回错误。 856 | 857 | 当编译器无法识别不可到达的代码时,例如使用像`log.Fatal`这样不会返回的函数时,也会使用 Panic: 858 | 859 | ```go 860 | // Good: 861 | func answer(i int) string { 862 | switch i { 863 | case 42: 864 | return "yup" 865 | case 54: 866 | return "base 13, huh" 867 | default: 868 | log.Fatalf("Sorry, %d is not the answer.", i) 869 | panic("unreachable") 870 | } 871 | } 872 | ``` 873 | 874 | [不要在标志被解析之前调用`log`函数](https://pkg.go.dev/github.com/golang/glog#pkg-overview)。如果你必须在 `init`函数中死亡,可以接受用 panic 来代替日志调用。 875 | 876 | ## 文档 877 | 878 | ### 公约 879 | 880 | 本节是对决策文件的[注释](https://gocn.github.io/styleguide/docs/03-decisions/#评论commentary)部分的补充。 881 | 以熟悉的风格记录的 Go 代码比那些错误记录或根本没有记录的代码更容易阅读,更不容易被误用。可运行的[实例](https://gocn.github.io/styleguide/docs/03-decisions/#示例examples)会出现在 Godoc 和代码搜索中,是解释如何使用你的代码的绝佳方式。 882 | 883 | #### 参数和配置 884 | 885 | 不是每个参数都必须在文档中列举出来。这适用于 886 | 887 | - 函数和方法参数 888 | - 结构字段 889 | - API的选项 890 | 891 | 将易出错或不明显的字段和参数记录下来,说说它们为什么有趣。 892 | 893 | 在下面的片段中,突出显示的注释对读者来说没有增加什么有用的信息: 894 | 895 | ```go 896 | // Bad: 897 | // Sprintf formats according to a format specifier and returns the resulting 898 | // string. 899 | // 900 | // format is the format, and data is the interpolation data. 901 | func Sprintf(format string, data ...interface{}) string 902 | ``` 903 | 904 | 然而,这个片段展示了一个与之前类似的代码场景,其中评论反而说明了一些不明显或对读者有实质性帮助的东西: 905 | 906 | ```go 907 | // Good: 908 | // Sprintf formats according to a format specifier and returns the resulting 909 | // string. 910 | // 911 | // The provided data is used to interpolate the format string. If the data does 912 | // not match the expected format verbs or the amount of data does not satisfy 913 | // the format specification, the function will inline warnings about formatting 914 | // errors into the output string as described by the Format errors section 915 | // above. 916 | func Sprintf(format string, data ...interface{}) string 917 | ``` 918 | 919 | 在选择文档的内容和深度时,要考虑到你可能的受众。维护者、新加入团队的人、外部用户,甚至是六个月后的你,可能会与你第一次来写文档时的想法略有不同的信息。 920 | 921 | 也请参见。 922 | 923 | - [GoTip #41: 识别函数调用参数](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 924 | - [GoTip #51: 配置的模式](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 925 | 926 | #### 上下文 927 | 928 | 这意味着取消一个上下文参数会中断提供给它的函数。如果该函数可以返回一个错误,习惯上是`ctx.Err()`。 929 | 930 | 这个事实不需要重述: 931 | 932 | ```go 933 | // Bad: 934 | // Run executes the worker's run loop. 935 | // 936 | // The method will process work until the context is cancelled and accordingly 937 | // returns an error. 938 | func (Worker) Run(ctx context.Context) error 939 | ``` 940 | 941 | 因为这句话是隐含的,所以下面的说法更好: 942 | 943 | ```go 944 | // Good: 945 | // Run executes the worker's run loop. 946 | func (Worker) Run(ctx context.Context) error 947 | ``` 948 | 949 | 如果上下文行为是不同的或不明显的,应该明确地记录下来: 950 | 951 | - 如果函数在取消上下文时返回一个除`ctx.Err()`以外的错误: 952 | 953 | ```go 954 | // Good: 955 | // Run executes the worker's run loop. 956 | // 957 | // If the context is cancelled, Run returns a nil error. 958 | func (Worker) Run(ctx context.Context) error 959 | ``` 960 | 961 | - 如果该功能有其他机制,可能会中断它或影响其生命周期: 962 | 963 | ```go 964 | // Good: 965 | // Run executes the worker's run loop. 966 | // 967 | // Run processes work until the context is cancelled or Stop is called. 968 | // Context cancellation is handled asynchronously internally: run may return 969 | // before all work has stopped. The Stop method is synchronous and waits 970 | // until all operations from the run loop finish. Use Stop for graceful 971 | // shutdown. 972 | func (Worker) Run(ctx context.Context) error 973 | 974 | func (Worker) Stop() 975 | ``` 976 | 977 | - 如果该函数对上下文的生命周期、脉络或附加值有特殊期望: 978 | 979 | ```go 980 | // Good: 981 | // NewReceiver starts receiving messages sent to the specified queue. 982 | // The context should not have a deadline. 983 | func NewReceiver(ctx context.Context) *Receiver 984 | 985 | // Principal returns a human-readable name of the party who made the call. 986 | // The context must have a value attached to it from security.NewContext. 987 | func Principal(ctx context.Context) (name string, ok bool) 988 | ``` 989 | 990 | **警告:**避免设计对其调用者提出这种要求(比如上下文没有截止日期)的API。以上只是一个例子,说明在无法避免的情况下该如何记录,而不是对该模式的认可。 991 | 992 | #### 并发 993 | 994 | Go 用户认为概念上的只读操作对于并发使用是安全的,不需要额外的同步。 995 | 996 | 在这个 Godoc 中,关于并发性的额外说明可以安全地删除: 997 | 998 | ```go 999 | // Len returns the number of bytes of the unread portion of the buffer; 1000 | // b.Len() == len(b.Bytes()). 1001 | // 1002 | // It is safe to be called concurrently by multiple goroutines. 1003 | func (*Buffer) Len() int 1004 | ``` 1005 | 1006 | 然而,变异操作并不被认为对并发使用是安全的,需要用户考虑同步化。 1007 | 同样地,这里可以安全地删除关于并发的额外注释: 1008 | 1009 | ```go 1010 | // Grow grows the buffer's capacity. 1011 | // 1012 | // It is not safe to be called concurrently by multiple goroutines. 1013 | func (*Buffer) Grow(n int) 1014 | ``` 1015 | 1016 | 强烈鼓励在以下情况下提供文档: 1017 | 1018 | - 目前还不清楚该操作是只读的还是变异的。 1019 | 1020 | ```go 1021 | // Good: 1022 | package lrucache 1023 | 1024 | // Lookup returns the data associated with the key from the cache. 1025 | // 1026 | // This operation is not safe for concurrent use. 1027 | func (*Cache) Lookup(key string) (data []byte, ok bool) 1028 | ``` 1029 | 1030 | 为什么?在查找密钥时,缓存命中会在内部突变一个 LRU 缓存。这一点是如何实现的,对所有的读者来说可能并不明显。 1031 | 1032 | - 同步是由 API 提供的 1033 | 1034 | ```go 1035 | // Good: 1036 | package fortune_go_proto 1037 | 1038 | // NewFortuneTellerClient returns an *rpc.Client for the FortuneTeller service. 1039 | // It is safe for simultaneous use by multiple goroutines. 1040 | func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient 1041 | ``` 1042 | 1043 | 为什么?Stubby 提供了同步性。 1044 | 1045 | **注意:**如果API是一个类型,并且 API 完整地提供了同步,传统上只有类型定义记录了语义。 1046 | 1047 | - 该 API 消费用户实现的接口类型,并且该接口的消费者有特殊的并发性要求: 1048 | 1049 | ```go 1050 | // Good: 1051 | package health 1052 | 1053 | // A Watcher reports the health of some entity (usually a backen service). 1054 | // 1055 | // Watcher methods are safe for simultaneous use by multiple goroutines. 1056 | type Watcher interface { 1057 | // Watch sends true on the passed-in channel when the Watcher's 1058 | // status has changed. 1059 | Watch(changed chan<- bool) (unwatch func()) 1060 | 1061 | // Health returns nil if the entity being watched is healthy, or a 1062 | // non-nil error explaining why the entity is not healthy. 1063 | Health() error 1064 | } 1065 | ``` 1066 | 1067 | 为什么?一个 API 是否能被多个 goroutines 安全使用是其契约的一部分。 1068 | 1069 | #### 清理 1070 | 1071 | 记录 API 的任何明确的清理要求。否则,调用者不会正确使用 API,导致资源泄漏和其他可能的错误。 1072 | 1073 | 调出由调用者决定的清理工作: 1074 | 1075 | ```go 1076 | // Good: 1077 | // NewTicker returns a new Ticker containing a channel that will send the 1078 | // current time on the channel after each tick. 1079 | // 1080 | // Call Stop to release the Ticker's associated resources when done. 1081 | func NewTicker(d Duration) *Ticker 1082 | 1083 | func (*Ticker) Stop() 1084 | ``` 1085 | 1086 | 如果有可能不清楚如何清理资源,请解释如何清理: 1087 | 1088 | ```go 1089 | // Good: 1090 | // Get issues a GET to the specified URL. 1091 | // 1092 | // When err is nil, resp always contains a non-nil resp.Body. 1093 | // Caller should close resp.Body when done reading from it. 1094 | // 1095 | // resp, err := http.Get("http://example.com/") 1096 | // if err != nil { 1097 | // // handle error 1098 | // } 1099 | // defer resp.Body.Close() 1100 | // body, err := io.ReadAll(resp.Body) 1101 | func (c *Client) Get(url string) (resp *Response, err error) 1102 | ``` 1103 | 1104 | ### 预览 1105 | 1106 | Go 的特点是有一个[文档服务器](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite)。建议在代码审查前和审查过程中预览你的代码产生的文档。这有助于验证[ godoc 格式化](https://gocn.github.io/styleguide/docs/04-best-practices/#godoc-格式化)是否正确呈现。 1107 | 1108 | ### Godoc 格式化 1109 | 1110 | [Godoc](https://pkg.go.dev/)为[格式化文档](https://go.dev/doc/comment)提供了一些特定的语法。 1111 | 1112 | - 段落之间需要有一个空行: 1113 | 1114 | ```go 1115 | // Good: 1116 | // LoadConfig reads a configuration out of the named file. 1117 | // 1118 | // See some/shortlink for config file format details. 1119 | ``` 1120 | 1121 | - 测试文件可以包含[可运行的例子](https://gocn.github.io/styleguide/docs/03-decisions/#示例examples),这些例子出现在 godoc 中相应的文档后面: 1122 | 1123 | ```go 1124 | // Good: 1125 | func ExampleConfig_WriteTo() { 1126 | cfg := &Config{ 1127 | Name: "example", 1128 | } 1129 | if err := cfg.WriteTo(os.Stdout); err != nil { 1130 | log.Exitf("Failed to write config: %s", err) 1131 | } 1132 | // Output: 1133 | // { 1134 | // "name": "example" 1135 | // } 1136 | } 1137 | ``` 1138 | 1139 | - 缩进的行数再加上两个空格,就可以将它们逐字排开: 1140 | 1141 | ```go 1142 | // Good: 1143 | // Update runs the function in an atomic transaction. 1144 | // 1145 | // This is typically used with an anonymous TransactionFunc: 1146 | // 1147 | // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { 1148 | // //... 1149 | // } 1150 | ``` 1151 | 1152 | 然而,请注意,把代码放在可运行的例子中,而不是把它放在注释中,往往会更合适。 1153 | 这种逐字格式化可以用于非godoc原生的格式化,如列表和表格: 1154 | 1155 | ```go 1156 | // Good: 1157 | // LoadConfig reads a configuration out of the named file. 1158 | // 1159 | // LoadConfig treats the following keys in special ways: 1160 | // "import" will make this configuration inherit from the named file. 1161 | // "env" if present will be populated with the system environment. 1162 | ``` 1163 | 1164 | - 一行以大写字母开始,除括号和逗号外不含标点符号,后面是另一个段落,其格式为标题: 1165 | 1166 | ```go 1167 | // Good: 1168 | // The following line is formatted as a heading. 1169 | // 1170 | // Using headings 1171 | // 1172 | // Headings come with autogenerated anchor tags for easy linking. 1173 | ``` 1174 | 1175 | 1176 | ### 信号增强 1177 | 1178 | 有时一行代码看起来很普通,但实际上并不普通。这方面最好的例子之一是`err == nil`的检查(因为`err != nil`更常见)。下面的两个条件检查很难区分: 1179 | 1180 | ```go 1181 | // Good: 1182 | if err := doSomething(); err != nil { 1183 | // ... 1184 | } 1185 | ``` 1186 | 1187 | ```go 1188 | // Bad: 1189 | if err := doSomething(); err == nil { 1190 | // ... 1191 | } 1192 | ``` 1193 | 1194 | 你可以通过添加评论来 "提高 "条件的信号: 1195 | 1196 | ```go 1197 | // Good: 1198 | if err := doSomething(); err == nil { // if NO error 1199 | // ... 1200 | } 1201 | ``` 1202 | 1203 | 该评论提请注意条件的不同。 1204 | 1205 | ## 变量声明 1206 | 1207 | ### 初始化 1208 | 1209 | 为了保持一致性,当用非零值初始化一个新的变量时,首选`:=`而不是`var`。 1210 | 1211 | ```go 1212 | // Good: 1213 | i := 42 1214 | // Bad: 1215 | var i = 42 1216 | ``` 1217 | 1218 | ### 非指针式零值 1219 | 1220 | 下面的声明使用[零值](https://golang.org/ref/spec#The_zero_value): 1221 | 1222 | ```go 1223 | // Good: 1224 | var ( 1225 | coords Point 1226 | magic [4]byte 1227 | primes []int 1228 | ) 1229 | ``` 1230 | 1231 | 当你想要传递一个空值**以供以后使用**时,你应该使用零值声明。使用带有显式初始化的复合字面会显得很笨重: 1232 | 1233 | ```go 1234 | // Bad: 1235 | var ( 1236 | coords = Point{X: 0, Y: 0} 1237 | magic = [4]byte{0, 0, 0, 0} 1238 | primes = []int(nil) 1239 | ) 1240 | ``` 1241 | 1242 | 零值声明的一个常见应用是当使用一个变量作为反序列化时的输出: 1243 | 1244 | ```go 1245 | // Good: 1246 | var coords Point 1247 | if err := json.Unmarshal(data, &coords); err != nil { 1248 | ``` 1249 | 1250 | 在你的结构体中,如果你需要一个[不得复制](https://gocn.github.io/styleguide/docs/03-decisions/#复制)的锁或其他字段,可以将其设为值类型以利用零值初始化。这确实意味着,现在必须通过指针而不是值来传递包含的类型。该类型的方法必须采用指针接收器。 1251 | 1252 | ```go 1253 | // Good: 1254 | type Counter struct { 1255 | // This field does not have to be "*sync.Mutex". However, 1256 | // users must now pass *Counter objects between themselves, not Counter. 1257 | mu sync.Mutex 1258 | data map[string]int64 1259 | } 1260 | 1261 | // Note this must be a pointer receiver to prevent copying. 1262 | func (c *Counter) IncrementBy(name string, n int64) 1263 | ``` 1264 | 1265 | 对复合体(如结构体和数组)的局部变量使用值类型是可以接受的,即使它们包含这种不可复制的字段。然而,如果复合体是由函数返回的,或者如果对它的所有访问最终都需要获取一个地址,那么最好在一开始就将变量声明为指针类型。同样地,protobufs 也应该被声明为指针类型。 1266 | 1267 | ```go 1268 | // Good: 1269 | func NewCounter(name string) *Counter { 1270 | c := new(Counter) // "&Counter{}" is also fine. 1271 | registerCounter(name, c) 1272 | return c 1273 | } 1274 | 1275 | var myMsg = new(pb.Bar) // or "&pb.Bar{}". 1276 | ``` 1277 | 1278 | 这是因为`*pb.Something`满足[`proto.Message`](https://pkg.go.dev/google.golang.org/protobuf/proto#Message)而`pb.Something`不满足。 1279 | 1280 | ```go 1281 | // Bad: 1282 | func NewCounter(name string) *Counter { 1283 | var c Counter 1284 | registerCounter(name, &c) 1285 | return &c 1286 | } 1287 | 1288 | var myMsg = pb.Bar{} 1289 | ``` 1290 | 1291 | > **重要的是:** Map 类型在被修改之前必须明确地初始化。然而,从零值 Map 中读取是完全可以的。 1292 | > 对于 map 和 slice 类型,如果代码对性能特别敏感,并且你事先知道大小,请参见[size hints](https://gocn.github.io/styleguide/docs/04-best-practices/#size-提示)部分。 1293 | 1294 | ### 复合字面量 1295 | 1296 | 以下是[复合字面量](https://golang.org/ref/spec#Composite_literals)的声明: 1297 | 1298 | ```go 1299 | // Good: 1300 | var ( 1301 | coords = Point{X: x, Y: y} 1302 | magic = [4]byte{'I', 'W', 'A', 'D'} 1303 | primes = []int{2, 3, 5, 7, 11} 1304 | captains = map[string]string{"Kirk": "James Tiberius", "Picard": "Jean-Luc"} 1305 | ) 1306 | ``` 1307 | 1308 | 当你知道初始元素或成员时,你应该使用复合字面量来声明一个值。 1309 | 1310 | 相比之下,与[零值初始化]相比,使用复合字面量声明空或无成员值可能会在视觉上产生噪音 1311 | 1312 | 当你需要一个指向零值的指针时,你有两个选择:空复合字面和`new`。两者都很好,但是`new`关键字可以提醒读者,如果需要一个非零值,这个复合字面量将不起作用: 1313 | 1314 | ```go 1315 | // Good: 1316 | var ( 1317 | buf = new(bytes.Buffer) // non-empty Buffers are initialized with constructors. 1318 | msg = new(pb.Message) // non-empty proto messages are initialized with builders or by setting fields one by one. 1319 | ) 1320 | ``` 1321 | 1322 | ### size 提示 1323 | 1324 | 以下是利用 size 提示来预分配容量的声明方式: 1325 | 1326 | ```go 1327 | // Good: 1328 | var ( 1329 | // Preferred buffer size for target filesystem: st_blksize. 1330 | buf = make([]byte, 131072) 1331 | // Typically process up to 8-10 elements per run (16 is a safe assumption). 1332 | q = make([]Node, 0, 16) 1333 | // Each shard processes shardSize (typically 32000+) elements. 1334 | seen = make(map[string]bool, shardSize) 1335 | ) 1336 | ``` 1337 | 1338 | 根据对代码及其集成的经验分析,对创建性能敏感和资源高效的代码,size 提示和预分配是重要的步骤。 1339 | 1340 | 大多数代码不需要 size 提示或预分配,可以允许运行时根据需要增长 slice 或 map。当最终大小已知时,预分配是可以接受的(例如,在 slice 或 map 之间转换时),但这不是一个可读性要求,而且在少数情况下可能不值得这样做。 1341 | 1342 | **警告:**预先分配比你需要的更多的内存,会在队列中浪费内存,甚至损害性能。如有疑问,请参阅[GoTip #3: Benchmarking Go Code](https://gocn.github.io/styleguide/docs/01-overview/#gotip)并默认为[零初始化](https://gocn.github.io/styleguide/docs/04-best-practices/#非指针式零值)或[复合字面量声明](https://gocn.github.io/styleguide/docs/04-best-practices/#复合字面量)。 1343 | 1344 | ### Channel 方向 1345 | 1346 | 尽可能地指定[ Channel 方向](https://go.dev/ref/spec#Channel_types)。 1347 | 1348 | ```go 1349 | // Good: 1350 | // sum computes the sum of all of the values. It reads from the channel until 1351 | // the channel is closed. 1352 | func sum(values <-chan int) int { 1353 | // ... 1354 | } 1355 | ``` 1356 | 1357 | 这可以防止在没有规范的情况下可能出现的随意编码错误。 1358 | 1359 | ```go 1360 | // Bad: 1361 | func sum(values chan int) (out int) { 1362 | for v := range values { 1363 | out += v 1364 | } 1365 | // values must already be closed for this code to be reachable, which means 1366 | // a second close triggers a panic. 1367 | close(values) 1368 | } 1369 | ``` 1370 | 1371 | 当方向被指定时,编译器会捕捉到像这样的简单错误。它还有助于向类型传达一种所有权的措施。 1372 | 也请看 Bryan Mills 的演讲 "重新思考经典的并发模式"。[PPT链接](https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view?usp=sharing) [视频链接](https://www.youtube.com/watch?v=5zXAHh5tJqQ)。 1373 | 1374 | ## 函数参数列表 1375 | 1376 | 不要让一个函数的签名变得太长。当一个函数中的参数越多,单个参数的作用就越不明确,同一类型的相邻参数就越容易混淆。有大量参数的函数不容易被记住,在调用点也更难读懂。 1377 | 1378 | 在设计 API 时,可以考虑将一个签名越来越复杂的高配置函数分割成几个更简单的函数。如果有必要的话,这些函数可以共享一个(未导出的)实现。 1379 | 1380 | 当一个函数需要许多输入时,可以考虑为一些参数引入一个 option 模式,或者采用更高级的变体选项技术。选择哪种策略的主要考虑因素应该是函数调用在所有预期的使用情况下看起来如何。 1381 | 1382 | 下面的建议主要适用于导出的 API,它比未导出的 API 的标准要高。这些技术对于你的用例可能是不必要的。使用你的判断,并平衡清晰性和最小机制的原则。 1383 | 1384 | 也请参见。[Go技巧#24:使用特定案例的结构](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 1385 | 1386 | ### option 模式 1387 | 1388 | option 模式是一种结构类型,它收集了一个函数或方法的部分或全部参数,然后作为最后一个参数传递给该函数或方法。(该结构只有在导出的函数中使用时,才应该导出)。 1389 | 1390 | 使用 option 模式有很多好处。 1391 | 1392 | - 结构体字面量包括每个参数的字段和值,这使得它们可以自己作为文档,并且更难被交换。 1393 | 1394 | - 不相关的或 "默认 "的字段可以被省略。 1395 | 1396 | - 调用者可以共享 option 模式,并编写帮助程序对其进行操作。 1397 | 1398 | - 与函数参数相比,结构体提供了更清晰的每个字段的文档。 1399 | 1400 | - option 模式可以随着时间的推移而增长,而不会影响到调用点。 1401 | 1402 | 下面是一个可以改进的函数的例子: 1403 | 1404 | ```go 1405 | // Bad: 1406 | func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { 1407 | // ... 1408 | } 1409 | ``` 1410 | 1411 | 上面的函数可以用一个 option 模式重写如下: 1412 | 1413 | ```go 1414 | // Good: 1415 | type ReplicationOptions struct { 1416 | Config *replicator.Config 1417 | PrimaryRegions []string 1418 | ReadonlyRegions []string 1419 | ReplicateExisting bool 1420 | OverwritePolicies bool 1421 | ReplicationInterval time.Duration 1422 | CopyWorkers int 1423 | HealthWatcher health.Watcher 1424 | } 1425 | 1426 | func EnableReplication(ctx context.Context, opts ReplicationOptions) { 1427 | // ... 1428 | } 1429 | ``` 1430 | 1431 | 然后,该函数可以在不同的包中被调用: 1432 | 1433 | ```go 1434 | // Good: 1435 | func foo(ctx context.Context) { 1436 | // Complex call: 1437 | storage.EnableReplication(ctx, storage.ReplicationOptions{ 1438 | Config: config, 1439 | PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, 1440 | ReadonlyRegions: []string{"us-east5", "us-central6"}, 1441 | OverwritePolicies: true, 1442 | ReplicationInterval: 1 * time.Hour, 1443 | CopyWorkers: 100, 1444 | HealthWatcher: watcher, 1445 | }) 1446 | 1447 | // Simple call: 1448 | storage.EnableReplication(ctx, storage.ReplicationOptions{ 1449 | Config: config, 1450 | PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, 1451 | }) 1452 | } 1453 | ``` 1454 | 1455 | **注意**:[option 模式中从不包含上下文](https://gocn.github.io/styleguide/docs/03-decisions/#上下文)。 1456 | 1457 | 当遇到以下某些情况时,通常首选 option 模式: 1458 | 1459 | - 所有调用者都需要指定一个或多个选项。 1460 | - 大量的调用者需要提供许多选项。 1461 | - 用户要调用的多个函数之间共享这些选项。 1462 | 1463 | ### 可变 option 模式 1464 | 1465 | 使用可变 option 模式,可以创建导出的函数,其返回的闭包可以传递给函数的[variadic(`...`)参数](https://golang.org/ref/spec#Passing_arguments_to_..._parameters)。该函数将选项的值作为其参数(如果有的话),而返回的闭包接受一个可变的引用(通常是一个指向结构体类型的指针),该引用将根据输入进行更新。 1466 | 1467 | 使用可变 option 模式可以提供很多好处。 1468 | 1469 | - 当不需要配置时,选项在调用点不占用空间。 1470 | - 选项仍然是值,所以调用者可以共享它们,编写帮助程序,并积累它们。 1471 | - 选项可以接受多个参数(例如:`cartesian.Translate(dx, dy int) TransformOption`)。 1472 | - 选项函数可以返回一个命名的类型,以便在 godoc 中把选项组合起来。 1473 | - 包可以允许(或阻止)第三方包定义(或不定义)自己的选项。 1474 | 1475 | **注意:**使用可变 option 模式需要大量的额外代码(见下面的例子),所以只有在好处大于坏处时才可以使用。 1476 | 1477 | 下面是一个可以改进的功能的例子: 1478 | 1479 | ```go 1480 | // Bad: 1481 | func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { 1482 | ... 1483 | } 1484 | ``` 1485 | 1486 | 上面的例子可以用可变 option 模式重写如下: 1487 | 1488 | ```go 1489 | // Good: 1490 | type replicationOptions struct { 1491 | readonlyCells []string 1492 | replicateExisting bool 1493 | overwritePolicies bool 1494 | replicationInterval time.Duration 1495 | copyWorkers int 1496 | healthWatcher health.Watcher 1497 | } 1498 | 1499 | // A ReplicationOption configures EnableReplication. 1500 | type ReplicationOption func(*replicationOptions) 1501 | 1502 | // ReadonlyCells adds additional cells that should additionally 1503 | // contain read-only replicas of the data. 1504 | // 1505 | // Passing this option multiple times will add additional 1506 | // read-only cells. 1507 | // 1508 | // Default: none 1509 | func ReadonlyCells(cells ...string) ReplicationOption { 1510 | return func(opts *replicationOptions) { 1511 | opts.readonlyCells = append(opts.readonlyCells, cells...) 1512 | } 1513 | } 1514 | 1515 | // ReplicateExisting controls whether files that already exist in the 1516 | // primary cells will be replicated. Otherwise, only newly-added 1517 | // files will be candidates for replication. 1518 | // 1519 | // Passing this option again will overwrite earlier values. 1520 | // 1521 | // Default: false 1522 | func ReplicateExisting(enabled bool) ReplicationOption { 1523 | return func(opts *replicationOptions) { 1524 | opts.replicateExisting = enabled 1525 | } 1526 | } 1527 | 1528 | // ... other options ... 1529 | 1530 | // DefaultReplicationOptions control the default values before 1531 | // applying options passed to EnableReplication. 1532 | var DefaultReplicationOptions = []ReplicationOption{ 1533 | OverwritePolicies(true), 1534 | ReplicationInterval(12 * time.Hour), 1535 | CopyWorkers(10), 1536 | } 1537 | 1538 | func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { 1539 | var options replicationOptions 1540 | for _, opt := range DefaultReplicationOptions { 1541 | opt(&options) 1542 | } 1543 | for _, opt := range opts { 1544 | opt(&options) 1545 | } 1546 | } 1547 | ``` 1548 | 1549 | 然后,该函数可以在不同的包中被调用: 1550 | 1551 | ```go 1552 | // Good: 1553 | func foo(ctx context.Context) { 1554 | // Complex call: 1555 | storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}, 1556 | storage.ReadonlyCells("ix", "gg"), 1557 | storage.OverwritePolicies(true), 1558 | storage.ReplicationInterval(1*time.Hour), 1559 | storage.CopyWorkers(100), 1560 | storage.HealthWatcher(watcher), 1561 | ) 1562 | 1563 | // Simple call: 1564 | storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}) 1565 | } 1566 | ``` 1567 | 1568 | 当遇到很多以下情况时,首选可变 option 模式: 1569 | 1570 | - 大多数调用者不需要指定任何选项。 1571 | - 大多数选项不经常使用。 1572 | - 有大量的选项。 1573 | - 选项需要参数。 1574 | - 选项可能会失败或设置错误(在这种情况下,选项函数会返回一个`错误')。 1575 | - 选项需要大量的文档,在一个结构中很难容纳。 1576 | - 用户或其他软件包可以提供自定义选项。 1577 | 1578 | 这种风格的选项应该接受参数,而不是在命名中标识来表示它们的价值;后者会使参数的动态组合变得更加困难。例如,二进制设置应该接受一个布尔值(例如,`rpc.FailFast(enable bool)`比`rpc.EnableFailFast()`更合适)。枚举的选项应该接受一个枚举的常数(例如`log.Format(log.Capacitor)`比`log.CapacitorFormat()`更好)。另一种方法使那些必须以编程方式选择传递哪些选项的用户更加困难;这种用户被迫改变参数的实际组成,而不是改变参数到选项。不要假设所有的用户都会知道全部的选项。 1579 | 1580 | 一般来说,option 应该被按顺序处理。如果有冲突或者一个非累积的选项被多次传递,将应用最后一个参数。 1581 | 1582 | 在这种模式下,选项函数的参数通常是未导出的,以限制选项只在包本身内定义。这是一个很好的默认值,尽管有时允许其他包定义选项也是合适的。 1583 | 1584 | 参见[Rob Pike 的原始博文](http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html)和[Dave Cheney的演讲](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis),以更深入地了解这些选项的使用方法。 1585 | 1586 | ## 复杂的命令行界面 1587 | 1588 | 一些程序希望为用户提供丰富的命令行界面,包括子命令。例如,`kubectl create`,`kubectl run`,以及许多其他的子命令都是由程序`kubectl`提供。至少有以下常用的库可以实现这个目的。 1589 | 1590 | 如果你没有偏好或者其他考虑因素相同,推荐使用[subcommands](https://pkg.go.dev/github.com/google/subcommands),因为它是最简单的,而且容易正确使用。然而,如果你需要它所不提供的不同功能,请挑选其他选项之一。 1591 | 1592 | - **[cobra](https://pkg.go.dev/github.com/spf13/cobra)** 1593 | * 习惯标志:getopt 1594 | * 在谷歌代码库之外很常见。 1595 | * 许多额外的功能。 1596 | * 使用中的陷阱(见下文)。 1597 | - **[subcommands](https://pkg.go.dev/github.com/google/subcommands)** 1598 | * 习惯标志:Go 1599 | * 简单且易于正确使用。 1600 | * 如果你不需要额外的功能,推荐使用。 1601 | 1602 | **警告**:cobra 命令函数应该使用 `cmd.Context()` 来获取上下文,而不是用 `context.Background` 来创建自己的根上下文。使用子命令包的代码已经将正确的上下文作为一个函数参数接收。 1603 | 1604 | 你不需要把每个子命令放在一个单独的包中,而且通常也没有必要这样做。应用与任何 Go 代码库相同的关于包边界的考虑。如果你的代码既可以作为库也可以作为二进制文件使用,通常将 CLI 代码和库分开是有好处的,使 CLI 只是其客户端中的一个。(这不是专门针对有子命令的CLI的,但在此提及,因为它经常出现。) 1605 | 1606 | ## 测试 1607 | 1608 | ### 把测试留给 `Test` 函数 1609 | 1610 | Go 区分了 "测试辅助函数 "和 "断言辅助函数"。 1611 | 1612 | - **测试辅助函数**就是做设置或清理任务的函数。所有发生在测试辅助函数中的故障都被认为是环境的故障(而不是来自被测试的代码)--例如,当一个测试数据库不能被启动,因为在这台机器上没有更多的空闲端口。对于这样的函数,调用`t.Helper`通常是合适的,[将其标记为测试辅助函数](https://gocn.github.io/styleguide/docs/03-decisions/#测试辅助函数)。参见 [测试辅助函数的错误处理](https://gocn.github.io/styleguide/docs/04-best-practices/#在测试辅助函数中处理错误) 了解更多细节。 1613 | - **断言辅助函数**是检查系统正确性的函数,如果没有达到预期,则测试失败。断言辅助函数在 Go 中[不被认为是常见用法](https://gocn.github.io/styleguide/docs/03-decisions/#断言库)。 1614 | 1615 | 测试的目的是报告被测试代码的通过/失败情况。测试失败的理想场所是在`Test`函数本身,因为这样可以确保[失败信息](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败)和测试逻辑是清晰的。 1616 | 1617 | 随着你的测试代码的增长,可能有必要将一些功能分解到独立的函数中。标准的软件工程考虑仍然适用,因为_测试代码仍然是代码_。如果这些功能不与测试框架交互,那么所有的常规规则都适用。然而,当通用代码与框架交互时,必须注意避免常见的陷阱,这些陷阱会导致语焉不详的失败信息和不可维护的测试。 1618 | 1619 | 如果许多独立的测试用例需要相同的验证逻辑,请以下列方式之一安排测试,而不是使用断言辅助函数或复杂的验证函数。 1620 | 1621 | - 在`Test`函数中内联逻辑(包括验证和失败),即使它是重复的。这在简单的情况下效果最好。 1622 | - 如果输入是类似的,可以考虑把它们统一到一个[表格驱动的测试](https://gocn.github.io/styleguide/docs/03-decisions/#表驱动测试),同时在循环中保持逻辑的内联。这有助于避免重复,同时在 "测试 "中保持验证和失败。 1623 | - 如果有多个调用者需要相同的验证功能,但表格测试不适合(通常是因为输入不够简单或验证需要作为操作序列的一部分),安排验证功能,使其返回一个值(通常是一个 "错误"),而不是接受一个 "testing.T "参数并使用它来让测试失败。在`测试`中使用逻辑来决定是否失败,并提供[有用的测试失败](https://gocn.github.io/styleguide/docs/03-decisions/#有用的测试失败)。你也可以创建测试辅助函数,以剔除常见的模板设置代码。 1624 | 1625 | 最后一点中概述的设计保持了正交性。例如,[`cmp` 包 ](https://pkg.go.dev/github.com/google/go-cmp/cmp)不是为了测试失败而设计的,而是为了比较(和差异)值。因此,它不需要知道进行比较的上下文,因为调用者可以提供这个。如果你的普通测试代码为你的数据类型提供了一个`cmp.Transformer`,这通常是最简单的设计。对于其他的验证,可以考虑返回一个`error`值。 1626 | 1627 | ```go 1628 | // Good: 1629 | // polygonCmp returns a cmp.Option that equates s2 geometry objects up to 1630 | // some small floating-point error. 1631 | func polygonCmp() cmp.Option { 1632 | return cmp.Options{ 1633 | cmp.Transformer("polygon", func(p *s2.Polygon) []*s2.Loop { return p.Loops() }), 1634 | cmp.Transformer("loop", func(l *s2.Loop) []s2.Point { return l.Vertices() }), 1635 | cmpopts.EquateApprox(0.00000001, 0), 1636 | cmpopts.EquateEmpty(), 1637 | } 1638 | } 1639 | 1640 | func TestFenceposts(t *testing.T) { 1641 | // This is a test for a fictional function, Fenceposts, which draws a fence 1642 | // around some Place object. The details are not important, except that 1643 | // the result is some object that has s2 geometry (github.com/golang/geo/s2) 1644 | got := Fencepost(tomsDiner, 1*meter) 1645 | if diff := cmp.Diff(want, got, polygonCmp()); diff != "" { 1646 | t.Errorf("Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\n%v", diff) 1647 | } 1648 | } 1649 | 1650 | func FuzzFencepost(f *testing.F) { 1651 | // Fuzz test (https://go.dev/doc/fuzz) for the same. 1652 | 1653 | f.Add(tomsDiner, 1*meter) 1654 | f.Add(school, 3*meter) 1655 | 1656 | f.Fuzz(func(t *testing.T, geo Place, padding Length) { 1657 | got := Fencepost(geo, padding) 1658 | // Simple reference implementation: not used in prod, but easy to 1659 | // reasonable and therefore useful to check against in random tests. 1660 | reference := slowFencepost(geo, padding) 1661 | 1662 | // In the fuzz test, inputs and outputs can be large so don't 1663 | // bother with printing a diff. cmp.Equal is enough. 1664 | if !cmp.Equal(got, reference, polygonCmp()) { 1665 | t.Errorf("Fencepost returned wrong placement") 1666 | } 1667 | }) 1668 | } 1669 | ``` 1670 | 1671 | `polygonCmp`函数对它的调用方式是不可知的;它不接受具体的输入类型,也不规定在两个对象不匹配的情况下该怎么做。因此,更多的调用者可以使用它。 1672 | 1673 | **注意:**在测试辅助函数和普通库代码之间有一个类比。库中的代码通常应该[不 panic](https://gocn.github.io/styleguide/docs/03-decisions/#不要-panic),除非在极少数情况下;从测试中调用的代码不应该停止测试,除非[继续进行没有意义](https://gocn.github.io/styleguide/docs/04-best-practices/#terror-vs-tfatal)。 1674 | 1675 | ### 设计可扩展的验证API 1676 | 1677 | 风格指南中关于测试的大部分建议都是关于测试你自己的代码。本节是关于如何为其他人提供设施来测试他们编写的代码,以确保它符合你的库的要求。 1678 | 1679 | #### 验收测试 1680 | 1681 | 这种测试被称为[验收测试](https://en.wikipedia.org/wiki/Acceptance_testing)。这种测试的前提是,使用测试的人不知道测试中发生的每一个细节;他们只是把输入交给测试机构来完成。这可以被认为是一种[控制反转](https://en.wikipedia.org/wiki/Inversion_of_control)的形式。 1682 | 1683 | 在典型的Go测试中,测试函数控制着程序流程,[无断言](https://gocn.github.io/styleguide/docs/03-decisions/#断言库)和[测试函数](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数)指南鼓励你保持这种方式。本节解释了如何以符合 Go 风格的方式来编写对这些测试的支持。 1684 | 1685 | 在深入探讨如何做之前,请看下面摘录的[`io/fs`](https://pkg.go.dev/io/fs)中的一个例子: 1686 | 1687 | ```go 1688 | type FS interface { 1689 | Open(name string) (File, error) 1690 | } 1691 | ``` 1692 | 1693 | 虽然存在众所周知的`fs.FS`的实现,但 Go 开发者可能会被期望编写一个。为了帮助验证用户实现的`fs.FS`是否正确,在[`testing/fstest`](https://pkg.go.dev/testing/fstest)中提供了一个通用库,名为[`fstest.TestFS`](https://pkg.go.dev/testing/fstest#TestFS)。这个API将实现作为一个黑箱来处理,以确保它维护了`io/fs`契约的最基本部分。 1694 | 1695 | #### 撰写验收测试 1696 | 1697 | 现在我们知道了什么是验收测试以及为什么要使用验收测试,让我们来探讨为`package chess`建立一个验收测试,这是一个用于模拟国际象棋游戏的包。`chess` 的用户应该实现 `chess.Player` 接口。这些实现是我们要验证的主要内容。我们的验收测试关注的是棋手的实现是否走了合法的棋,而不是这些棋是否聪明。 1698 | 1699 | 1.为验证行为创建一个新的包,[习惯上命名为](https://gocn.github.io/styleguide/docs/04-best-practices/#测试替身包和类型),在包名后面加上 "test "一词(例如:`chesstest`)。 1700 | 1701 | 2.创建执行验证的函数,接受被测试的实现作为参数,并对其进行练习: 1702 | 1703 | ```go 1704 | // ExercisePlayer tests a Player implementation in a single turn on a board. 1705 | // The board itself is spot checked for sensibility and correctness. 1706 | // 1707 | // It returns a nil error if the player makes a correct move in the context 1708 | // of the provided board. Otherwise ExercisePlayer returns one of this 1709 | // package's errors to indicate how and why the player failed the 1710 | // validation. 1711 | func ExercisePlayer(b *chess.Board, p chess.Player) error 1712 | ``` 1713 | 1714 | 测试应该注意哪些不变式被破坏,以及如何破坏。你的设计可以选择两种失败报告的原则: 1715 | 1716 | - **快速失败**:一旦实现违反了一个不变式,就返回一个错误。 1717 | 1718 | 这是最简单的方法,如果预计验收测试会快速执行,那么它的效果很好。简单的错误 [sentinels](https://gocn.github.io/styleguide/docs/01-overview/#gotip)和[自定义类型](https://gocn.github.io/styleguide/docs/01-overview/#gotip)在这里可以很容易地使用,反过来说,这使得测试验收测试变得很容易。 1719 | 1720 | ```go 1721 | for color, army := range b.Armies { 1722 | // The king should never leave the board, because the game ends at 1723 | // checkmate. 1724 | if army.King == nil { 1725 | return &MissingPieceError{Color: color, Piece: chess.King} 1726 | } 1727 | } 1728 | ``` 1729 | 1730 | - **集合所有的失败**:收集所有的失败,并报告它们。 1731 | 这种方法类似于[keep going](https://gocn.github.io/styleguide/docs/03-decisions/#测试继续进行)的指导,如果验收测试预计会执行得很慢,这种方法可能更可取。 1732 | 1733 | 你如何聚集故障,应该由你是否想让用户或你自己有能力询问个别故障(例如,为你测试你的验收测试)来决定的。下面演示了使用一个[自定义错误类型](https://gocn.github.io/styleguide/docs/01-overview/#gotip),[聚合错误](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。 1734 | 1735 | ```go 1736 | var badMoves []error 1737 | 1738 | move := p.Move() 1739 | if putsOwnKingIntoCheck(b, move) { 1740 | badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move}) 1741 | } 1742 | 1743 | if len(badMoves) > 0 { 1744 | return SimulationError{BadMoves: badMoves} 1745 | } 1746 | return nil 1747 | ``` 1748 | 1749 | 1750 | 验收测试应该遵守 [keep going](https://gocn.github.io/styleguide/docs/03-decisions/#测试继续进行) 的指导,不调用`t.Fatal`,除非测试检测到被测试系统中的不变量损坏。 1751 | 例如,`t.Fatal`应该保留给特殊情况,如[设置失败](https://gocn.github.io/styleguide/docs/04-best-practices/#在测试辅助函数中处理错误),像往常一样: 1752 | 1753 | ```go 1754 | func ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error { 1755 | t.Helper() 1756 | 1757 | if cfg.Simulation == Modem { 1758 | conn, err := modempool.Allocate() 1759 | if err != nil { 1760 | t.Fatalf("no modem for the opponent could be provisioned: %v", err) 1761 | } 1762 | t.Cleanup(func() { modempool.Return(conn) }) 1763 | } 1764 | // Run acceptance test (a whole game). 1765 | } 1766 | ``` 1767 | 1768 | 这种技术可以帮助你创建简明、规范的验证。但不要试图用它来绕过[断言指南](https://gocn.github.io/styleguide/docs/03-decisions/#断言库)。 1769 | 最终产品应该以类似这样的形式提供给终端用户: 1770 | 1771 | ```go 1772 | // Good: 1773 | package deepblue_test 1774 | 1775 | import ( 1776 | "chesstest" 1777 | "deepblue" 1778 | ) 1779 | 1780 | func TestAcceptance(t *testing.T) { 1781 | player := deepblue.New() 1782 | err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player) 1783 | if err != nil { 1784 | t.Errorf("deepblue player failed acceptance test: %v", err) 1785 | } 1786 | } 1787 | ``` 1788 | 1789 | ### 使用真正的传输工具 1790 | 1791 | 当测试组件集成时,特别是 HTTP 或 RPC 被用作组件之间的底层传输时,最好使用真正的底层传输来连接到测试版本的后端。 1792 | 1793 | 例如,假设你要测试的代码(有时被称为 "被测系统 "或SUT)与实现[长期运行操作](https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning) API 的后端交互。为了测试你的 SUT,使用一个真正的[OperationsClient](https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsClient),它连接到[OperationsServer](https://en.wikipedia.org/wiki/Test_double)的[替身测试](例如,mock、stub或fake)上。 1794 | 1795 | 为了确保测试代码贴切于生产环境,相对于使用手工实现的客户端,我们更加推荐使用生产的客户端和专用的测试服务器来模拟生产环境的复杂性。 1796 | 1797 | **提示:**在可能的情况下,使用由被测服务的作者提供的测试库。 1798 | 1799 | ### `t.Error` vs. `t.Fatal` 1800 | 1801 | 正如在[执行策略](https://gocn.github.io/styleguide/docs/03-decisions/#测试继续进行)中讨论的一样, 测试过程不应该在遇到问题的地方中断。 1802 | 1803 | 然而,有些情况需要终止当前测试。当某些测试需要标记失败时,调用t.Fatal是合适的,特别是在 使用测试辅助函数时,没有它,你就不能运行测试的其余部分。在表格驱动的测试中,t.Fatal适合于在测试在进入循环之前整个测试函数标记为失败状态。它只会影响整个测试列表中被标记为失败的测试函数不能继续向前推进, 而不会影响其他的测试函数, 所以, 错误报告应该像下面这样: 1804 | 1805 | - 如果你不使用 `t.Run子` 测试,使用 `t.Error`,后面跟一个 `continue` 语句,继续到下一个测试项。 1806 | - 如果你使用子测试(并且你在调用`t.Run`时),使用`t.Fatal`,结束当前子测试,并允许你的测试用例进入下一个子测试。 1807 | 1808 | **警告:**调用和`t.Fatal`和类似函数并不总是安全的。[更多细节在这里](https://gocn.github.io/styleguide/docs/04-best-practices/#不要从独立的-goroutines-中调用tfatal)。 1809 | 1810 | ### 在测试辅助函数中处理错误 1811 | 1812 | **注意:**本节讨论的[测试辅助函数](https://gocn.github.io/styleguide/docs/03-decisions/#测试辅助函数)是 Go 使用的术语:这些函数用于准备测试环境和清理测试现场,而不是普通的断言设施。更多的讨论请参见 [test functions](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数) 部分。 1813 | 1814 | 由测试辅助函数执行的操作有时会失败。例如,设置一个带有文件的目录涉及到 I/O,这可能会失败。当测试辅助函数失败时,它们的失败往往标志着测试不能继续,因为一个设置的前提条件失败了。当这种情况发生时,最好在辅助函数中调用一个`Fatal`函数: 1815 | 1816 | ```go 1817 | // Good: 1818 | func mustAddGameAssets(t *testing.T, dir string) { 1819 | t.Helper() 1820 | if err := os.WriteFile(path.Join(dir, "pak0.pak"), pak0, 0644); err != nil { 1821 | t.Fatalf("Setup failed: could not write pak0 asset: %v", err) 1822 | } 1823 | if err := os.WriteFile(path.Join(dir, "pak1.pak"), pak1, 0644); err != nil { 1824 | t.Fatalf("Setup failed: could not write pak1 asset: %v", err) 1825 | } 1826 | } 1827 | ``` 1828 | 1829 | 这就使调用测试辅助函数返回错误给测试本身更清晰: 1830 | 1831 | ```go 1832 | // Bad: 1833 | func addGameAssets(t *testing.T, dir string) error { 1834 | t.Helper() 1835 | if err := os.WriteFile(path.Join(d, "pak0.pak"), pak0, 0644); err != nil { 1836 | return err 1837 | } 1838 | if err := os.WriteFile(path.Join(d, "pak1.pak"), pak1, 0644); err != nil { 1839 | return err 1840 | } 1841 | return nil 1842 | } 1843 | ``` 1844 | 1845 | **警告:**调用和 `t.Fatal`类似函数并不总是安全的。点击这里查看[更多细节](https://gocn.github.io/styleguide/docs/04-best-practices/#不要从独立的-goroutines-中调用tfatal)。 1846 | 1847 | 失败信息应该包括对错误的详细描述信息。这一点很重要,因为你可能会向许多用户提供测试 API,特别是在测试辅助函数中产生错误的场景增多时。用户应该知道在哪里,以及为什么产生错误。 1848 | 1849 | **提示:** Go 1.14引入了一个[`t.Cleanup`](https://pkg.go.dev/testing#T.Cleanup)函数,可以用来注册清理函数,在你的测试完成后运行。该函数也适用于测试辅助函数。参见 [GoTip #4: Cleaning Up Your Tests](https://gocn.github.io/styleguide/docs/01-overview/#gotip) 以获得简化测试辅助程序的指导。 1850 | 1851 | 下面是一个名为`paint_test.go`的虚构文件中的片段,演示了`(*testing.T).Helper`如何影响 Go 测试中的失败报告: 1852 | 1853 | ```go 1854 | package paint_test 1855 | 1856 | import ( 1857 | "fmt" 1858 | "testing" 1859 | ) 1860 | 1861 | func paint(color string) error { 1862 | return fmt.Errorf("no %q paint today", color) 1863 | } 1864 | 1865 | func badSetup(t *testing.T) { 1866 | // This should call t.Helper, but doesn't. 1867 | if err := paint("taupe"); err != nil { 1868 | t.Fatalf("could not paint the house under test: %v", err) // line 15 1869 | } 1870 | } 1871 | 1872 | func mustGoodSetup(t *testing.T) { 1873 | t.Helper() 1874 | if err := paint("lilac"); err != nil { 1875 | t.Fatalf("could not paint the house under test: %v", err) 1876 | } 1877 | } 1878 | 1879 | func TestBad(t *testing.T) { 1880 | badSetup(t) 1881 | // ... 1882 | } 1883 | 1884 | func TestGood(t *testing.T) { 1885 | mustGoodSetup(t) // line 32 1886 | // ... 1887 | } 1888 | ``` 1889 | 1890 | 下面是运行该输出的一个例子。请注意突出显示的文本和它的不同之处: 1891 | 1892 | ```go 1893 | === RUN TestBad 1894 | paint_test.go:15: could not paint the house under test: no "taupe" paint today 1895 | --- FAIL: TestBad (0.00s) 1896 | === RUN TestGood 1897 | paint_test.go:32: could not paint the house under test: no "lilac" paint today 1898 | --- FAIL: TestGood (0.00s) 1899 | FAIL 1900 | ``` 1901 | 1902 | `paint_test.go:15`的错误是指在`badSetup`中失败的设置函数的那一行: 1903 | `t.Fatalf("could not paint the house under test: %v", err)` 1904 | 而`paint_test.go:32`指的是在`TestGood`中失败的那一行测试: 1905 | `goodSetup(t)`。 1906 | 1907 | 正确地使用`(*testing.T).Helper`可以更好地归纳出失败的位置,当: 1908 | 1909 | - 辅助函数数量增加 1910 | - 在辅助函数中使用其他的辅助函数 1911 | - 测试函数使用辅助函数的数量增加 1912 | 1913 | **提示:**如果一个辅助函数调用`(*testing.T).Error`或`(*testing.T).Fatal`,在格式字符串中提供一些上下文,以帮助确定出错的原因。 1914 | 1915 | **提示:**如果一个辅助函数没有做任何事情会导致测试失败,那么它就不需要调用`t.Helper`。通过从函数参数列表中删除`t`来简化其签名。 1916 | 1917 | ### 不要从独立的 goroutines 中调用`t.Fatal` 1918 | 1919 | 正如[package testing](https://pkg.go.dev/testing#T)中记载的那样,在测试函数或子测试函数之外的任何 goroutine 中调用 `t.FailNow`,`t.Fatal` 等都是不正确的。如果你的测试启动了新的 goroutine,它们不能从这些 goroutine 内部调用这些函数。 1920 | 1921 | [测试辅助函数](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数)通常不会从新的 goroutine 发出失败信号,因此它们使用`t.Fatal`是完全正确的。如果有疑问,可以调用 t.Error 并返回。 1922 | 1923 | ```go 1924 | // Good: 1925 | func TestRevEngine(t *testing.T) { 1926 | engine, err := Start() 1927 | if err != nil { 1928 | t.Fatalf("Engine failed to start: %v", err) 1929 | } 1930 | 1931 | num := 11 1932 | var wg sync.WaitGroup 1933 | wg.Add(num) 1934 | for i := 0; i < num; i++ { 1935 | go func() { 1936 | defer wg.Done() 1937 | if err := engine.Vroom(); err != nil { 1938 | // This cannot be t.Fatalf. 1939 | t.Errorf("No vroom left on engine: %v", err) 1940 | return 1941 | } 1942 | if rpm := engine.Tachometer(); rpm > 1e6 { 1943 | t.Errorf("Inconceivable engine rate: %d", rpm) 1944 | } 1945 | }() 1946 | } 1947 | wg.Wait() 1948 | 1949 | if seen := engine.NumVrooms(); seen != num { 1950 | t.Errorf("engine.NumVrooms() = %d, want %d", seen, num) 1951 | } 1952 | } 1953 | ``` 1954 | 1955 | 在测试或子测试中添加`t.Parallel`并不会使调用`t.Fatal`变得不安全。 1956 | 1957 | 当所有对 `testing` API 的调用都在 [test function](https://gocn.github.io/styleguide/docs/04-best-practices/#把测试留给-test-函数) 中时,通常很容易发现不正确的用法,因为`go`关键字是显而易见的。传递`testing.T`参数会使跟踪这种用法更加困难。通常,传递这些参数的原因是为了引入一个测试辅助函数,而这些测试辅助函数不应该依赖于被测系统。因此,如果一个测试辅助函数[注册了一个致命的测试失败](https://gocn.github.io/styleguide/docs/04-best-practices/#在测试辅助函数中处理错误),它可以而且应该从测试的 goroutine 中这样做。 1958 | 1959 | ### 对结构字使用字段标签 1960 | 1961 | 在表格驱动的测试中,最好为每个测试用例指定密钥。当测试用例覆盖了大量的垂直空间(例如,超过20-30行),当有相同类型的相邻字段,以及当你希望省略具有零值的字段时,这是有帮助的。比如说: 1962 | 1963 | ```go 1964 | // Good: 1965 | tests := []struct { 1966 | foo *pb.Foo 1967 | bar *pb.Bar 1968 | want string 1969 | }{ 1970 | { 1971 | foo: pb.Foo_builder{ 1972 | Name: "foo", 1973 | // ... 1974 | }.Build(), 1975 | bar: pb.Bar_builder{ 1976 | Name: "bar", 1977 | // ... 1978 | }.Build(), 1979 | want: "result", 1980 | }, 1981 | } 1982 | ``` 1983 | 1984 | ### 保持设置代码在特定的测试范围内 1985 | 1986 | 在可能的情况下,资源和依赖关系的设置应该尽可能地与具体的测试用例密切相关。例如,给定一个设置函数: 1987 | 1988 | ```go 1989 | // mustLoadDataSet loads a data set for the tests. 1990 | // 1991 | // This example is very simple and easy to read. Often realistic setup is more 1992 | // complex, error-prone, and potentially slow. 1993 | func mustLoadDataset(t *testing.T) []byte { 1994 | t.Helper() 1995 | data, err := os.ReadFile("path/to/your/project/testdata/dataset") 1996 | 1997 | if err != nil { 1998 | t.Fatalf("could not load dataset: %v", err) 1999 | } 2000 | return data 2001 | } 2002 | ``` 2003 | 2004 | 在需要的测试函数中明确调用`mustLoadDataset`: 2005 | 2006 | ```Go 2007 | // Good: 2008 | func TestParseData(t *testing.T) { 2009 | data := mustLoadDataset(t) 2010 | parsed, err := ParseData(data) 2011 | if err != nil { 2012 | t.Fatalf("unexpected error parsing data: %v", err) 2013 | } 2014 | want := &DataTable{ /* ... */ } 2015 | if got := parsed; !cmp.Equal(got, want) { 2016 | t.Errorf("ParseData(data) = %v, want %v", got, want) 2017 | } 2018 | } 2019 | 2020 | func TestListContents(t *testing.T) { 2021 | data := mustLoadDataset(t) 2022 | contents, err := ListContents(data) 2023 | if err != nil { 2024 | t.Fatalf("unexpected error listing contents: %v", err) 2025 | } 2026 | want := []string{ /* ... */ } 2027 | if got := contents; !cmp.Equal(got, want) { 2028 | t.Errorf("ListContents(data) = %v, want %v", got, want) 2029 | } 2030 | } 2031 | 2032 | func TestRegression682831(t *testing.T) { 2033 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2034 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2035 | } 2036 | } 2037 | ``` 2038 | 2039 | 测试函数`TestRegression682831`不使用数据集,因此不调用`mustLoadDataset`,这可能会很慢且容易失败: 2040 | 2041 | ```go 2042 | // Bad: 2043 | var dataset []byte 2044 | 2045 | func TestParseData(t *testing.T) { 2046 | // As documented above without calling mustLoadDataset directly. 2047 | } 2048 | 2049 | func TestListContents(t *testing.T) { 2050 | // As documented above without calling mustLoadDataset directly. 2051 | } 2052 | 2053 | func TestRegression682831(t *testing.T) { 2054 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2055 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2056 | } 2057 | } 2058 | 2059 | func init() { 2060 | dataset = mustLoadDataset() 2061 | } 2062 | ``` 2063 | 2064 | 用户希望在与其他函数隔离的情况下运行一个函数,不应受到这些因素的影响: 2065 | 2066 | ```sh 2067 | # No reason for this to perform the expensive initialization. 2068 | $ go test -run TestRegression682831 2069 | ``` 2070 | 2071 | #### 何时使用自定义的 `TestMain` 入口点 2072 | 2073 | 如果**包中的所有测试**都需要共同设置,并且**设置需要拆解**,你可以使用[自定义测试主入口](https://golang.org/pkg/testing/#hdr-Main)。如果测试用例所需的资源的设置特别昂贵,而且成本应该被摊销,就会发生这种情况。通常情况下,你在这一点上已经从测试套件中提取了任何无关的测试。它通常只用于[功能测试](https://en.wikipedia.org/wiki/Functional_testing)。 2074 | 2075 | 使用自定义的 `TestMain` **不应该是你的首选**,因为要正确使用它,必须要有足够的谨慎。首先考虑[_摊销普通测试设置_](https://gocn.github.io/styleguide/docs/04-best-practices/#摊销共同测试设置)部分的解决方案或普通的[测试辅助函数](https://gocn.github.io/styleguide/docs/04-best-practices/#保持设置代码在特定的测试范围内)是否足以满足你的需求。 2076 | 2077 | ```go 2078 | // Good: 2079 | var db *sql.DB 2080 | 2081 | func TestInsert(t *testing.T) { /* omitted */ } 2082 | 2083 | func TestSelect(t *testing.T) { /* omitted */ } 2084 | 2085 | func TestUpdate(t *testing.T) { /* omitted */ } 2086 | 2087 | func TestDelete(t *testing.T) { /* omitted */ } 2088 | 2089 | // runMain sets up the test dependencies and eventually executes the tests. 2090 | // It is defined as a separate function to enable the setup stages to clearly 2091 | // defer their teardown steps. 2092 | func runMain(ctx context.Context, m *testing.M) (code int, err error) { 2093 | ctx, cancel := context.WithCancel(ctx) 2094 | defer cancel() 2095 | 2096 | d, err := setupDatabase(ctx) 2097 | if err != nil { 2098 | return 0, err 2099 | } 2100 | defer d.Close() // Expressly clean up database. 2101 | db = d // db is defined as a package-level variable. 2102 | 2103 | // m.Run() executes the regular, user-defined test functions. 2104 | // Any defer statements that have been made will be run after m.Run() 2105 | // completes. 2106 | return m.Run(), nil 2107 | } 2108 | 2109 | func TestMain(m *testing.M) { 2110 | code, err := runMain(context.Background(), m) 2111 | if err != nil { 2112 | // Failure messages should be written to STDERR, which log.Fatal uses. 2113 | log.Fatal(err) 2114 | } 2115 | // NOTE: defer statements do not run past here due to os.Exit 2116 | // terminating the process. 2117 | os.Exit(code) 2118 | } 2119 | ``` 2120 | 2121 | 理想情况下,一个测试用例在自身的调用和其他测试用例之间是密封的。 2122 | 2123 | 至少要确保单个测试用例重置他们所修改的任何全局状态,如果他们已经这样做了(例如,如果测试是与外部数据库一起工作)。 2124 | 2125 | #### 摊销共同测试设置 2126 | 2127 | 如果普通设置中存在以下情况,使用 `sync.Once` 可能是合适的,尽管不是必须的。 2128 | 2129 | - 它很昂贵。 2130 | - 它只适用于某些测试。 2131 | - 它不需要拆解。 2132 | 2133 | ```go 2134 | // Good: 2135 | var dataset struct { 2136 | once sync.Once 2137 | data []byte 2138 | err error 2139 | } 2140 | 2141 | func mustLoadDataset(t *testing.T) []byte { 2142 | t.Helper() 2143 | dataset.once.Do(func() { 2144 | data, err := os.ReadFile("path/to/your/project/testdata/dataset") 2145 | // dataset is defined as a package-level variable. 2146 | dataset.data = data 2147 | dataset.err = err 2148 | }) 2149 | if err := dataset.err; err != nil { 2150 | t.Fatalf("could not load dataset: %v", err) 2151 | } 2152 | return dataset.data 2153 | } 2154 | ``` 2155 | 2156 | 当`mustLoadDataset` 被用于多个测试函数时,其成本被摊销: 2157 | 2158 | ```go 2159 | // Good: 2160 | func TestParseData(t *testing.T) { 2161 | data := mustLoadDataset(t) 2162 | 2163 | // As documented above. 2164 | } 2165 | 2166 | func TestListContents(t *testing.T) { 2167 | data := mustLoadDataset(t) 2168 | 2169 | // As documented above. 2170 | } 2171 | 2172 | func TestRegression682831(t *testing.T) { 2173 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2174 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2175 | } 2176 | } 2177 | ``` 2178 | 2179 | 普通拆解之所以棘手,是因为没有统一的地方来注册清理线程。如果设置函数(本例中为`loadDataset`)依赖于上下文,`sync.Once`可能会有问题。这是因为对设置函数的两次调用中的第二次需要等待第一次调用完成后再返回。这段等待时间不容易做到尊重上下文的取消。 2180 | 2181 | ## 字符串拼接 2182 | 2183 | 在 Go 中,有几种字符串拼接的方法。包括下面几种例子: 2184 | 2185 | - "+"运算符 2186 | - `fmt.Sprintf` 2187 | - `strings.Builder ` 2188 | - `text/template` 2189 | - `safehtml/template` 2190 | 2191 | 尽管选择哪种方法没有一刀切的规则,但下面的指南概述了在什么情况下哪种方法是首选。 2192 | 2193 | ### 简单情况下,首选 "+" 2194 | 2195 | 当连接几个字符串时,更愿意使用 "+"。这种方法在语法上是最简单的,不需要导入包。 2196 | 2197 | ```go 2198 | // Good: 2199 | key := "projectid: " + p 2200 | ``` 2201 | 2202 | ### 格式化时首选`fmt.Sprintf` 2203 | 2204 | 当建立一个带有格式化的复杂字符串时,倾向于使用`fmt.Sprintf`。使用许多 "+"运算符可能会掩盖最终的结果。 2205 | 2206 | ```go 2207 | // Good: 2208 | str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst) 2209 | ``` 2210 | 2211 | ```go 2212 | // Bad: 2213 | bad := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String() 2214 | ``` 2215 | 2216 | **最佳做法:**当构建字符串操作的输出是一个`io.Writer`时,不要用`fmt.Sprintf`构建一个临时字符串,只是为了把它发送给 Writer。相反,使用`fmt.Fprintf`来直接向 Writer 发送。 2217 | 2218 | 当格式化更加复杂时,请酌情选择 [`text/template`](https://pkg.go.dev/text/template) 或 [`safehtml/template`](https://pkg.go.dev/github.com/google/safehtml/template)。 2219 | 2220 | ### 倾向于使用`strings.Builder`来零散地构建一个字符串 2221 | 2222 | 在逐位建立字符串时,更倾向于使用`strings.Builder`。`strings.Builder`需要摊销的线性时间,而 "+"和`fmt.Sprintf`在连续调用以形成一个较大的字符串时需要二次时间。 2223 | 2224 | ```go 2225 | // Good: 2226 | b := new(strings.Builder) 2227 | for i, d := range digitsOfPi { 2228 | fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d) 2229 | } 2230 | str := b.String() 2231 | ``` 2232 | 2233 | **注意**。更多的讨论,请参见[GoTip #29: 高效地构建字符串](https://gocn.github.io/styleguide/docs/01-overview/#gotip)。 2234 | 2235 | ### 常量字符串 2236 | 2237 | 在构建常量、多行字符串变量时,倾向于使用反引号(`)。 2238 | 2239 | ```go 2240 | // Good: 2241 | usage := `Usage: 2242 | 2243 | custom_tool [args]` 2244 | ``` 2245 | 2246 | ```go 2247 | // Bad: 2248 | usage := "" + 2249 | "Usage:\n" + 2250 | "\n" + 2251 | "custom_tool [args]" 2252 | ``` 2253 | 2254 | {{< button relref="./03-decisions.md" >}}上一章{{< /button >}} 2255 | {{< button relref="/" >}}首页{{< /button >}} 2256 | -------------------------------------------------------------------------------- /content/zh/menu/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | headless: true 3 | --- 4 | 5 | - **开始学习** 6 | - [概述]({{< relref "/docs/01-overview.md" >}}) 7 | - [风格指南]({{< relref "/docs/02-guide.md" >}}) 8 | - [风格决策]({{< relref "/docs/03-decisions.md" >}}) 9 | - [最佳实践]({{< relref "/docs/04-best-practices.md" >}}) 10 | 11 | - [**Fork on Github**](https://github.com/gocn/styleguide) 12 | -------------------------------------------------------------------------------- /resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.content: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)} -------------------------------------------------------------------------------- /resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.json: -------------------------------------------------------------------------------- 1 | {"Target":"book.min.c58292d36b18b675680ab9baea2029204537b839ea72f258746ec0f32ce8d6c8.css","MediaType":"text/css","Data":{"Integrity":"sha256-xYKS02sYtnVoCrm66iApIEU3uDnqcvJYdG7A8yzo1sg="}} -------------------------------------------------------------------------------- /resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)} -------------------------------------------------------------------------------- /resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json: -------------------------------------------------------------------------------- 1 | {"Target":"book.min.c58292d36b18b675680ab9baea2029204537b839ea72f258746ec0f32ce8d6c8.css","MediaType":"text/css","Data":{"Integrity":"sha256-xYKS02sYtnVoCrm66iApIEU3uDnqcvJYdG7A8yzo1sg="}} -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # Google Go 编程规范 2 | 3 | [《Google Go 编程规范》](https://github.com/gocn/styleguide)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/styleguide/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/styleguide/pulls) 来共同参与贡献。 4 | 5 | 贡献者(按昵称首字母排序): 6 | 7 | > [astaxie](https://github.com/astaxie) | [Fivezh](https://github.com/fivezh) | [刘思家](https://github.com/lsj1342) | [Sijing233](https://github.com/sijing233) | [小超人](https://github.com/focozz) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang) | [784909593](https://github.com/784909593) 8 | 9 | 安装完 `hugo` 之后,需要先同步主题文件 10 | 11 | ```bash 12 | git submodule update --init --recursive 13 | ``` 14 | 15 | 同步完成后,可在根目录执行以下指令来测试网站: 16 | 17 | ```bash 18 | hugo server 19 | ``` 20 | 21 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。 22 | 23 | ## 目录 24 | 25 | 1. 概述 26 | 2. 风格指南 27 | 3. 风格决策 28 | 4. 最佳实践 29 | 30 | ## 授权 31 | 32 | The articles in 《Google Go 编程规范》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 33 | --------------------------------------------------------------------------------