前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang工程通用构建方式

golang工程通用构建方式

原创
作者头像
周亮宇
发布2019-08-19 11:11:04
2.1K0
发布2019-08-19 11:11:04
举报
文章被收录于专栏:golang工程实践golang工程实践

在团队多人合作开发golang工程时,我们经常会遇到下面的问题:

  • 线上运行的应用程序版本号对应工程代码的哪个分支,哪个commit
  • 线上运行的二进制文件?上线的服务是使用golang的哪个版本编译的?
  • A引入了bouk/staticfiles等工具将工程目录下的文件嵌入到二进制程序中,B如何方便的在修改文件后同步更新asset文件?
  • 如何不口口相传的告知团队成员如何编译工程中众多的应用?

要解决上述的问题,我们需要一个构建脚本/工具来自动化的在开发、持续集成、预发布阶段提供下列功能:

  • 提供无学习成本的简单命令完成编译(make build)、嵌入文件(make asset)、代码生成(make gen)、本地执行(make run)、单元测试(make test)、清理(make clean)、制作镜像(make image)等诸多动作;
  • 在构建开始前能检查各种依赖的工具/环境是否符合条件,例如:golang的版本,是否安装了revive代码静态扫描工具,是否安装了符合条件的docker版本等等;
  • 跨平台支持以符合团队成员的各种开发环境;
  • 编译过程中自动的将git的commit/branch/tag、编译的时间、golang的版本、os等信息嵌入程序中;

不幸的是,golang官方以及社区目前并没有一个类似java世界中的maven/gradle,rust世界中的cargo,c/c++世界中的cmake等工具来支持上述的诉求:

  • bash脚本跨平台不友好;
  • bazel不好用我也不无脑粉google神教;
  • maven/gradle的golang插件对没有java经验的团队成员学习成本高;

跟随社区大流,使用gnu/make以及Makefile来做为当下golang工程的构建工具似乎是一个最佳选择。但是Makefile的编写同样有不小的学习成本,因此,在这里我将经过多个大小工程的全套Makefile分享给大家。

工程文件结构

工程整体结构如下:

image.png
image.png

首先,使用go mod init example.com/group/repo初始化golang工程后,添加.gitignore文件,设置忽略output目录下的所有文件,该目录在工程编译后输出不同平台可执行文件以及单元测试后输出单元测试报告,这些内容无需添加到git中。

主Makefile文件

根目录下Makefile内容如下:

代码语言:txt
复制
.PHONY: all
all: lint test build

# ==============================================================================
# Build Options

ROOT_PACKAGE=github.com/choujimmy/gomakefile
VERSION_PACKAGE=$(ROOT_PACKAGE)/pkg/app/version

# ==============================================================================
# Includes

include build/lib/common.mk
include build/lib/golang.mk
include build/lib/image.mk

# ==============================================================================
# Targets

## build: Build source code for host platform.
.PHONY: build
build:
	@$(MAKE) go.build

## build.all: Build source code for all platforms.
.PHONY: build.all
build.all:
	@$(MAKE) go.build.all
	
## image: Build docker images and push to registry.
.PHONY: image
image:
	@$(MAKE) image.push

## clean: Remove all files that are created by building.
.PHONY: clean
clean:
	@$(MAKE) go.clean

## lint: Check syntax and styling of go sources.
.PHONY: lint
lint:
	@$(MAKE) go.lint

## test: Run unit test.
.PHONY: test
test:
	@$(MAKE) go.test

## help: Show this help info.
.PHONY: help
help: Makefile
	@echo -e "\nUsage: make <OPTIONS> ... <TARGETS>\n\nTargets:"
	@sed -n 's/^##//p' $< | column -t -s ':' |  sed -e 's/^/ /'

其中,ROOT_PACKAGE变量修改为实际的工程模块导入包名,每个make的target上使用两个##号开头的注释内容供help的target解析生成make的帮助说明,实际效果如下:

代码语言:txt
复制
$ make help

Usage: make <OPTIONS> ... <TARGETS>

Targets:
  build       Build source code for host platform.
  build.all   Build source code for all platforms.
  image       Build docker images and push to registry.
  clean       Remove all files that are created by building.
  lint        Check syntax and styling of go sources.
  test        Run unit test.
  help        Show this help info.

common.mk

build/lib/common.mk文件内容如下:

代码语言:txt
复制
# ==============================================================================
# Makefile helper functions for common
#

SHELL := /bin/bash

COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))

ifeq ($(origin ROOT_DIR),undefined)
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/../.. && pwd -P))
endif
ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := $(ROOT_DIR)/output
endif
ifeq ($(origin TOOLS_DIR),undefined)
TOOLS_DIR := $(OUTPUT_DIR)/tools
endif
ifeq ($(origin TMP_DIR),undefined)
TMP_DIR := $(OUTPUT_DIR)/tmp
endif

# set the version number. you should not need to do this
# for the majority of scenarios.
ifeq ($(origin VERSION), undefined)
VERSION := $(shell git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2' )
endif
export VERSION

COMMA := ,
SPACE :=
SPACE +=

该文件作为依赖包含在根目录下的Makefile文件中,定义了工程通用的路径变量以及根据git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2'命名的结果获取工程的git代码库状态作为工程的版本信息。

golang.mk

build/lib/golang.mk文件内容如下:

代码语言:txt
复制
# ==============================================================================
# Makefile helper functions for golang
#

GO := go
GO_SUPPORTED_VERSIONS ?= 1.11|1.12
GO_LDFLAGS += -X $(VERSION_PACKAGE).GitVersion=$(VERSION) -X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')

ifeq ($(GOOS),windows)
	GO_OUT_EXT := .exe
endif

ifeq ($(ROOT_PACKAGE),)
	$(error the variable ROOT_PACKAGE must be set prior to including golang.mk)
endif

ifeq ($(origin PLATFORM), undefined)
	ifeq ($(origin GOOS), undefined)
		GOOS := $(shell go env GOOS)
	endif
	ifeq ($(origin GOARCH), undefined)
		GOARCH := $(shell go env GOARCH)
	endif
	PLATFORM := $(GOOS)_$(GOARCH)
else
	GOOS := $(word 1, $(subst _, ,$(PLATFORM)))
	GOARCH := $(word 2, $(subst _, ,$(PLATFORM)))
endif

GOPATH := $(shell go env GOPATH)
ifeq ($(origin GOBIN), undefined)
	GOBIN := $(GOPATH)/bin
endif

PLATFORMS ?= darwin_amd64 windows_amd64 linux_amd64
COMMANDS ?= $(wildcard ${ROOT_DIR}/cmd/*)
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))

ifeq (${COMMANDS},)
  $(error Could not determine COMMANDS, set ROOT_DIR or run in source dir)
endif
ifeq (${BINS},)
  $(error Could not determine BINS, set ROOT_DIR or run in source dir)
endif

.PHONY: go.build.verify
go.build.verify:
ifneq ($(shell $(GO) version | grep -q -E '\bgo($(GO_SUPPORTED_VERSIONS))\b' && echo 0 || echo 1), 0)
	$(error unsupported go version. Please make install one of the following supported version: '$(GO_SUPPORTED_VERSIONS)')
endif

.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)/$(OS)/$(ARCH)
	@CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) $(GO) build -o $(OUTPUT_DIR)/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) -ldflags "$(GO_LDFLAGS)" $(ROOT_PACKAGE)/cmd/$(COMMAND)

.PHONY: go.build
go.build: go.build.verify $(addprefix go.build., $(addprefix $(PLATFORM)., $(BINS)))

.PHONY: go.build.all
go.build.all: go.build.verify $(foreach p,$(PLATFORMS),$(addprefix go.build., $(addprefix $(p)., $(BINS))))

.PHONY: go.clean
go.clean:
	@echo "===========> Cleaning all build output"
	@rm -rf $(OUTPUT_DIR)

.PHONY: go.lint.verify
go.lint.verify: go.build.verify
ifeq (,$(wildcard $(GOBIN)/revive))
	@echo "===========> Installing revive"
	@GO111MODULE=off $(GO) get -u github.com/mgechev/revive
endif

.PHONY: go.lint
go.lint: go.lint.verify
	@echo "===========> Run revive to lint source codes"
	@$(GOBIN)/revive -config $(ROOT_DIR)/build/linter/revive.toml \
        -exclude vendor/... \
        ./...

.PHONY: go.test.verify
go.test.verify: go.build.verify
ifeq (,$(wildcard $(GOBIN)/go-junit-report))
	@echo "===========> Installing go-junit-report"
	@GO111MODULE=off $(GO) get -u github.com/jstemmer/go-junit-report
endif

.PHONY: go.test
go.test: go.test.verify
	@echo "===========> Run unit test"
	@mkdir -p $(OUTPUT_DIR)
	@$(GO) test -count=1 -timeout=10m -short -v ./... 2>&1 | tee >($(GOBIN)/go-junit-report --set-exit-code >$(OUTPUT_DIR)/report.xml)

在这里我们定义了golang工程常用的编译、单元测试、代码检查等目标,其中编译包含:

  • make build: 编译当目前操作系统系统目标的可执行文件
  • make build.all: 同时编译macos/windows/linux的64位可执行程序

在使用上,还支持指定options,例如仅编译linux/amd64的可执行程序:

代码语言:txt
复制
$ make build.all PLATFORMS="linux_amd64"
===========> Building binary app1 a6ac381 for linux amd64
===========> Building binary app2 a6ac381 for linux amd64

仅编译app1的windows和linux的64位可执行程序:

代码语言:txt
复制
$ make build.all PLATFORMS="linux_amd64 windows_amd64" BINS="app1"
===========> Building binary app1 a6ac381 for linux amd64
===========> Building binary app1 a6ac381 for windows amd64

使用注意:

  • 必须按照golang工程建议的规范在根目录下的cmd目录下为每一个可执行程序建立单独包
  • 使用go module作为依赖管理工具,仅支持golang的1.11,1.12版本
  • 代码检查工具使用的是revive,且示例工程中的规则文件build/linter/revive.toml中的规则非常严谨,各位看官自行修改

image.mk

build/lib/image.mk文件内容如下:

代码语言:txt
复制
# ==============================================================================
# Makefile helper functions for docker image
#

DOCKER := docker
DOCKER_SUPPORTED_VERSIONS ?= 17|18

REGISTRY_PREFIX ?= jimmychou

# Determine image files by looking into hack/docker/*.Dockerfile
IMAGE_FILES=$(wildcard ${ROOT_DIR}/build/docker/*.Dockerfile)
# Determine images names by stripping out the dir names
IMAGES=$(foreach image,${IMAGE_FILES},$(subst .Dockerfile,,$(notdir ${image})))

ifeq (${IMAGES},)
  $(error Could not determine IMAGES, set ROOT_DIR or run in source dir)
endif

.PHONY: image.build.verify
image.build.verify:
ifneq ($(shell $(DOCKER) -v | grep -q -E '\bversion ($(DOCKER_SUPPORTED_VERSIONS))\b' && echo 0 || echo 1), 0)
	$(error unsupported docker version. Please make install one of the following supported version: '$(DOCKER_SUPPORTED_VERSIONS)')
endif
	@echo "===========> Docker version verification passed"

.PHONY: image.build
image.build: image.build.verify go.build.verify $(addprefix image.build., $(IMAGES))

.PHONY: image.push
image.push: image.build.verify go.build.verify $(addprefix image.push., $(IMAGES))

.PHONY: image.build.%
image.build.%: go.build.linux_amd64.%
	@echo "===========> Building $* $(VERSION) docker image"
	@cat $(ROOT_DIR)/build/docker/$*.Dockerfile\
		| sed "s#{{REGISTRY_PREFIX}}#$(REGISTRY_PREFIX)#g" >tmp_$*.Dockerfile
	@$(DOCKER) build --pull -t $(REGISTRY_PREFIX)/$*:$(VERSION) -f tmp_$*.Dockerfile .
	@rm tmp_$*.Dockerfile

.PHONY: image.push.%
image.push.%: image.build.%
	@echo "===========> Pushing $* $(VERSION) image to $(REGISTRY_PREFIX)"
	@$(DOCKER) push $(REGISTRY_PREFIX)/$*:$(VERSION)

该文件提供对本地构建容器镜像以及推送容器镜像等的支持,在使用前请先修改REGISTRY_PREFIX变量的值。

示例

makefile的全部文件以及工程示例已在github上建立示范工程,地址: gomakefile

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 工程文件结构
  • 主Makefile文件
  • common.mk
  • golang.mk
  • image.mk
  • 示例
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档