上一节课,我们初始化了一个 Go 项目仓库。初始化完仓库后,还需要给项目仓库添加基本的初始化文件。本节课,我们会初始化以下几类初始化文件:
- Air 工具配置文件;
- 版权声明文件;
- Makefile 脚本。
热加载 Go 应用
在 Go 项目开发过程中,经常需要修改代码、编译代码、重新启动程序,然后测试程序。若每次都手动操作,则效率较低。此时,可以借助程序热加载工具来自动编译并重启程序。在 Go 生态中,有许多此类工具,其中较为流行的是 Air 工具。你可以直接参考 Air 官方文档了解如何使用 Air 工具。
以下是安装和配置 Air 工具的步骤。
(1)安装 Air 工具
安装命令如下:
$ go install github.com/air-verse/air@latest(2)配置 Air 工具。
这里我们使用 Air 官方仓库中给出的示例配置:air_example.toml。air_example.toml 中的示例配置基本能满足绝大部分项目需求,一般只需再配置 cmd、bin、args_bin 三个参数即可。
在 miniblog 项目根目录下创建 .air.toml 文件,文件内容见 miniblog 仓库 feature/s01 分支下的 .air.toml 文件。.air.toml 基于 air_example.toml 文件修改了以下参数配置:
# air在运行时存储临时文件的目录
tmp_dir = "/tmp/air"
[build]
# cmd 指定了监听文件有变化时,air 需要执行的命令。
# 这里指定了执行 make build 重新构建 mb-apiserver 二进制文件
cmd = "go build -o _output/mb-apiserver -v cmd/mb-apiserver/main.go"
# bin 指定了执行完 cmd 命令后,执行的二进制文件。
# 这里指定了执行 _output/ mb-apiserver 文件
bin = "_output/mb-apiserver"
# args_bin 指定了运行二进制文件(bin/full_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通过上述 air 命令的输出,可以得知 air 成功编译并运行了 miniblog 项目。之后,修改项目目录下的 Go 文件时,air 工具会自动编译源代码并运行配置的可执行文件。
添加版权声明
如果项目是一个开源项目或计划在未来开源,则需要为项目添加版权声明,主要包括以下内容:
- 存放在项目根目录下的 LICENSE 文件,用于声明项目所遵循的开源协议;
- 项目源文件中的版权头信息,用于说明文件所遵循的开源协议。
业界当前有上百种开源协议可供选择,常用的有六种,按从严格到宽松的顺序依次为:GPL、MPL、LGPL、Apache、BSD、MIT。
miniblog 项目使用了最宽松的 MIT 协议。
miniblog 添加 LICENSE 文件
一般项目的根目录下会存放一个 LICENSE 文件,用于声明开源项目所遵循的协议,因此我们也需要为 miniblog 初始化一个 LICENSE 文件。我们可以使用 license 工具来生成 LICENSE 文件,具体操作命令如下:
$ go install github.com/nishanths/license/v5@latest
$ license -list # 查看支持的代码协议
# 在 miniblog 项目根目录下执行
$ license -n 'colin404(孔令飞) <colin404@foxmail.com>' -o LICENSE mit上述命令将在当前目录下生成一个名为 LICENSE 的文件,该文件包含 MIT 开源协议声明。
给源文件添加版本声明
除了添加整个项目的开源协议声明,还可以为每个源文件添加版权头信息,以声明文件所遵循的开源协议。miniblog 的版权头信息保存在 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 .可以看到 main.go 文件已经添加了版权头信息,内容如下:
// Copyright 2024 孔令飞 <colin404@foxmail.com>. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. The original repo for
// this file is https://github.com/onexstack/miniblog. The professional
// version of this repository is https://github.com/onexstack/onex.
package main
import "fmt"
// Go 程序的默认入口函数。阅读项目代码的入口函数.
func main() {
fmt.Println("Hello World!")
}编写 Makefile 脚本
在编译 miniblog 项目时,我们手动执行了以下编译命令来编译源码:
$ go build -o _output/mb-apiserver -v cmd/mb-apiserver/main.go但是,随着项目的迭代,上述编译命令可能会变得更长,并且在整个项目开发周期中,这一命令将被频繁执行。如果每次都手动执行命令编译源码,不仅效率低下,而且容易出错。如果一个项目有多个开发者协作,每个开发者执行的命令和参数可能不同,这将导致编译结果的不一致,进而增加沟通和维护成本。
那么,该如何解决这些问题呢?最佳实践是使用构建工具来管理项目,并将这些高频操作集成到构建工具中。构建工具(Build Tool)是一种用于自动化从源代码生成可用目标(Targets)的工具,这些目标可以是库、可执行文件或生成的脚本等。
业界当前有许多构建工具,相对受欢迎的有:Make、Bazel 和 CMake。Make 是一个广泛使用的 GNU 构建工具,其优点在于高普及度和简单易学的语法,但在大型项目中构建速度较慢,且不支持增量构建和缓存。Bazel 是由谷歌开发的构建工具,支持多语言和多平台,具有高可伸缩性和局部增量编译的优势,但系统复杂度较高,学习曲线陡峭。CMake 是一个跨平台的构建工具,能够自动发现库和配置工具链,易于书写的 CMakeLists.txt 文件使其使用方便,但相比于 Make 更为复杂,生成的 Makefile 可能较为臃肿。
这三种构建工具各有优缺点。在选择最合适的管理工具时,需要对这三种工具有深入的了解,明晰每种工具的优缺点及其适用场景。以下是我的建议:如果没有特殊需求,建议在 Make 和 CMake 中选择更为普适的 Make。对于一般项目(即绝大多数 Go 项目),可以使用更加通用的 Make 工具。对于超大型项目(例如公司级别的 Git 大仓),则可以考虑使用 Bazel。
miniblog 项目选择 Make 作为构建工具。业界优秀的项目基本都是采用 Make 来管理的,例如:Kubernetes、Docker、Istio 等。
编写简单的 Makefile
要使用 Makefile 管理项目,必须学会编写 Makefile 脚本。由于 Makefile 语法较为复杂,网上也有许多优秀的课程,因此本课程不会详细介绍。建议通过以下方式学习 Makefile 编程。
- 学习 Makefile 基本语法,可参考 docs/book/makefile.md 文件;
- 学习 Makefile 高级语法(如果有时间或感兴趣):陈皓老师编写的《跟我一起写 Makefile(PDF 重制版)》。
miniblog 项目的 Makefile 文件位于项目根目录下,内容如下:
# ==============================================================================
# 定义全局 Makefile 变量方便后面引用
COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# 项目根目录
PROJ_ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/ && pwd -P))
# 构建产物、临时文件存放目录
OUTPUT_DIR := $(PROJ_ROOT_DIR)/_output
# ==============================================================================
# 定义默认目标为 all
.DEFAULT_GOAL := all
# 定义 Makefile all 伪目标,执行 `make` 时,会默认会执行 all 伪目标
.PHONY: all
all: tidy format build add-copyright
# ==============================================================================
# 定义其他需要的伪目标
.PHONY: build
build: tidy # 编译源码,依赖 tidy 目标自动添加/移除依赖包.
@go build -v -o $(OUTPUT_DIR)/mb-apiserver $(PROJ_ROOT_DIR)/cmd/mb-apiserver/main.go
.PHONY: format
format: # 格式化 Go 源码.
@gofmt -s -w ./
.PHONY: add-copyright
add-copyright: # 添加版权头信息.
@addlicense -v -f $(PROJ_ROOT_DIR)/scripts/boilerplate.txt $(PROJ_ROOT_DIR) --skip-dirs=third_party,vendor,$(OUTPUT_DIR)
.PHONY: tidy
tidy: # 自动添加/移除依赖包.
@go mod tidy
.PHONY: clean
clean: # 清理构建产物、临时文件等.
@-rm -vrf $(OUTPUT_DIR)在编写 Makefile 规则之后,可以执行 make <target> 命令以运行指定的 Makefile 规则。例如,可以执行 make build 命令编译 mb-apiserver 组件。
项目支持了 Makefile 之后,未来所有的编译、单元测试等项目管理操作,都建议通过执行 Makefile 规则来完成。添加了 Makefile 之后,还需要更新 .air.toml 文件,将其中的 cmd 改成cmd = "make build"。
上述 Makefile 文件中的 Makefile 规则如下表所示。
Makefile 规则语法
为了使你更好地理解 Makefile 的内容,本节将介绍 Makefile 中的重点语法。Makefile 的规则语法如下:
target ...: prerequisites ...
command ...
...target 代表了一个 Makefile 规则,可以是对象文件(object file)、可执行文件或标签(label)。可以使用通配符,当有多个目标时,目标之间用空格分隔。
prerequisites 指定了生成该 target 所需的依赖规则,当有多个依赖规则时,依赖规则之间用空格分隔。
command 指定了该 target 要执行的命令(任意 shell 命令)。在执行 command 之前,默认会先打印出该命令,然后输出命令的结果。如果不想打印出命令,可以在各个 command 前加上 @。command 可以是多条,可以分行书写,但每行都须以 TAB 键开始。此外,如果后一条命令依赖于前一条命令,则这两条命令需要写在同一行,并用分号进行分隔。如果要忽略命令的错误,需要在每个 command 之前加上减号-。
只要 targets 不存在或 prerequisites 中有一个以上的文件比 target 文件新,command 所定义的命令就会被执行。command 将产生所需的文件或执行预期的操作。
Makefile 核心内容解读
为了让你更好地理解 Makefile,本节将详细介绍上述 Makefile 所实现的核心功能。
1. 获取项目根目录的绝对路径。
以下几行 Makefile 代码最终获取了项目根目录的绝对路径:
COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# 项目根目录
PROJ_ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/ && pwd -P))
# 构建产物、临时文件存放目录
OUTPUT_DIR := $(PROJ_ROOT_DIR)/_outputMAKEFILE_LIST 变量是 Makefile 的内置变量,包含了所有被 make 读取的 Makefile 的文件名列表。当前 makefile 的文件名总是位于列表的最后,文件名之间以空格进行分隔。函数 $(lastword <text>) 会返回符串 <text> 中的最后一个单词。函数 $(dir <names...>) 从文件名序列 <names> 中取出目录部分。目录部分是指最后一个反斜杠(/)之前的部分。如果没有反斜杠,那么返回 ./。所以,最终 COMMON_SELF_DIR 变量保存了当前 Makefile 所在目录的路径。
函数 $(shell <操作系统命令>) 执行操作系统命令,并返回操作结果。函数 $(abspath <text>) 将 <text> 中的路径转换成绝对路径,并将转换后的结果返回。所以,最终 PROJ_ROOT_DIR 保存了项目根目录的绝对路径。OUTPUT_DIR 变量定义构建输出文件和临时文件的存放目录。
2. 设置 Makefile 默认规则。
通过 .DEFAULT_GOAL:=all 设置了执行 make 命令时的默认规则。例如:
# 定义默认目标为 all
.DEFAULT_GOAL := all
# 定义 Makefile all 伪目标,可以用于手动调用
.PHONY: all
all: tidy format build add-copyright如果没有指定 .DEFAULT_GOAL,则会默认执行 Makefile 文件中的第一条规则。
上述 all 伪目标指定了当执行 make(不带参数)命令时默认执行的目标,按执行顺序依次为:tidy(自动添加/移除依赖包)、format(格式化 Go 源码)、build(编译源码)、add-copyright(添加版权头信息)。
3. 运行指定规则
有以下规则:
.PHONY: build
build: tidy # 编译源码,依赖 tidy 目标自动添加/移除依赖包.
@go build -v -o $(OUTPUT_DIR)/mb-apiserver $(PROJ_ROOT_DIR)/cmd/mb-apiserver/main.go上述 Makefile 规则中,build 是 Makefile 规则,通过 .PHONY 将其设置为伪目标。.PHONY 是一个特殊的伪目标,其主要作用是告诉 make 编译器,.PHONY 指定的目标不是文件名,而是命令或动作,make 总是会执行相关命令,忽略同名文件的存在。因此,当执行 make build 命令时,会执行 build 规则。
build 规则依赖于 tidy 规则。在执行 build 之前,make 会检查 tidy 是否需要执行。如果 tidy 目标发生变化或者从未被执行过,make 会先执行 tidy。在执行完 tidy 规则之后,便会执行 build 规则中指定的命令:
go build -v -o $(OUTPUT_DIR)/mb-apiserver $(PROJ_ROOT_DIR)/cmd/mb-apiserver/main.go4. 实现幂等删除。
建议在清理临时目录时使用 @-rm -vrf $(OUTPUT_DIR) 语法,- 语法可以确保在临时目录不存在时,Makefile 执行结果仍然成功,实现幂等清理的效果。
5. 使用绝对路径。
建议在编写 Makefile 脚本时,通过 $(PROJ_ROOT_DIR) 变量引用文件的绝对路径,例如:
.PHONY: clean
clean: # 清理构建产物、临时文件等.
@-rm -vrf $(OUTPUT_DIR)这里的 $(OUTPUT_DIR) 是绝对路径。通过引用绝对路径,可以确保路径符合预期,避免使用相对路径带来的潜在问题。
小结
本课程重点讲解了如何为 Go 项目添加关键的初始化文件,以提升开发效率和规范性。首先,通过配置 Air 工具实现代码热加载,借助自动编译和重启功能,开发者无需手动操作即可实时测试修改。安装 Air 后,需自定义.air.toml文件,指定构建命令和运行路径,确保修改后代码即时生效。
其次,版权声明是开源项目的重要环节。使用license工具生成 MIT 协议的 LICENSE 文件,并通过addlicense工具自动为每个源文件添加统一版权头信息。这不仅满足开源合规性要求,也避免手动维护的疏漏,尤其适合协作开发场景。
最后,引入 Makefile 实现构建流程的自动化。通过定义build、format、clean等核心规则,标准化编译、格式化、清理等操作。Makefile 采用绝对路径和伪目标设计,确保跨环境一致性,同时支持依赖管理和幂等操作(如清理临时文件)。结合 Air 配置的更新,开发者仅需执行make build即可触发完整构建链,显著提升协作效率和可维护性。