最近在codeing代码到了测试阶段,让运维和测试去部署程序的时候发现仅仅通过口口相传是不行的,就算给他讲清楚到了现场之后还是会通过电话来轰炸你;有一些开发人员是写了文档,但是文档层次结构、目录不统一,导致文档不能传达该传达的意思。

最近在浏览论坛的时候找到一片关于语言工程化设计的文章,特此摘抄下来,规范自己的文档编写。

工程化规范

无规矩不成方圆,生活如此,软件开发也是如此。一个应用基本都是多人协作开发的,但不同人的开发习惯、方式都不同。如果没有一个统一的规范,就会造成非常多的问题,比如:

  • 代码风格不一:代码仓库中有多种代码风格,读 / 改他人的代码都是一件痛苦的事情,整个代码库也会看起来很乱。
  • 目录杂乱无章:相同的功能被放在不同的目录,或者一个目录你根本不知道它要完成什么功能,新开发的代码你也不知道放在哪个目录或文件。这些都会严重降低代码的可维护性。
  • 接口不统一:对外提供的 API 接口不统一,例如修改用户接口为/v1/users/colin,但是修改密钥接口为/v1/secret?name=secret0,难以理解和记忆。
  • 错误码不规范:错误码会直接暴露给用户,主要用于展示错误类型,以定位错误问题。错误码不规范会导致难以辨别错误类型,或者同类错误拥有不同错误码,增加理解难度。

因此,在设计阶段、编码之前,我们需要一个好的规范来约束开发者,以确保大家开发的是“一个应用”。一个好的规范不仅可以提高软件质量,还可以提高软件的开发效率,降低维护成本,甚至能减少 Bug 数,也可以使你的开发体验如行云流水一般顺畅。所以,在编码之前,有必要花一些时间和团队成员一起讨论并制定规范。

有哪些地方需要制定规范?

一个 Go 项目会涉及很多方面,所以也会有多种规范,同类规范也会因为团队差异而有所不同。所以,我将它们分为非编码类规范和编码类规范:

  • 非编码类规范:文档规范,版本规范,Git 规范,发布规范,…
  • 编码类规范:目录规范,代码规范,接口规范,日志规范,错误码规范,…

非编码类规范

文档规范

README文档

README 文档是项目的门面,它是开发者学习项目时第一个阅读的文档,会放在项目的根目录下。因为它主要是用来介绍项目的功能、安装、部署和使用的,所以它是可以规范化的。

下面,我们直接通过一个 README 模板,来看一下 README 规范中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 项目名称

<!-- 写一段简短的话描述项目 -->

## 功能特性

<!-- 描述该项目的核心功能点 -->

## 软件架构(可选)

<!-- 可以描述下项目的架构 -->

## 快速开始

### 依赖检查

<!-- 描述该项目的依赖,比如依赖的包、工具或者其他任何依赖项 -->

### 构建

<!-- 描述如何构建该项目 -->

### 运行

<!-- 描述如何运行该项目 -->

## 使用指南

<!-- 描述如何使用该项目 -->

## 如何贡献

<!-- 告诉其他开发者如果给该项目贡献源码 -->

## 社区(可选)

<!-- 如果有需要可以介绍一些社区相关的内容 -->

## 关于作者

<!-- 这里写上项目作者 -->

## 谁在用(可选)

<!-- 可以列出使用本项目的其他有影响力的项目,算是给项目打个广告吧 -->

## 许可证

<!-- 这里链接上该项目的开源许可证 -->

建议使用 readme.so 在线网站生成。

项目文档

项目文档包括一切需要文档化的内容,它们通常集中放在 /docs 目录下。当我们在创建团队的项目文档时,通常会预先规划并创建好一些目录,用来存放不同的文档。因此,在开始 Go 项目开发之前,我们也要制定一个软件文档规范。好的文档规范有 2 个优点:易读和可以快速定位文档。

不同项目有不同的文档需求,在制定文档规范时,你可以考虑包含两类文档。

  • 开发文档:描述项目开发流程,包括如何搭建开发环境、构建二进制文件、测试、部署等。
  • 用户文档:软件的使用文档,对象一般是软件使用者。内容包括 API 文档、SDK 文档、安装文档、功能介绍文档、最佳实践、操作指南、常见问题等。

参考目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
docs
├── devel # 开发文档,可以提前规划好,英文版文档和中文版文档
│ ├── en-US/ # 英文版文档,可以根据需要组织文件结构
│ └── zh-CN # 中文版文档,可以根据需要组织文件结构
│ └── development.md # 开发手册,可以说明如何编译、构建、运行项目
├── guide # 用户文档
│ ├── en-US/ # 英文版文档,可以根据需要组织文件结构
│ └── zh-CN # 中文版文档,可以根据需要组织文件结构
│ ├── api/ # API 文档
│ ├── best-practice # 最佳实践,存放一些比较重要的实践文章
│ │ └── authorization.md
│ ├── faq # 常见问题
│ │ ├── iam-apiserver
│ │ └── installation
│ ├── installation # 安装文档
│ │ └── installation.md
│ ├── introduction/ # 产品介绍文档
│ ├── operation-guide # 操作指南,可根据 RESTful 资源再划分为更细的子目录,存放系统功能操作手册
│ │ ├── policy.md
│ │ ├── secret.md
│ │ └── user.md
│ ├── quickstart # 快速入门
│ │ └── quickstart.md
│ ├── README.md # 用户文档入口文件
│ └── sdk # SDK 文档
│ └── golang.md
└── images # 图片存放目录
└── 部署架构v1.png

API文档

一般由后端开发人员编写,描述组件提供的 API 以及调用方法。

可以编写 Word/Markdown 格式文档、借助工具编写(填充内容)、通过注释生成(如 Swagger)等。

通常需要包含完整的 API 接口介绍文档(接口描述、请求方法、请求参数、输出参数和请求示例)、API 接口变更历史文档、通用说明、数据结构说明、错误码描述和 API 接口使用文档。

  • README.md :API 接口介绍文档,会分类介绍 IAM 支持的 API 接口,并会存放相关 API 接口文档的链接,方便开发者查看。
  • CHANGELOG.md :API 接口文档变更历史,方便进行历史回溯,也可以使调用者决定是否进行功能更新和版本更新。
  • generic.md :用来说明通用的请求参数、返回参数、认证方法和请求方法等。
  • struct.md :用来列出接口文档中使用的数据结构。这些数据结构可能被多个 API 接口使用,会在 user.md、secret.md、policy.md 文件中被引用。
  • user.md 、 secret.md 、 policy.md :API 接口文档,相同 REST 资源的接口会存放在一个文件中,以 REST 资源名命名文档名。
  • error_code.md :错误码描述,通过程序自动生成。

其中接口描述:

  • 接口描述:描述接口实现的功能。
  • 请求方法:接口的请求方法,格式为 HTTP 方法 请求路径,比如 POST /v1/users。在 通用说明 中的 请求方法 部分,会说明接口的请求协议和请求地址。
  • 输入参数:接口的输入字段,分为 Header 参数、Query 参数、Body 参数、Path 参数。每个字段通过:参数名称必选类型描述 4 个属性来描述。如果参数有限制或者默认值,可在描述部分注明。
  • 输出参数:接口返回字段,每个字段通过 参数名称类型描述 3 个属性来描述。
  • 请求示例:真实的 API 接口请求和返回示例。

接口文档示例:

Get item

1
GET /api/items/${id}
Input Parameter Type Description
Query id string Required. Id of item to fetch

版本规范

一般使用 语义化版本规范(SemVer,Semantic Versioning),即 主版本号.次版本号.修订号(X.Y.Z,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零)。

也有先行版本号与编译版本号 v1.2.3-aplha.1+001:即把先行版本号(Pre-release)和版本编译元数据,作为延伸加到了主版本号.次版本号.修订号的后面:X.Y.Z[-先行版本号][+版本编译元数据]

  • 主版本号(MAJOR):不兼容的 API 修改。

    • 必须在有任何不兼容的修改被加入公共 API 时递增。其中可以包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须归零。
    • 主版本号为零(0.y.z)的软件处于开发初始阶段,由于一切都可能随时被改变,不应该被视为稳定版。1.0.0 被界定为第一个稳定版本,之后的所有版本号更新都基于该版本修改。
  • 次版本号(MINOR):向下兼容的功能性新增及修改。一般偶数为稳定版本,奇数为开发版本。在任何公共 API 功能被标记为弃用时也必须递增,当有改进时也可以递增。其中可以包括修订级别的改变。每当次版本号递增时,修订号必须归零。

  • 修订号(PATCH):向下兼容的问题修正,即 bug 修复。

  • 先行版本号:该版本不稳定,可能存在兼容性问题。

  • 编译版本号:编译器在编译过程中自动生成,开发者只定义其格式、不进行人为控制。

使用建议:

  • 使用 0.1.0 作为首个开发版本号,在后续每次发行时递增次版本号。

  • 稳定版本并第一次对外发布时版本号可定为 1.0.0。

  • 严格按照 Angular commit message 规范提交代码时:

  • fix 类型的 commit 可将修订号 +1。

  • feat 类型的 commit 可将次版本号 +1。

  • 带 BREAKING CHANGE 的 commit 可将主版本号 +1。

Git规范

常见的开源社区 commit message 规范有: jQuery、Ember、JSHint AngularJS和Conventional commits,这里详细介绍Angular规范。

Angular规范包括:

  • 语义化:commit message 被归为有意义的类型用来说明本次 commit 的类型。
  • 规范化:commit message 遵循预先定义好的规范,比如格式固定、都属于某个类型,可被开发者和工具识别。
Commit规范

基本格式:

1
2
3
4
5
<type>[optional scope]: <description>
// 空行
[optional body]
// 空行
[optional footer(s)]

分别对应 Commit message 的三个部分:HeaderBodyFooter;其中,Header 是必需的,BodyFooter 可以省略。

建议使用 git commit 时不要使用 -m 选项,而 -a 进入交互界面编辑 commit message。

下面对 HeaderBodyFooter详细介绍

Header

1
<type>[optional scope]: <subject:description>

只有一行,包括:type(必选)、scope(可选)和 subject(必选)。

type 分为:

  • Development:一般是项目管理类的变更,不会影响最终用户和生产环境的代码,比如 CI 流程、构建方式等的修改。通常可以免测发布。
  • Production:会影响最终的用户和生产环境的代码。一定要慎重并在提交前做好充分的测试。
类型 类别 说明
feature Production 新增功能
fix Production 修复Bug
perf Production 提高代码性能的变更
refactor Production 其他代码类的变更,这些变更不属于feature、fix、perf和style;例如简化代码、重命名变量、删除冗余代码等
style Development 代码格式类的变更,比如用 gofmt 格式化代码、删除空行等
test Development 新增测试用例或是更新现有测试用例
ci Development 持续继承和部署相关的改动,比如修改 Jenkins、GitLab CI 等CI配置文件或者更新 systemd unit 文件
docs Development 文档类的更新,包括修改用户文档或者开发文档等
chore Development 其他类型;比如构建流程、依赖管理或者辅助工具的改动

scope 说明 commit 影响范围,必须是名词,因项目而异。

  • 初期可设置粒度较大的 scope(如按组件名或功能设置),后续项目有变动或有新功能时可添加新的 scope。
  • 不适合设置太具体的值。会导致项目有太多的 scope 难以维护,开发者也难以确定 commit 所属的 scope 导致错放,反而失去分类的意义。

subject 是 commit 的简短描述,明确指出 commit 执行的操作,以动词开头(小写)、使用现在时,不加句号。

Body

可分成多行,格式较自由。以动词开头、使用现在时。

必须包括修改的动机、跟上一版本相比的改动点。

示例:

1
The body is mandatory for all commits except for those of scope "docs". When the body is required it must be at least 20 characters long.

Footer

根据需要选择,一般格式:

1
2
3
4
5
6
BREAKING CHANGE: <breaking change summary>
// 空行
<breaking change description + migration instructions>
// 空行
// 空行
Fixes #<issue number>

分为两部分,BREAKING CHANGE 和 Fixes

BREAKING CHANGE 之后跟上简要的描述,空一行写上具体的信息:包括说明变动的描述、变动的理由和迁移方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.

To migrate the code follow the example below:

Before:

scope: {
myAttr: 'attribute',
}

After:

scope: {
myAttr: '@',
}

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

Fixes部分跟上该功能需要关闭的Issue列表

关闭的 Bug 需要在 Footer 部分新建一行,并以 Closes 开头列出,比如:Closes #123。如果关闭多个 Issue,可以列出:Closes #123, #432, #886

1
2
3
Change pause version value to a constant for image

Closes #1137

规范Commit Message的插件

如果在使用JB系的编辑器,就可以使用 Git Commit Message Helper 插件来规范提交信息。

编码类规范

目录规范

目录结构是一个项目的门面。很多时候,根据目录结构就能看出开发者对这门语言的掌握程度。所以,在我看来,遵循一个好的目录规范,把代码目录设计得可维护、可扩展,甚至比文档规范、Commit 规范来得更加重要。

那具体怎么组织一个好的代码目录呢?一个好的目录应该要有以下几点:

  • 命名清晰:目录命名要清晰、简洁,不要太长,也不要太短,目录名要能清晰地表达出该目录实现的功能,并且目录名最好用单数。一方面是因为单数足以说明这个目录的功能,另一方面可以统一规范,避免单复混用的情况。

  • 功能明确:一个目录所要实现的功能应该是明确的、并且在整个项目目录中具有很高的辨识度。也就是说,当需要新增一个功能时,我们能够非常清楚地知道把这个功能放在哪个目录下。

  • 全面性:目录结构应该尽可能全面地包含研发过程中需要的功能,例如文档、脚本、源码管理、API 实现、工具、第三方包、测试、编译产物等。

  • 可预测性:项目规模一定是从小到大的,所以一个好的目录结构应该能够在项目变大时,仍然保持之前的目录结构。

  • 可扩展性:每个目录下存放了同类的功能,在项目变大时,这些目录应该可以存放更多同类功能。

当前 Go 社区比较推荐的结构化目录结构是 [[golang的项目结构布局|project-layout]] 。虽然它并不是官方和社区的规范,但因为组织方式比较合理,被很多 Go 开发人员接受。所以,我们可以把它当作是一个事实上的规范。

如下图所示:

根据上面的图分为3部分:Go应用、项目管理以及文档部分,下面就根据这3部分详细讲解。

Go应用

在Go应用这一部分中又可以根据类型划分,分别划分为:前后端代码、测试代码、应用部署相关代码、项目管理代码、文档代码。

开发阶段所涉及到的目录。我们开发的代码包含前端代码和后端代码,可以分别存放在前端目录和后端目录中。

前后端代码

  1. /web

前端代码存放目录,主要用来存放 Web 静态资源,服务端模板和单页应用(SPAs)。

  1. /cmd

一个项目有很多组件,可以把组件 main 函数所在的文件夹统一放在/cmd 目录下

1
2
3
4
5
$ ls cmd/
gendocs geniamdocs genman genswaggertypedocs genyaml iam-apiserver iam-authz-server iamctl iam-pump

$ ls cmd/iam-apiserver/
apiserver.go

在项目目录的 cmd 目录中,文件夹的个数应该与该项目的组件数量相匹配。

每个组件的目录名应该跟你期望的可执行文件名是一致的。这里要保证 /cmd/<组件名> 目录下不要存放太多的代码,如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。

  1. /internal

存放私有应用和库代码。如果一些代码,你不希望在其他应用和库中被导入,可以将这部分代码放在/internal 目录下。

可以通过 Go 语言本身的机制来约束其他项目 import 项目内部的包。/internal 目录建议包含如下目录:

  • /internal/apiserver:该目录中存放真实的应用代码。这些应用的共享代码存放在/internal/pkg 目录下。
  • /internal/pkg:存放项目内可共享,项目外不共享的包。这些包提供了比较基础、通用的功能,例如工具、错误码、用户验证等功能。

一开始将所有的共享代码存放在 /internal/pkg 目录下,当该共享代码做好了对外开发的准备后,再转存到/pkg目录下。

  1. pkg

/pkg 目录是 Go 语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如 Kubernetes、Prometheus、Moby、Knative 等。

该目录中存放可以被外部应用使用的代码库,其他项目可以直接通过 import 导入这里的代码。所以,我们在将代码库放入该目录时一定要慎重。

  1. /vendor

项目依赖,可通过 go mod vendor 创建。需要注意的是,如果是一个 Go 库,不要提交 vendor 依赖包。

  1. /third_party

外部帮助工具,分支代码或其他第三方应用(例如 Swagger UI)。比如我们 fork 了一个第三方 go 包,并做了一些小的改动,我们可以放在目录 /third_party/forked 下。一方面可以很清楚的知道该包是 fork 第三方的,另一方面又能够方便地和 upstream 同步。

测试代码
7. /test

用于存放其他外部测试应用和测试数据。/test 目录的构建方式比较灵活:对于大的项目,有一个数据子目录是有意义的。例如,如果需要 Go 忽略该目录中的内容,可以使用 /test/data 或 /test/testdata 目录。

需要注意的是,Go 也会忽略以“.”或 “_” 开头的目录或文件。 这样在命名测试数据目录方面,可以具有更大的灵活性。

应用部署相关代码

  1. /config

这个目录用来配置文件模板或默认配置。例如,可以在这里存放 confd 或 consul-template 模板文件。这里有一点要注意,配置中不能携带敏感信息,这些敏感信息,我们可以用占位符来替代,例如:

1
2
3
4
apiVersion: v1    
user:
username: ${CONFIG_USER_USERNAME} # iam 用户名
password: ${CONFIG_USER_PASSWORD} # iam 密码

然后这些环境变量可以通过脚本文件进行设置。

  1. /deployments

用来存放 Iaas、PaaS 系统和容器编排部署配置和模板(Docker-Compose,Kubernetes/Helm,Mesos,Terraform,Bosh)。在一些项目,特别是用 Kubernetes 部署的项目中,这个目录可能命名为 deploy。

为什么要将这类跟 Kubernetes 相关的目录放到目录结构中呢?主要是因为当前软件部署基本都在朝着容器化的部署方式去演进。

  1. init

存放初始化系统(systemd,upstart,sysv)和进程管理配置文件(runit,supervisord)。比如 sysemd 的 unit 文件。这类文件,在非容器化部署的项目中会用到。

项目管理

  1. /Makefile

虽然 Makefile 是一个很老的项目管理工具,但它仍然是最优秀的项目管理工具。所以,一个 Go 项目在其根目录下应该有一个 Makefile 工具,用来对项目进行管理,Makefile 通常用来执行静态代码检查、单元测试、编译等功能。其他常见功能。

在实际开发中,我们可以将一些重复性的工作自动化,并添加到 [[Makefile基本语法以及使用|Makefile]] 文件中统一管理。

  1. /scripts

该目录主要用来存放脚本文件,实现构建、安装、分析等不同功能。不同项目,里面可能存放不同的文件,但通常可以考虑包含以下 3 个目录:

  • /scripts/make-rules:用来存放 makefile 文件,实现 /Makefile 文件中的各个功能。Makefile 有很多功能,为了保持它的简洁,我建议你将各个功能的具体实现放在/scripts/make-rules 文件夹下。
  • /scripts/lib:shell 库,用来存放 shell 脚本。一个大型项目中有很多自动化任务,比如发布、更新文档、生成代码等,所以要写很多 shell 脚本,这些 shell 脚本会有一些通用功能,可以抽象成库,存放在/scripts/lib 目录下,比如 logging.sh,util.sh 等。
  • /scripts/install:如果项目支持自动化部署,可以将自动化部署脚本放在此目录下。如果部署脚本简单,也可以直接放在 /scripts 目录下。

另外,shell 脚本中的函数名,建议采用语义化的命名方式,例如 iam::log::info 这种语义化的命名方式,可以使调用者轻松的辨别出函数的功能类别,便于函数的管理和引用。在 Kubernetes 的脚本中,就大量采用了这种命名方式。

  1. /build

这里存放安装包和持续集成相关的文件。这个目录下有 3 个大概率会使用到的目录,在设计目录结构时可以考虑进去。

  • /build/package:存放容器(Docker)、系统(deb, rpm, pkg)的包配置和脚本。
  • /build/ci:存放 CI(travis,circle,drone)的配置文件和脚本。
  • /build/docker:存放子项目各个组件的 Dockerfile 文件。
  1. /tools

存放这个项目的支持工具。这些工具可导入来自 /pkg 和 /internal 目录的代码。

  1. /githooks

Git 钩子。比如,我们可以将 commit-msg 存放在该目录。

  1. /assets

项目使用的其他资源 (图片、CSS、JavaScript 等)。

文档

  1. README.md

项目的 README 文件一般包含了项目的介绍、功能、快速安装和使用指引、详细的文档链接以及开发指引等。有时候 README 文档会比较长,为了能够快速定位到所需内容,需要添加 markdown toc 索引,可以借助工具 tocenize 来完成索引的添加。

这里还有个建议,前面我们也介绍过 README 是可以规范化的,所以这个 README 文档,可以通过脚本或工具来自动生成。

  1. /docs

这部分参考前文 [[Go工程化规范#项目文档]]

  1. /CONTRIBUTING.md

如果是一个开源就绪的项目,最好还要有一个 CONTRIBUTING.md 文件,用来说明如何贡献代码,如何开源协同等等。CONTRIBUTING.md 不仅能够规范协同流程,还能降低第三方开发者贡献代码的难度。

  1. /api

这部分参考前文 [[Go工程化规范#API文档]]

  1. /LICENSE

版权文件可以是私有的,也可以是开源的。常用的开源协议有:Apache 2.0、MIT、BSD、GPL、Mozilla、LGPL。有时候,公有云产品为了打造品牌影响力,会对外发布一个本产品的开源版本,所以在项目规划初期最好就能规划下未来产品的走向,选择合适的 LICENSE。

为了声明版权,你可能会需要将 LICENSE 头添加到源码文件或者其他文件中,这部分工作可以通过工具实现自动化,推荐工具: addlicense

  1. /CHANGELOG

当项目有更新时,为了方便了解当前版本的更新内容或者历史更新内容,需要将更新记录存放到 CHANGELOG 目录。编写 CHANGELOG 是一个复杂、繁琐的工作,我们可以结合 Angular 规范 和 git-chglog 来自动生成 CHANGELOG。