最近在codeing代码到了测试阶段,让运维和测试去部署程序的时候发现仅仅通过口口相传是不行的,就算给他讲清楚到了现场之后还是会通过电话来轰炸你;有一些开发人员是写了文档,但是文档层次结构、目录不统一,导致文档不能传达该传达的意思。
最近在浏览论坛的时候找到一片关于语言工程化设计的文章,特此摘抄下来,规范自己的文档编写。
工程化规范
无规矩不成方圆,生活如此,软件开发也是如此。一个应用基本都是多人协作开发的,但不同人的开发习惯、方式都不同。如果没有一个统一的规范,就会造成非常多的问题,比如:
- 代码风格不一:代码仓库中有多种代码风格,读 / 改他人的代码都是一件痛苦的事情,整个代码库也会看起来很乱。
- 目录杂乱无章:相同的功能被放在不同的目录,或者一个目录你根本不知道它要完成什么功能,新开发的代码你也不知道放在哪个目录或文件。这些都会严重降低代码的可维护性。
- 接口不统一:对外提供的 API 接口不统一,例如修改用户接口为/v1/users/colin,但是修改密钥接口为/v1/secret?name=secret0,难以理解和记忆。
- 错误码不规范:错误码会直接暴露给用户,主要用于展示错误类型,以定位错误问题。错误码不规范会导致难以辨别错误类型,或者同类错误拥有不同错误码,增加理解难度。
因此,在设计阶段、编码之前,我们需要一个好的规范来约束开发者,以确保大家开发的是“一个应用”。一个好的规范不仅可以提高软件质量,还可以提高软件的开发效率,降低维护成本,甚至能减少 Bug 数,也可以使你的开发体验如行云流水一般顺畅。所以,在编码之前,有必要花一些时间和团队成员一起讨论并制定规范。
有哪些地方需要制定规范?
一个 Go 项目会涉及很多方面,所以也会有多种规范,同类规范也会因为团队差异而有所不同。所以,我将它们分为非编码类规范和编码类规范:
- 非编码类规范:文档规范,版本规范,Git 规范,发布规范,…
- 编码类规范:目录规范,代码规范,接口规范,日志规范,错误码规范,…
非编码类规范
文档规范
README文档
README 文档是项目的门面,它是开发者学习项目时第一个阅读的文档,会放在项目的根目录下。因为它主要是用来介绍项目的功能、安装、部署和使用的,所以它是可以规范化的。
下面,我们直接通过一个 README 模板,来看一下 README 规范中的内容:
1 |
|
建议使用 readme.so 在线网站生成。
项目文档
项目文档包括一切需要文档化的内容,它们通常集中放在 /docs 目录下。当我们在创建团队的项目文档时,通常会预先规划并创建好一些目录,用来存放不同的文档。因此,在开始 Go 项目开发之前,我们也要制定一个软件文档规范。好的文档规范有 2 个优点:易读和可以快速定位文档。
不同项目有不同的文档需求,在制定文档规范时,你可以考虑包含两类文档。
- 开发文档:描述项目开发流程,包括如何搭建开发环境、构建二进制文件、测试、部署等。
- 用户文档:软件的使用文档,对象一般是软件使用者。内容包括 API 文档、SDK 文档、安装文档、功能介绍文档、最佳实践、操作指南、常见问题等。
参考目录结构:
1 | docs |
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 | <type>[optional scope]: <description> |
分别对应 Commit message 的三个部分:Header
,Body
和 Footer
;其中,Header
是必需的,Body
和 Footer
可以省略。
建议使用
git commit
时不要使用-m
选项,而-a
进入交互界面编辑 commit message。
下面对 Header、Body 和Footer详细介绍
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 | BREAKING CHANGE: <breaking change summary> |
分为两部分,BREAKING CHANGE 和 Fixes
在 BREAKING CHANGE 之后跟上简要的描述,空一行写上具体的信息:包括说明变动的描述、变动的理由和迁移方法:
1 | BREAKING CHANGE: isolate scope bindings definition has changed and |
Fixes部分跟上该功能需要关闭的Issue列表
关闭的 Bug 需要在 Footer 部分新建一行,并以 Closes 开头列出,比如:Closes #123
。如果关闭多个 Issue,可以列出:Closes #123, #432, #886
。
1 | Change pause version value to a constant for image |
规范Commit Message的插件
如果在使用JB系的编辑器,就可以使用 Git Commit Message Helper 插件来规范提交信息。
编码类规范
目录规范
目录结构是一个项目的门面。很多时候,根据目录结构就能看出开发者对这门语言的掌握程度。所以,在我看来,遵循一个好的目录规范,把代码目录设计得可维护、可扩展,甚至比文档规范、Commit 规范来得更加重要。
那具体怎么组织一个好的代码目录呢?一个好的目录应该要有以下几点:
命名清晰:目录命名要清晰、简洁,不要太长,也不要太短,目录名要能清晰地表达出该目录实现的功能,并且目录名最好用单数。一方面是因为单数足以说明这个目录的功能,另一方面可以统一规范,避免单复混用的情况。
功能明确:一个目录所要实现的功能应该是明确的、并且在整个项目目录中具有很高的辨识度。也就是说,当需要新增一个功能时,我们能够非常清楚地知道把这个功能放在哪个目录下。
全面性:目录结构应该尽可能全面地包含研发过程中需要的功能,例如文档、脚本、源码管理、API 实现、工具、第三方包、测试、编译产物等。
可预测性:项目规模一定是从小到大的,所以一个好的目录结构应该能够在项目变大时,仍然保持之前的目录结构。
可扩展性:每个目录下存放了同类的功能,在项目变大时,这些目录应该可以存放更多同类功能。
当前 Go 社区比较推荐的结构化目录结构是 [[golang的项目结构布局|project-layout]] 。虽然它并不是官方和社区的规范,但因为组织方式比较合理,被很多 Go 开发人员接受。所以,我们可以把它当作是一个事实上的规范。
如下图所示:
根据上面的图分为3部分:Go应用、项目管理以及文档部分,下面就根据这3部分详细讲解。
Go应用
在Go应用这一部分中又可以根据类型划分,分别划分为:前后端代码、测试代码、应用部署相关代码、项目管理代码、文档代码。
开发阶段所涉及到的目录。我们开发的代码包含前端代码和后端代码,可以分别存放在前端目录和后端目录中。
前后端代码
- /web
前端代码存放目录,主要用来存放 Web 静态资源,服务端模板和单页应用(SPAs)。
- /cmd
一个项目有很多组件,可以把组件 main 函数所在的文件夹统一放在/cmd 目录下
1 | $ ls cmd/ |
在项目目录的 cmd 目录中,文件夹的个数应该与该项目的组件数量相匹配。
每个组件的目录名应该跟你期望的可执行文件名是一致的。这里要保证 /cmd/<组件名> 目录下不要存放太多的代码,如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。
- /internal
存放私有应用和库代码。如果一些代码,你不希望在其他应用和库中被导入,可以将这部分代码放在/internal 目录下。
可以通过 Go 语言本身的机制来约束其他项目 import 项目内部的包。/internal 目录建议包含如下目录:
- /internal/apiserver:该目录中存放真实的应用代码。这些应用的共享代码存放在/internal/pkg 目录下。
- /internal/pkg:存放项目内可共享,项目外不共享的包。这些包提供了比较基础、通用的功能,例如工具、错误码、用户验证等功能。
一开始将所有的共享代码存放在 /internal/pkg 目录下,当该共享代码做好了对外开发的准备后,再转存到/pkg目录下。
- pkg
/pkg 目录是 Go 语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如 Kubernetes、Prometheus、Moby、Knative 等。
该目录中存放可以被外部应用使用的代码库,其他项目可以直接通过 import 导入这里的代码。所以,我们在将代码库放入该目录时一定要慎重。
- /vendor
项目依赖,可通过 go mod vendor 创建。需要注意的是,如果是一个 Go 库,不要提交 vendor 依赖包。
- /third_party
外部帮助工具,分支代码或其他第三方应用(例如 Swagger UI)。比如我们 fork 了一个第三方 go 包,并做了一些小的改动,我们可以放在目录 /third_party/forked 下。一方面可以很清楚的知道该包是 fork 第三方的,另一方面又能够方便地和 upstream 同步。
测试代码
7. /test
用于存放其他外部测试应用和测试数据。/test 目录的构建方式比较灵活:对于大的项目,有一个数据子目录是有意义的。例如,如果需要 Go 忽略该目录中的内容,可以使用 /test/data 或 /test/testdata 目录。
需要注意的是,Go 也会忽略以“.”或 “_” 开头的目录或文件。 这样在命名测试数据目录方面,可以具有更大的灵活性。
应用部署相关代码
- /config
这个目录用来配置文件模板或默认配置。例如,可以在这里存放 confd 或 consul-template 模板文件。这里有一点要注意,配置中不能携带敏感信息,这些敏感信息,我们可以用占位符来替代,例如:
1 | apiVersion: v1 |
然后这些环境变量可以通过脚本文件进行设置。
- /deployments
用来存放 Iaas、PaaS 系统和容器编排部署配置和模板(Docker-Compose,Kubernetes/Helm,Mesos,Terraform,Bosh)。在一些项目,特别是用 Kubernetes 部署的项目中,这个目录可能命名为 deploy。
为什么要将这类跟 Kubernetes 相关的目录放到目录结构中呢?主要是因为当前软件部署基本都在朝着容器化的部署方式去演进。
- init
存放初始化系统(systemd,upstart,sysv)和进程管理配置文件(runit,supervisord)。比如 sysemd 的 unit 文件。这类文件,在非容器化部署的项目中会用到。
项目管理
- /Makefile
虽然 Makefile 是一个很老的项目管理工具,但它仍然是最优秀的项目管理工具。所以,一个 Go 项目在其根目录下应该有一个 Makefile 工具,用来对项目进行管理,Makefile 通常用来执行静态代码检查、单元测试、编译等功能。其他常见功能。
在实际开发中,我们可以将一些重复性的工作自动化,并添加到 [[Makefile基本语法以及使用|Makefile]] 文件中统一管理。
- /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 的脚本中,就大量采用了这种命名方式。
- /build
这里存放安装包和持续集成相关的文件。这个目录下有 3 个大概率会使用到的目录,在设计目录结构时可以考虑进去。
- /build/package:存放容器(Docker)、系统(deb, rpm, pkg)的包配置和脚本。
- /build/ci:存放 CI(travis,circle,drone)的配置文件和脚本。
- /build/docker:存放子项目各个组件的 Dockerfile 文件。
- /tools
存放这个项目的支持工具。这些工具可导入来自 /pkg 和 /internal 目录的代码。
- /githooks
Git 钩子。比如,我们可以将 commit-msg 存放在该目录。
- /assets
项目使用的其他资源 (图片、CSS、JavaScript 等)。
文档
- README.md
项目的 README 文件一般包含了项目的介绍、功能、快速安装和使用指引、详细的文档链接以及开发指引等。有时候 README 文档会比较长,为了能够快速定位到所需内容,需要添加 markdown toc 索引,可以借助工具 tocenize 来完成索引的添加。
这里还有个建议,前面我们也介绍过 README 是可以规范化的,所以这个 README 文档,可以通过脚本或工具来自动生成。
- /docs
这部分参考前文 [[Go工程化规范#项目文档]]
- /CONTRIBUTING.md
如果是一个开源就绪的项目,最好还要有一个 CONTRIBUTING.md 文件,用来说明如何贡献代码,如何开源协同等等。CONTRIBUTING.md 不仅能够规范协同流程,还能降低第三方开发者贡献代码的难度。
- /api
这部分参考前文 [[Go工程化规范#API文档]]
- /LICENSE
版权文件可以是私有的,也可以是开源的。常用的开源协议有:Apache 2.0、MIT、BSD、GPL、Mozilla、LGPL。有时候,公有云产品为了打造品牌影响力,会对外发布一个本产品的开源版本,所以在项目规划初期最好就能规划下未来产品的走向,选择合适的 LICENSE。
为了声明版权,你可能会需要将 LICENSE 头添加到源码文件或者其他文件中,这部分工作可以通过工具实现自动化,推荐工具: addlicense 。
- /CHANGELOG
当项目有更新时,为了方便了解当前版本的更新内容或者历史更新内容,需要将更新记录存放到 CHANGELOG 目录。编写 CHANGELOG 是一个复杂、繁琐的工作,我们可以结合 Angular 规范 和 git-chglog 来自动生成 CHANGELOG。