makefile快速开始(go项目makefile编写实战)
Makefile是一个名为`make`软件所需要的脚本文件,该脚本文件可以指导make软件控制gcc等工具链去编译工程文件最终得到可执行文件,几乎所有的Linux发行版都内置了GNU-Make软件,VScode等多种IED也内置了Make程序。
文章目录
一、makefile快速开始
1. 什么是makefile、make
Makefile是一个名为make软件所需要的脚本文件,该脚本文件可以指导make软件控制gcc等工具链去编译工程文件最终得到可执行文件,几乎所有的Linux发行版都内置了GNU-Make软件,VScode等多种IED也内置了Make程序。
你见到的xxx.mk文件或者Makefile都统称为Makefile脚本文件。大多数的make都支持“makefile”和“Makefile”这两种默认文件名称。
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
make是一个命令工具,它解释Makefile 中的指令。Makefile用来调用各种命令做自动化构建工具非常方便。
1.1 make命令 工作流程
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。我们编写 Makefile 的时可以使用的文件的名称 “GNUmakefile” 、“makefile” 、“Makefile” ,make 执行时回去寻找 Makefile 文件,推荐使用Makefile。
1.2 关于程序的编译和链接
一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(LibraryFile),也就是 .lib文件,在UNIX下,是Archive File,也就是.a文件。
总结: 源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)。
2. 掌握 Makefile 常用语法基础
2.1 Makefile 规则语法
规则:用于说明如何生成一个或多个目标文件
规则的格式:
targets:prerequisites
command
目标: 依赖
[tab键]命令
-
target 目标
可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(伪目标)。 -
prerequisites
生成该target所依赖的文件或target。生成目标所要依赖的文件,可能不止一个。 -
command
该target要执行的命令(任意的shell命令)。生成目标所要执行的命令,注意所有的命令必须要以tab 键开头。
总结:target这一个或多个的目标文件依赖于prerequisites中的文件, 其生成规则定义在command中。
2.2 伪目标 | Makefile中.PHONY的作用
.PHONY(伪目标)
伪目标是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行所在规则定义的命令,有时也可以将一个伪目标称为标签。伪目标通过PHONY来指明。
如果我们指定的目标不是创建目标文件,而是使用makefile执行一些特定的命令,例如:
clean:
rm *.o temp
我们希望,只要输入make clean后,rm *.o temp命令就会执行。但是,当前目录中存在一个和指定目标重名的文件时,例如clean文件,结果就不是我们想要的了。输入make clean后,rm *.o temp 命令一定不会被执行。
解决的办法:将目标clean定义成伪目标就成了。 无论当前目录下是否存在clean这个文件,输入make clean后,rm *.o temp命令都会被执行。
这种做法的带来的好处还不止此,它同时提高了make的执行效率,因为将clean定义成伪目标后,make的执行程序不会试图寻找clean的隐含规则。
举例如下:
clean:
rm -rf $(CLEAN) $(BINS)
.PHONY: clean
2.3 变量赋值
Makefile变量的定义和使用
参考URL: http://c.biancheng.net/view/7096.html
变量的使用在我们的 Makefile 编写中还是非常广泛的,可以说我们的 Makefile 中必不可少的东西。
Makefile 的变量的四种基本赋值方式:
- 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
- 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
- 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
- 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。
- 简单赋值
x:=foo
y:=$(x)b
x:=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
:=与我们实际上更符合我们平时写代码的思维,也就是顺序声明变量,使用这种赋值方式,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。
比较常用。
- 递归赋值
x=foo
y=$(x)b
x=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
结果:
[root@centos7 ~]# make test
y=>newb
x=>new
使用= 需要注意2点:
1)这种赋值方式是有先后顺序的,后面的赋值会覆盖掉前面的赋值。因此这种赋值方式,比较好用。不太符合我们的书写逻辑。
2) 右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。
x = $(y)
y = hello
- 条件赋值
x:=foo
y:=$(x)b
x?=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
- 追加赋值
x:=foo
y:=$(x)b
x?=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
- 追加赋值
x:=foo
y:=$(x)b
x+=$(y)
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
结果:
[root@centos7 ~]# make test
y=>foob
x=>foo foob
+=的含义是:为变量追加值,但是如果变量之前没有没定义过,那么+=会自动转化为=,如果前面变量的定义形式为:=那么+=会以:=为其赋值。
+= 连接的字符串中间有空格。
总结:
Makefile中并不要求赋值运算符两边一定要有空格或者无空格,这一点比shell的格式要求要松一些。
不同的赋值方式会产生不同的结果,我们使用的时候应该根据具体的情况选择相应的赋值规则。
自动变量(Automatic Variables)
除了上面提到的几种形式的变量外,make还提供了一系列的自动变量,这些自动变量是根据make自动根据规则生成的,不需要显示指出相应的文件或目标名。
下面列出一些比较常见的自动变量的形式:
(1)$@
$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代foo。
a.txt b.txt:
touch $@
等同于下面的写法。
a.txt:
touch a.txt
b.txt:
touch b.txt
(2)$*
如果目标文件的后缀是make所识别的,那么"$*"就是除了后缀的那一部分
内置变量(Implicit Variables)
Make命令提供一系列内置变量,比如, ( C C ) 指 向 当 前 使 用 的 编 译 器 , (CC) 指向当前使用的编译器, (CC)指向当前使用的编译器,(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。
output:
$(CC) -o output input.c
2.4 makefile条件语句
日常使用 Makefile 编译文件时,要根据判断,分条件执行语句。
| 关键字 | 功能 |
|---|---|
| ifeq | 判断参数是否不相等,相等为 true,不相等为 false |
| ifneq | 判断参数是否不相等,不相等为 true,相等为 false |
| ifdef | 判断是否有值,有值为 true,没有值为 false |
| ifndef | 判断是否有值,没有值为 true,有值为 false |
2.5 Makefile 常用函数
参考URL: http://www.ruanyifeng.com/blog/2015/02/make.html
Makefile 还可以使用函数,格式如下。
$(function arguments)
# 或者
${function arguments}
(1)shell 函数
ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := $(ROOT_DIR)/_output
$(shell mkdir -p $(OUTPUT_DIR))
endif
shell 函数用来执行 shell 命令
(2) makefile word函数
$(word 1,hello jello yello)
上面的语句执行后的结果为hello,意为取字符串的第一个单词
用来取单词的函数
(3) subst
用法是$(subst FROM,TO,TEXT),即将TEXT中的东西从FROM变为TO
(4) eval函数
函数原型:
$(eval text)
它的意思是 text 的内容将作为makefile的一部分而被make解析和执行。
需要注意的是该函数在执行时会对它的参数进行两次展开,第一次展开是由函数本身完成,第二次是函数展开后的结果被作为makefile内容时由make解析时展开.
(5) addprefix 函数
addprefix 是makefile中的函数,是添加前缀的函数
$(addprefix PREFIX,NAMES…)
函数名称:加前缀函数—addprefix。
函数功能:为“NAMES…”中的每一个文件名添加前缀“PREFIX”。参数“NAMES…”
是空格分割的文件名序列,将“SUFFIX”添加到此序列的每一个文件名
之前。
返回值:以单空格分割的添加了前缀“PREFIX”的文件名序列。
例如:
$(addprefix src/,foo bar)
返回值为“src/foo src/bar”.
(6) wildcard 函数
在Makefile规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN…) 。
一般我们可以使用“$(wildcard *.c)”来获取工作目录下的所有的.c文件列表。
(7) foreach函数
foreach作为makefile中的函数,相当于一个循环函数。循环处理文件列表。
$(foreach var text commond)
var:局部变量
text:文件列表,空格隔开,每一次取一个值赋值为变量var
commond:对var变量进行操作(一般会使用var变量,不然没意义),每次操作结果都会以空格隔开,最后返回空格隔开的列表。
(8)notdir函数
notdir用于去掉文件的绝对路径,只保留文件名。剥离文件的绝对路径,只保留文件名。
$(notdir 文件列表)
(9)反过滤函数—filter-out。
$(filter-out PATTERN…,TEXT)
函数名称 :反过滤函数—filter-out。
函数功能 :和“filter”函数实现的功能相反。过滤掉字串“TEXT”中所有符合模式“PATTERN”的单词,保留所有不符合此模式的单词。可以有多个模式。存在多个模式时,模式表达式之间使用空格分割。
返回值 :空格分割的“TEXT”字串中所有不符合模式“PATTERN”的字串。
函数说明: “filter-out”函数也可以用来去除一个变量中的某些字符串(实现和“filter”函数相反)。
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects))
实现了去除变量“objects”中“mains”定义的字串(文件名)功能。它的返回值为“foo.o bar.o”。
6-9 实战综合demo
# Determine image files by looking into build/docker/*/Dockerfile
IMAGES_DIR ?= $(wildcard ${ROOT_DIR}/build/docker/*)
# Determine images names by stripping out the dir names
IMAGES ?= $(filter-out tools,$(foreach image,${IMAGES_DIR},$(notdir ${image})))
| 函数名 | 功能描述 |
|---|---|
| $(origin ) | 告诉变量的“出生情况”,有如下返回值:
|
| $(addsuffix ,<names…>) | 把后缀加到中的每个单词后面,并返回加过后缀的文件名序列。 |
| $(addprefix ,<names…>) | 把前缀加到中的每个单词前面,并返回加过前缀的文件名序列。 |
| $(wildcard ) | 扩展通配符,例如:$(wildcard ${ROOT_DIR}/build/docker/*) |
| $(word , |
取字符串 |
| $(subst ,, |
把字串 |
| $(eval |
将 |
| $(firstword |
取字符串 |
| $(lastword |
取字符串 |
| $(abspath |
将 |
| $(shell cat foo) | 执行操作系统命令,并返回操作结果 |
| $(info <text …>) | 输出一段信息 |
| $(warning <text …>) | 出一段警告信息,而 make 继续执行 |
| $(error <text …>) | 产生一个致命的错误,<text …> 是错误信息 |
| $(filter <pattern…>, |
以模式过滤 |
| $(filter-out <pattern…>, |
以模式过滤 |
| $(dir <names…>) | 从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(/)之前的部分。返回文件名序列的目录部分。 |
| $(notdir <names…>) | 从文件名序列中取出非目录部分。非目录部分是指最後一个反斜杠(/)之后的部分。返回文件名序列的非目录部分。 |
| $(strip ) | 去掉字串中开头和结尾的空字符,并返回去掉空格后的字符串 |
| $(suffix <names…>) | 从文件名序列中取出各个文件名的后缀。返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。 |
| $(foreach ,, |
把参数中的单词逐一取出放到参数所指定的变量中,然后再执行 |
2.6 makefile日志打印调试
使用info/warning/error增加调试信息
info
$(info “here add the debug info”)
注,info信息,不打印信息所在行号
warning
$(warning “here add the debug info”)
error
$(error “error: this will stop the compile”)
这个可以停止当前makefile的编译
打印变量的值
$(info $(TARGET_DEVICE) )
使用echo增加调试信息
注:echo只能在target:后面的语句中使用,且前面是个TAB,
验证demo:
parent_dir_fname:=$(lastword $(MAKEFILE_LIST))
$(info "xxx" $(parent_dir_fname) )
all:
@echo $(parent_dir_fname)
2.7 其他常见必看基础
- .DEFAULT_GOAL := all
all应该是make文件中定义的第一个目标,或者被指定为默认目标。
这是一个惯例,你提供了一个“全部”规则,GNU Make版本3.81引入了一个名为.DEFAULT_GOAL的特殊变量,可用于告知如果在命令行中未指定目标,应该构建哪个目标(或目标)。
.DEFAULT_GOAL := all
通过.DEFAULT_GOAL实际上可以在Makefile中的其他位置读取$(.DEFAULT_GOAL)的值。测试表明,它是否明确设置或者是否通过选择第一条规则来确定没有区别。
all目标通常是makefile中的第一个目标,因为如果你只是在命令行写入make ,而不指定目标,它将会build立第一个目标。 你期望它是all 。
all通常也是一个.PHONY目标。
GNU Make手册给出了标准目标列表中 all的清晰定义。
如果Makefile的作者遵循这个约定,那么目标应该是:
- 编译整个程序,但不能编译文档。
- 成为默认的目标。all应该是make文件中定义的第一个目标,或者被指定为默认目标 .DEFAULT_GOAL := all
- include 引用其它makefile文件
include scripts/make-rules/common.mk
- Makefile 变量 MAKEFILE_LIST
makefile使用MAKEFILE_LIST获取inlcude文件所在的目录
parent_dir_fname:=$(shell pwd)/$(lastword $(MAKEFILE_LIST))
all:
@echo $(parent_dir_fname)
利用shell dirname命令
dirname命令去除文件名中的非目录部分,仅显示与目录有关的内容。dirname命令读取指定路径名保留最后一个/及其后面的字符,删除其他部分,并写结果到标准输出。如果最后一个/后无字符,dirname 命令使用倒数第二个/,并忽略其后的所有字符
-
@$(MAKE)
make 定义了很多默认变量,像常用的命令或者是命令选项之类的,什么CC啊,CFLAGS啊之类。
${MAKE} 就是预设的 make 这个命令的名称(或者路径)。make -p可以查看所有预定义的变量的当前值。 -
makefile export的用法 //设置环境变量
export FLASK_ENV=dev
export FLASK_DEBUG=1
dev:
@echo $(FLASK_ENV)
@echo $(FLASK_DEBUG)
运行make dev时则返回:
$ make dev
dev
需求:在一个目标中设置一个环境变量,在依赖目标或命令中使用该环境变量。
因为在依赖目标或命令中经常需要根据不同的变量值做不同的处理。
设置环境变量值有三种方法:
- Makefile中直接: export 变量
- 目标命令中,在命令前:变量=值 command
- 单独定义一个目标(不能有命令),目标1: export 变量=值
-
makefile define
对makefile里的define补充说明
参考URL: https://blog.csdn.net/seven_tree/article/details/106060613
makefile里可能会用到define来打包一些可能会重用的指令,但是因为makefile里实际上会重用代码的情况并不多 -
Makefile之文件名操作函数 dir
取目录函数——dir
解释: 从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分
示例: $(dir src/foo.c hacks)返回值是“src/, ./”。
注意: 函数说明:如果文件名中没有斜线,认为此文件为当前目录(“./” )下的文件。
- Makefile之origin函数
origin 函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:
$(origin )
注意,是变量的名字,不应该是引用。所以你最好不要在中使用“$”字符。Origin 函数会以其返回值来告诉你这个变量的“出生情况”,下面,是 origin函数的返回值:
“undefined”
如果从来没有定义过,origin 函数返回这个值“undefined”。
ifeq ($(origin VERSION), undefined)
VERSION := $(shell git describe --tags --always --match='v*')
endif
- make 定义了很多默认变量,${MAKE} 就是预设的 make 这个命令的名称(或者路径)。
make -p 可以查看所有预定义的变量的当前值。
@表示在执行命令时不输出命令本身(不回显),只输出命令执行的结果。
3. makefile demo举例
我们以fastdfs libfastcommon项目举例:
.SUFFIXES: .c .o .lo
COMPILE = $(CC) $(CFLAGS)
INC_PATH =
LIB_PATH = $(LIBS)
TARGET_LIB = $(TARGET_PREFIX)/$(LIB_VERSION)
FAST_SHARED_OBJS = hash.lo chain.lo shared_func.lo ini_file_reader.lo \
logger.lo sockopt.lo base64.lo sched_thread.lo \
http_func.lo md5.lo pthread_func.lo local_ip_func.lo \
avl_tree.lo ioevent.lo ioevent_loop.lo fast_task_queue.lo \
fast_timer.lo process_ctrl.lo fast_mblock.lo \
connection_pool.lo fast_mpool.lo fast_allocator.lo \
fast_buffer.lo multi_skiplist.lo flat_skiplist.lo \
system_info.lo fast_blocked_queue.lo id_generator.lo \
char_converter.lo char_convert_loader.lo common_blocked_queue.lo \
multi_socket_client.lo skiplist_set.lo json_parser.lo \
buffered_file_writer.lo
FAST_STATIC_OBJS = hash.o chain.o shared_func.o ini_file_reader.o \
logger.o sockopt.o base64.o sched_thread.o \
http_func.o md5.o pthread_func.o local_ip_func.o \
avl_tree.o ioevent.o ioevent_loop.o fast_task_queue.o \
fast_timer.o process_ctrl.o fast_mblock.o \
connection_pool.o fast_mpool.o fast_allocator.o \
fast_buffer.o multi_skiplist.o flat_skiplist.o \
system_info.o fast_blocked_queue.o id_generator.o \
char_converter.o char_convert_loader.o common_blocked_queue.o \
multi_socket_client.o skiplist_set.o json_parser.o \
buffered_file_writer.o
HEADER_FILES = common_define.h hash.h chain.h logger.h base64.h \
shared_func.h pthread_func.h ini_file_reader.h _os_define.h \
sockopt.h sched_thread.h http_func.h md5.h local_ip_func.h \
avl_tree.h ioevent.h ioevent_loop.h fast_task_queue.h \
fast_timer.h process_ctrl.h fast_mblock.h \
connection_pool.h fast_mpool.h fast_allocator.h \
fast_buffer.h skiplist.h multi_skiplist.h flat_skiplist.h \
skiplist_common.h system_info.h fast_blocked_queue.h \
php7_ext_wrapper.h id_generator.h char_converter.h \
char_convert_loader.h common_blocked_queue.h \
multi_socket_client.h skiplist_set.h fc_list.h \
json_parser.h buffered_file_writer.h
ALL_OBJS = $(FAST_STATIC_OBJS) $(FAST_SHARED_OBJS)
ALL_PRGS =
SHARED_LIBS = libfastcommon.so
STATIC_LIBS = libfastcommon.a
ALL_LIBS = $(SHARED_LIBS) $(STATIC_LIBS)
all: $(ALL_OBJS) $(ALL_PRGS) $(ALL_LIBS)
libfastcommon.so: $(FAST_SHARED_OBJS)
$(COMPILE) -o $@ -shared $(FAST_SHARED_OBJS) $(LIB_PATH)
libfastcommon.a: $(FAST_STATIC_OBJS)
ar rcs $@ $(FAST_STATIC_OBJS)
.o:
$(COMPILE) -o $@ $< $(FAST_STATIC_OBJS) $(LIB_PATH) $(INC_PATH)
.c:
$(COMPILE) -o $@ $< $(FAST_STATIC_OBJS) $(LIB_PATH) $(INC_PATH)
.c.o:
$(COMPILE) -c -o $@ $< $(INC_PATH)
.c.lo:
$(COMPILE) -c -fPIC -o $@ $< $(INC_PATH)
install:
mkdir -p $(TARGET_LIB)
mkdir -p $(TARGET_PREFIX)/lib
mkdir -p $(TARGET_PREFIX)/include/fastcommon
install -m 755 $(SHARED_LIBS) $(TARGET_LIB)
install -m 644 $(HEADER_FILES) $(TARGET_PREFIX)/include/fastcommon
if [ ! -e $(TARGET_PREFIX)/lib/libfastcommon.so ]; then ln -s $(TARGET_LIB)/libfastcommon.so $(TARGET_PREFIX)/lib/libfastcommon.so; fi
clean:
rm -f $(ALL_OBJS) $(ALL_PRGS) $(ALL_LIBS)
C语言下“.lo”和“.o” .a 的区别
.lo文件是libtool生成的文件,被libtool用来生成共享库的;而.o 文件是标准的目标文件。
.a 为静态库,是好多个.o合在一起,用于静态连接
lo: 使用libtool编译出的目标文件,其实就是在o文件中添加了一些信息
创建.a库文件和.o库文件:
$ gcc -c mylib.c
$ ar -r mylib.a mylib.o
makefile 中 $@ $^ %< 使用
makefile 中 $@ $^ %< 使用
参考URL: https://www.cnblogs.com/baiduboy/p/6849587.html
Makefile有三个非常有用的变量。分别是 @ , @, @,^,$<代表的意义分别是:
@ − − 目 标 文 件 , @--目标文件, @−−目标文件,^–所有的依赖文件,$<–第一个依赖文件。
二、go项目makefile编写实战
代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。
Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。
本节,我们将介绍如何使用Makefile在Go(GoLang)支持的任何操作系统 和平台上运行 ,构建和编译应用程序。
1. 如何编写高质量的Makefile?
14 | 项目管理:如何编写高质量的Makefile?
参考URL: https://time.geekbang.org/column/article/388920
要写出一个优雅的 Go 项目,不仅仅是要开发一个优秀的 Go 应用,而且还要能够高效地管理项目。有效手段之一,就是通过 Makefile 来管理我们的项目,这就要求我们要为项目编写 Makefile 文件。
本节,推荐查看原文!
2. go项目makefile编写实战
思路:
- 全局Makefile定义
ROOT_PACKAGE=mygoapp
VERSION_PACKAGE=mygoapp/pkg/version common.mk中,自定义一些通用的变量- 在
golang.mk中为项目定义的 go相关环境变量。
定义伪目标 go.build.% image.mk中,定义 docker镜像制作相关。
1)定义相应docker镜像命令一些变量。
2)伪目标image.verify 通过shell docker version 结合awk取版本号,然后判断docker api版本是否满足要求(大于1.32)。
Linux三剑客之awk命令
参考URL: https://www.cnblogs.com/ginvip/p/6352157.html
3)伪目标image.daemon.verify 判断 docker 是否支持 docker 开启experimental功能
4)然后执行目标: image.build.%
在makefile替换DockerFile中 基础镜像为 centos8
然后执行 docker build --platform 命令
docker build --platform linux --pull -t $(REGISTRY_PREFIX)/$(IMAGE)-$(ARCH):$(VERSION) $(TMP_DIR)/$(IMAGE)
实际组装命令如下:
docker build --platform linux --pull -t mygoapp/file-boom-amd64:v1.23.4 /root/code/mygoapp/_output/tmp/file-boom
构建完成后删除docker镜像
其中go编译,用到以下3个点:
使用ldflags设置Go应用程序的版本信息
go ldflags设置
-w 去掉调试信息
-s 去掉符号表
-X 注入变量, 编译时赋值
可以在go install 、go build、go run 、go test中使用
常用变量
Module=github.com/pubgo/xxx
GOPATH=$(shell go env GOPATH)
Version=$(shell git tag --sort=committerdate | tail -n 1)
GoVersion=$(shell go version)
BuildTime=$(shell date "+%F %T")
CommitID=$(shell git rev-parse HEAD)
LDFLAGS:=-ldflags "-X 'github.com/pubgo/xxx/version.GoVersion=${GoVersion}' \
-X 'github.com/pubgo/xxx/version.BuildTime=${BuildTime}' \
-X 'github.com/pubgo/xxx/version.GoPath=${GOPATH}' \
-X 'github.com/pubgo/xxx/version.CommitID=${CommitID}' \
-X 'github.com/pubgo/xxx/version.Module=${Module}' \
-X 'github.com/pubgo/xxx/version.Version=${Version:-v0.0.1}'"
编译
go build ${LDFLAGS} -mod vendor -race -v -o main main.go
go build -ldflags "-w -s" -mod vendor -race -v -o main main.go
-w -s
如果使用这两个将会看不见文件名、行号, 对于调试不利
gdb看不到源码。
golang编译时选择json解析库 -tags=jsoniter
golang官方为我们提供了标准的json解析库–encoding/json,大部分情况下,使用它已经够用了。不过这个解析包有个很大的问题–性能。它不够快,如果我们开发高性能、高并发的网络服务就无法满足,这时就需要高性能的json解析库,目前性能比较高的有json-iterator和easyjson。
json-iterator
https://github.com/json-iterator/go
jsoniter 最大的优势就在于:能够 100% 兼容标准库,因此代码能够非常方便地进行迁移。
实现思路: 利用条件编译,实现灵活选择json解析库的目的。
编译构建的话,改为go build -tags=jsoniter .即可,即可生成使用json-iterator解析的可执行文件。
什么时候使用jsoniter,网友讨论:
没有瓶颈的前提下就用标准库。
直接用标准库的就可以,性能有问题的时候尝试切换。
一般是这么个操作。不过还是标准库第一位。
其实性能影响没那么大的
golang编译时的参数传递 -gcflags “all=-N -l”
go build 可以用-gcflags给go编译器传入参数,也就是传给go tool compile的参数,因此可以用go tool compile --help查看所有可用的参数。
-gcflags "all=-N -l"
-
-N 参数代表禁止优化,
-
-l 参数代表禁止内联,
go在编译目标程序的时候会嵌入运行时(runtime)的二进制,
禁止优化和内联可以让运行时(runtime)中的函数变得更容易调试。
三、参考
跟我一起写Makefile
参考URL: https://seisman.github.io/how-to-write-makefile/overview.html
14 | 项目管理:如何编写高质量的Makefile?
参考URL: https://time.geekbang.org/column/article/388920
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)