# Go 项目
# GOPATH
Go 1.11 之后,弱化了 GOPATH 规则,已有代码(很多库肯定是在 1.11 之前建立的)肯定符合这个规则,建议保留 GOPATH 规则,便于维护代码。
建议只使用一个 GOPATH。如果使用多个 GOPATH,编译生效的 bin 目录是在第一个 GOPATH 下。
# 依赖管理
Go 1.11 以上必须使用 Go Modules。
在提交代码时必须提交 go.sum 文件,不建议提交 vendor 目录。
# Makefile 设计
指代码工程项目的管理,一般使用 Makefile 管理 Go 项目。
# 规划实现功能
Go 项目的 Makefile 应实现:格式化代码、静态代码检查、单元测试、代码构建、文件清理、帮助等。如通过 docker 部署,还需要有 docker 镜像打包功能(注意支持不同的 CPU 架构和平台)。
为了能够更好地控制 Makefile 命令的行为,还需要支持 Options。可参考 IAM Makefile:iam/Makefile (opens new window),执行 make help
了解详情。
# 设计结构
建议分层设计,根目录下的 Makefile 聚合所有 Makefile 命令,具体实现则按功能分类放在另外的 Makefile 中。
复杂的 Shell 脚本可供 Makefile 调用,简单的命令可以直接集成在 Makefile 中。
├── Makefile
├── scripts
│ ├── gendoc.sh
│ ├── make-rules
│ │ ├── gen.mk
│ │ ├── golang.mk
│ │ ├── image.mk
│ │ └── ...
└── ...
include call
/Makefile ------> makefile/xxx.mk ---> shell/xxx.sh
| ↑
+--------------------------------------+ call
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编写技巧
善用通配符和自动变量:便于修改、定位具体的 Makefile 文件。
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
2
善用函数:参考 iam/scripts/make-rules (opens new window)。
依赖工具:某个目标命令中用到某个工具,可将该工具放在目标的依赖中。当执行该目标时可以指定检查系统是否安装该工具,没有安装则自动安装。
.PHONY: format
format: tools.verify.golines tools.verify.goimports
@echo "===========> Formating codes"
@$(FIND) -type f -name '*.go' | $(XARGS) gofmt -s -w
@$(FIND) -type f -name '*.go' | $(XARGS) goimports -w -local $(ROOT_PACKAGE)
@$(FIND) -type f -name '*.go' | $(XARGS) golines -w --max-len=120 --reformat-tags --shorten-comments --ignore-generated .
# ...
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
# ...
.PHONY: install.golines
install.golines:
@$(GO) get -u github.com/segmentio/golines
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
常用功能放在 /Makefile 中,不常用的放在分类 Makefile 中。
编写可扩展的 Makefile:
- 在不改变 Makefile 结构的情况下添加新功能。
- 扩展项目时新功能可自动纳入到 Makefile 现有逻辑中。
# 执行 make go.build 时可构建 cmd/ 目录下的所有组件,当有新组件添加时,make go.build 仍然能够构建新增的组件。
COMMANDS ?= $(filter-out %.md, $(wildcard ${ROOT_DIR}/cmd/*))
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
.PHONY: go.build
go.build: go.build.verify $(addprefix go.build., $(addprefix $(PLATFORM)., $(BINS)))
.PHONY: go.build.%
go.build.%:
$(eval COMMAND := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Building binary $(COMMAND) $(VERSION) for $(OS) $(ARCH)"
@mkdir -p $(OUTPUT_DIR)/platforms/$(OS)/$(ARCH)
@CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/platforms/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) $(ROOT_PACKAGE)/cmd/$(COMMAND)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将所有输出存放在一个目录下,方便清理和查找:例如 Go 编译后的二进制文件、测试覆盖率数据等。清理只需:
.PHONY: go.clean
go.clean:
@echo "===========> Cleaning all build output"
@-rm -vrf $(OUTPUT_DIR)
2
3
4
使用带层级的命名方式:可以实现目标分组管理。当 Makefile 有大量目标时,通过分组可以更好地管理;分组方便理解,通过组名一眼识别出该目标的功能类别;而且大大减小目标重名的概率。
.PHONY: gen.run
gen.run: gen.clean gen.errcode gen.docgo
.PHONY: gen.errcode
gen.errcode: gen.errcode.code gen.errcode.doc
.PHONY: gen.errcode.code
gen.errcode.code: tools.verify.codegen
...
.PHONY: gen.errcode.doc
gen.errcode.doc: tools.verify.codegen
...
2
3
4
5
6
7
8
9
10
11
12
做好目标拆分:将安装工具拆分成两个,即验证工具是否已安装和安装工具,可提高灵活性。
gen.errcode.code: tools.verify.codegen
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
.PHONY: install.codegen
install.codegen:
@$(GO) install ${ROOT_DIR}/tools/codegen/codegen.go
2
3
4
5
6
7
8
设置 OPTIONS:把一些可变的功能通过 OPTIONS 来控制。
# /Makefile 中定义 USAGE_OPTIONS 。定义 USAGE_OPTIONS 可以使开发者在执行 make help 后感知到此 OPTION,并根据需要进行设置。
define USAGE_OPTIONS
Options:
...
BINS The binaries to build. Default is all of cmd.
...
...
V Set to 1 enable verbose build. Default is 0.
endef
export USAGE_OPTIONS
# scripts/make-rules/common.mk 通过判断有没有设置 V 选项,来选择不同的行为
ifndef V
MAKEFLAGS += --no-print-directory
endif
# 还可以通过下面的方法来使用 V
ifeq ($(origin V), undefined)
MAKEFLAGS += --no-print-directory
endif
# 或在 Makefile 中是直接使用的 Option
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
# ...
go.build: go.build.verify $(addprefix go.build., $(addprefix $(PLATFORM)., $(BINS)))
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
定义环境变量:多处同时生效,避免重复工作。
GO := go
GO_SUPPORTED_VERSIONS ?= 1.13|1.14|1.15|1.16|1.17
GO_LDFLAGS += -X $(VERSION_PACKAGE).GitVersion=$(VERSION) \
-X $(VERSION_PACKAGE).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PACKAGE).GitTreeState=$(GIT_TREE_STATE) \
-X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
ifneq ($(DLV),)
GO_BUILD_FLAGS += -gcflags "all=-N -l"
LDFLAGS = ""
endif
GO_BUILD_FLAGS += -tags=jsoniter -ldflags "$(GO_LDFLAGS)"
...
FIND := find . ! -path './third_party/*' ! -path './vendor/*'
XARGS := xargs --no-run-if-empty
2
3
4
5
6
7
8
9
10
11
12
13
14
调用自身:比如 A-Target 目标命令中需要完成操作 B-Action,而操作 B-Action 已经通过伪目标 B-Target 实现过。为了达到最大的代码复用度,最好的方式是在 A-Target 的命令中执行 B-Target。
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
# 默认情况下,Makefile 在切换目录时会输出:
# make tools.install.codegen
# ===========> Installing codegen
# make[1]: Entering directory `/home/colin/workspace/golang/src/github.com/marmotedu/iam'
# make[1]: Leaving directory `/home/colin/workspace/golang/src/github.com/marmotedu/iam'
# 可以设置 MAKEFLAGS += --no-print-directory 来禁止 Makefile 打印 Entering directory 等信息。
2
3
4
5
6
7
8
9
10