Go 项目开发的第一步是初始化一个 Go 项目仓库。根据开发能力、项目类别、项目需求等,可以选择不同的项目初始化方式。一般而言有以下几种项目初始化方式,具体如下表所示:
| 方式 | 方式描述 |
|---|---|
| 生成工具 | 借助项目开发脚手架自动生成项目模版,例如 kratos、nirvana、kubebuilder、osctl 等工具 |
| 复制已有项目 | 复制一个可以满足功能需求的现有项目,然后重命名项目目录并修改 Go 包导入路径,最后基于修改后的源代码进行开发 |
| 从零初始化 | 从头初始化一个 Go 项目,包括设计目录结构、代码结构以及进行源代码开发 |
在实际开发中,可以根据需要选择合适的项目初始化方法。如果脚手架工具生成的代码,在目录结构设计、开发风格、代码质量、架构设计、功能列表等方面能够满足你的需求,则可以优先考虑使用工具来快速初始化 Go 项目。如果工具不能满足需求,建议基于一个优秀的 Go 项目进行魔改。该系列的项目是为了学习别人的优秀设计而选择从零开始模仿构建。
开始 Go 项目开发的第一步便是初始化一个项目仓库。对于 Go 项目而言,主要包括以下初始化内容:
- 创建项目目录;
- 初始化目录为 Go 模块;
- 初始化目录为 Git 仓库;
- 创建需要的目录。
初始化完仓库后,还需要给项目仓库添加基本的初始化文件:
- Air 工具配置文件;
- 版权声明文件;
- Makefile 脚本。
创建项目目录#
在开始初始化项目之前,需先设计好项目的名称。一个简洁、易懂且合适的项目名是开发高质量 Go 项目的第一步。通常,需要提前确认以下名称:
- 项目名称:项目名要具有一定语义,说明该项目的功能。建议的格式为纯小写的精短名字。如果项目名字过长,可以按单词用中杠线(-)分割,但最好不要使用。以下是一些合格的名字:api、controllermanager、controller-manager。不建议命名为:controller_manager;
- 项目名称大小写:还需确认项目名在代码中的大小写格式。统一大小写格式可以使整个代码命名格式保持统一。例如:controller-manager/controllermanager 项目的小写格式为 controllermanager,大写格式为 ControllerManager;
- 项目名简写格式:有些项目名出于易读性考虑,可能会较长。在编写代码时,如果引用了项目名,可能会导致代码行过长。为了使代码行简短易读,通常会采用简写模式。带中杠线分割的项目名的简短模式一般为每个单词的首字母,例如:controller-manager 为 cm。不带中杠线分割的项目名简写模式需要根据具体名字确定,且没有统一的命名规则,例如:controller 可以简写为 ctrl。
例如该系列的项目名字为iam(ldentity and Access Management),代码中大写格式为IAM,代码中小写格式为iam。
初始化目录为 Go 模块#
iam 是一个 Go 项目,根据 Go 语法要求,还需要将该项目初始化为一个 Go 模块,并添加到 Go 工作区中。初始化命令如下:
$ go mod init # 初始化当前项目为一个 Go 模块
$ go work init . # 初始化当前目录为一个 Go 工作区
$ go work use . # 添加当前模块到 Go 工作区
初始化目录为 Git 仓库#
当前项目开发基本上都是使用 Git 来管理项目源码,因此,还需要将项目仓库初始化为一个 Git 仓库。
在提交代码时,有些文件,例如备份文件、临时文件和日志文件不需要提交到项目仓库中,可以通过在项目目录下添加 .gitignore 文件来忽略这些文件。如果你不知道如何配置 .gitignore 文件中的内容,可以借助 .gitignore 文件生成工具来自动生成一个可用的 .gitignore 文件,例如 gitignore.io 就是一个很好用的 .gitignore 文件生成工具。iam 项目使用的 .gitignore 文件就是用上面的工具生成。
可以执行以下命令将 Go 项目仓库初始化为一个 Git 仓库:
$ git init # 初始化当前目录为 Git 仓库
$ git config user.name 'Robin Liu' # 设置仓库级别用户名
$ git config user.email robinliutech@gmail.com # 设置仓库级别邮箱
$ git config --global credential.helper store # 永久保存凭据
$ git add . # 添加所有被 Git 追踪的文件到暂存区
$ git remote add origin https://github.com/robinlg/iam # 将本地仓库与远程仓库相关联
$ git commit -m "feat: 第一次提交" # 将暂存区内容添加到本地仓库中
之后,可以在 iam 目录下进行代码开发,并根据需要提交代码。提交后的源码目录内容如下:
$ ls -AF
.git/ .gitignore go.mod go.work README.md
创建需要的目录#
初始化代码仓库后,我们可以根据前面设计的目录规范创建一些空目录,例如:
$ mkdir -p cmd configs docs scripts
$ ls -F
cmd/ configs/ docs/ go.mod go.work README.md scripts/
提前创建一些符合目录规范的空目录,可以带来以下好处:
- 提前规划目录相当于提前规划未来的功能,将未来要实现的功能以目录的形式固化在项目仓库中,起到记录的作用;
- 提前创建目录有利于后续文件按照功能存放在预先规划好的目录中,从而使项目更加规范。否则,不同开发者可能会根据各自的开发习惯,创建各种各样的目录结构和目录名称。
例如,可以将一些说明文档存放在 docs/devel/zh-CN 目录中。因为 Git 不追踪空目录,为了让 Git 追踪空目录,我们可以在空目录下创建一个空文件 .keep,并在适当的时候执行以下命令以删除这些临时 .keep 文件:
$ find . -name .keep | xargs -i rm {}
热加载 Go 应用#
在 Go 项目开发过程中,经常需要修改代码、编译代码、重新启动程序,然后测试程序。若每次都手动操作,则效率较低。此时,可以借助程序热加载工具来自动编译并重启程序。在 Go 生态中,有许多此类工具,其中较为流行的是 Air 工具。你可以直接参考 Air 官方文档了解如何使用 Air 工具。热加载并不是每个项目必须具备的功能,因此,你可以根据项目需求决定是否使用该功能。目前iam还未使用该功能。
以下是安装和配置 Air 工具的步骤。
(1) 安装 Air 工具
安装命令如下:
$ go install github.com/air-verse/air@latest
(2) 配置 Air 工具
这里我们使用 Air 官方仓库中给出的示例配置:air_example.toml。air_example.toml 中的示例配置基本能满足绝大部分项目需求,一般只需再配置 cmd、bin、args_bin 三个参数即可。
(3) 启动 Air 工具
配置完成后,在项目根目录下运行 air 命令。
# 默认使用当前目录下的 .air.toml 配置,你也可以通过 `-c` 选项指定配置,例如:`air -c .air.toml`
$ air
...
Proxy server listening on http://localhost:8090
building...
running...
Hello World!
Process Exit with Code 0
添加版权声明#
如果项目是一个开源项目或计划在未来开源,则需要为项目添加版权声明,主要包括以下内容:
- 存放在项目根目录下的 LICENSE 文件,用于声明项目所遵循的开源协议;
- 项目源文件中的版权头信息,用于说明文件所遵循的开源协议。
业界当前有上百种开源协议可供选择,常用的有六种,按从严格到宽松的顺序依次为:GPL、MPL、LGPL、Apache、BSD、MIT。iam 项目使用了最宽松的 MIT 协议。
iam 添加 LICENSE 文件#
一般项目的根目录下会存放一个 LICENSE 文件,用于声明开源项目所遵循的协议,因此我们也需要为 iam 初始化一个 LICENSE 文件。我们可以使用 license 工具来生成 LICENSE 文件,具体操作命令如下:
$ go install github.com/nishanths/license/v5@latest
$ license -list # 查看支持的代码协议
# 在 iam 项目根目录下执行
$ license -n 'Robin Liu' -o LICENSE mit
上述命令将在当前目录下生成一个名为 LICENSE 的文件,该文件包含 MIT 开源协议声明。
给源文件添加版本声明#
除了添加整个项目的开源协议声明,还可以为每个源文件添加版权头信息,以声明文件所遵循的开源协议。iam 的版权头信息保存在 scripts/boilerplate.txt 文件中。
提示: 版权头信息保存的文件名,通常命名为 boilerplate。
有了版权头信息,在新建文件时需要将这些信息放在文件头中。如果手动添加,不仅容易出错,还容易遗漏文件。最好的方法是通过自动化手段追加版权头信息。追加方法如下。
(1) 安装 addlicense 工具
安装命令如下:
$ go install github.com/marmotedu/addlicense@latest
(2) 运行 addlicense 工具添加版权头信息
运行以下命令添加版权头信息。
$ addlicense -v -f ./scripts/boilerplate.txt --skip-dirs=third_party,_output .
Makefile 项目管理#
优秀的开发者在开发一个 Go 项目时,会提前考虑应用的扩展性,在开发之初,甚至功能都是按着大规模应用的标准去设计的。
对于一个庞大的应用,可能有众多开发者,每天都在提交代码,每天都在重复执行:静态代码检查、代码编译、镜像构建、单元测试、Protobuf 文件编译等操作。试想如果没有一个统一、高效的项目管理,你很可能会面临以下 2 个核心难题:
- 执行结果不一致带来一些潜在的问题和沟通成本:这么多开发者,相同的操作每个人执行的命令和结果都是不一样的。不一致的结果,可能会程序运行结果不一致,并带来一些沟通成本;
- 执行效率低下:例如编译一个 Protobuf 文件需要键入的命令长度就可能超过 200 个字符。如果没有一个高效的编译方法,每次手动输入这么多命令字符,整个操作是非常低效的,而且还可能因为命令执行错误,导致运行失败。
上面 2 个难题最终导致的结果就是:项目管理效率低,或者说开发效率低。
所以为了提高开发效率,在开发 Go 项目时,需要有一个统一、高效的项目管理手段。当前,对于一个 Go 项目,有多种项目管理手段,但最普遍的方法是使用 Makefile 来管理项目。
如何通过 Makefile 管理项目?#
在 Go 项目开发中,开发者可能会编写各种质量、各种结构的 Makefile 文件。一个简单的 Makefile 如下:
# 变量定义
APP_NAME = myapp
SRC = ./...
OUTPUT_DIR = ./bin
BUILD_FILE = $(OUTPUT_DIR)/$(APP_NAME)
# 默认目标
all: build
# 构建二进制文件
build:
@echo "Building the application..."
@mkdir -p $(OUTPUT_DIR)
go build -o $(BUILD_FILE) ./main.go
# 运行应用程序
run:
@echo "Running the application..."
@$(BUILD_FILE)
# 运行测试
test:
@echo "Running tests..."
go test $(SRC)
# 格式化代码
fmt:
@echo "Formatting code..."
go fmt $(SRC)
# 清理生成的文件
clean:
@echo "Cleaning build files..."
@rm -rf $(OUTPUT_DIR)
# 伪目标 (防止文件与目标名称冲突)
.PHONY: all build run test fmt clean
对于一个小型项目而言,上述 Makefile 基本能够满足需求。然而,对于一个大型项目来说,上述 Makefile 并不适用。大型项目在开发过程中通常涉及功能繁多、条件复杂、且命令较为冗长。如果直接将这些功能简单拼凑到一个 Makefile 文件中,虽然能够一定程度上提升开发效率,但效果非常有限。一个冗长、难以阅读且操作复杂的 Makefile 同样会对项目管理造成阻碍。
如果想管理大型项目,还需要升级 Makefile 的实现,通过结构化、灵活、可扩展的方式来编写 Makefile。
结构化 Makefile 设计方式#
结构化设计 Makefile 文件的方式有很多种,一个经典的结构化 Makefile 设计示例如下:
├─ Makefile
├─ scripts/
│ └─ make-rules/
│ ├─ all.mk
│ ├─ common.mk
│ ├─ gen.mk
│ ├─ golang.mk
│ ├─ swagger.mk
│ └─ tools.mk
上述 Makefile 结构也是 iam 项目采用的结构。项目根目录下的 Makefile 主要用来聚合 Makefile 核心规则,具体的规则实现存放于 scripts/make-rules/ 目录下。all.mk 也是一个 Makefile 文件,该文件导入了 scripts/make-rules/ 目录下的其他 Makefile 文件。
项目根目录下的 Makefile 文件通过导入 scripts/make-rules/all.mk,间接导入 scripts/make-rules/ 目录下的 Makefile 文件,例如:
include scripts/make-rules/common.mk
include scripts/make-rules/tools.mk # include at second order
include scripts/make-rules/golang.mk
include scripts/make-rules/gen.mk
include scripts/make-rules/swagger.mk
这种设计方式能够确保根目录下的 Makefile 文件结构清晰、易于阅读和维护。同时,由于功能按类别拆分到 scripts/make-rules/ 目录中的多个 Makefile 文件,也能保证每个文件代码简洁、功能聚焦,从而提升代码的可读性和可维护性。
对于较复杂的功能实现,可以通过编写 Shell 脚本来完成。这些 Shell 脚本可以放置于 scripts/ 目录中,并在 Makefile 中进行引用。相比直接在 Makefile 中实现功能,使用 Shell 脚本不仅提高了开发效率、降低了实现难度,还能够有效减少 Makefile 文件的代码量。
因此,一个合理的 Makefile 结构如图 13-2 所示。
