Golang使用go build 进行条件编译

当我们编写的go代码依赖特定平台或者cpu架构的时候,我们需要给出不同的实现

C语言有预处理器,可以通过宏或者#define包含特定平台指定的代码进行编译

但是Go没有预处理器,他是通过 go/build包 里定义的tags和命名约定来让Go的包可以管理不同平台的代码

这篇文章将讲述Go的条件编译系统是如何实现的,并且通过实例来说明如何使用

1. 预备知识:go list命令的使用

在讲条件编译之前需要了解go list的简单用法

go list访问源文件里那些能够影响编译进程内部的数据结构

go list与go build ,test,install大部分的参数相同,但是go list不会执行编译操作。使用-f参数可以让我们提供的text/template里的代码在包含go/build.Package上下文的环境里正确执行(就是让go/build.Package里的上下文去格式化 text/template里这种格式 '{{.GoFiles}}'里的占位符,写过http server程序的同学看到应该很熟悉)

使用格式化参数,我们能通过go list获取将会被编译的文件名

% go list -f '{{.GoFiles}}' os/exec
[exec.go lp_unix.go]

上面这个例子里我们用go list来查看在linux/arm平台下 os/exec包里有哪些文件将会被编译。

结果显示:exec.go包含了通用的代码在所有的平台下可用,lp_unix.go包含了*nix系统里的exec.LookPath

在windows系统下运行同样的命令,结果如下:

C:\go> go list -f '{{.GoFiles}}' os/exec
[exec.go lp_windows.go]

上面这个例子是Go 条件编译系统的两个部分,称之为:编译约束,下面将详细描述

2. 第一种条件编译的方法:编译标签

在源代码里添加标注,通常称之为编译标签( build tag)

编译标签是在尽量靠近源代码文件顶部的地方用注释的方式添加

go build在构建一个包的时候会读取这个包里的每个源文件并且分析编译便签,这些标签决定了这个源文件是否参与本次编译

编译标签添加的规则(附上原文):

  1. a build tag is evaluated as the OR of space-separated options

  2. each option evaluates as the AND of its comma-separated terms

  3. each term is an alphanumeric word or, preceded by !, its negation

  4. 编译标签由空格分隔的编译选项(options)以"或"的逻辑关系组成

  5. 每个编译选项由逗号分隔的条件项以逻辑"与"的关系组成

  6. 每个条件项的名字用字母+数字表示,在前面加!表示否定的意思

例子(编译标签要放在源文件顶部)

// +build darwin freebsd netbsd openbsd

这个将会让这个源文件只能在支持kqueue的BSD系统里编译

一个源文件里可以有多个编译标签,多个编译标签之间是逻辑"与"的关系

// +build linux darwin
// +build 386

这个将限制此源文件只能在 linux/386或者darwin/386平台下编译

关于注释的说明

刚开始使用编译标签经常会犯下面这个错误

// +build !linux
package mypkg // wrong

这个例子里的编译标签和包的声明之间没有用空行隔开,这样编译标签会被当做包声明的注释而不是编译标签从而被忽略掉

下面这个是正确的标签的书写方式,标签的结尾添加一个空行这样标签就不会当做其他声明的注释

// +build !linux

package mypkg // correct

用go vet命令也可以检测到这个缺少空行的错误,初期可以用这个命令来避免缺少空行的错误

% go vet mypkg
mypkg.go:1: +build comment appears too late in file
exit status 1

作为参考,下面的例子将licence声明,编译标签和包声明放在一起,请大家注意分辨

% head headspin.go 
// Copyright 2013 Way out enterprises. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build someos someotheros thirdos,!amd64

// Package headspin implements calculates numbers so large
// they will make your head spin.
package headspin

3. 第二种条件编译方法:文件后缀

这个方法通过改变文件名的后缀来提供条件编译,这种方案比编译标签要简单,go/build可以在不读取源文件的情况下就可以决定哪些文件不需要参与编译

文件命名约定可以在go/build 包里找到详细的说明,简单来说如果你的源文件包含后缀:$GOOS.go,那么这个源文件只会在这个平台下编译,$GOARCH.go也是如此。这两个后缀可以结合在一起使用,但是要注意顺序:$GOOS$GOARCH.go, 不能反过来用:$GOARCH$GOOS.go

例子如下:

mypkg_freebsd_arm.go // only builds on freebsd/arm systems
mypkg_plan9.go       // only builds on plan9

源文件不能只提供条件编译后缀,还必须有文件名:

_linux.go
_freebsd_386.go

这两个源文件在所有平台下都会被忽略掉,因为go/build将会忽略所有以下划线或者点开头的源文件

4. 编译标签和文件后缀的选择

编译标签和文件后缀的功能上有重叠,例如一个文件名:mypkg_linux.go包含了// +build linux将会出现冗余

  • linux 平台

通常情况下,如果源文件与平台或者cpu架构完全匹配,那么用文件后缀,例如:

mypkg_linux.go         // only builds on linux systems
mypkg_windows_amd64.go // only builds on windows 64bit platforms

相反,如果这个源文件可以在超过一个平台或者超过一个cpu架构下可以使用或者需要去除指定平台,那么使用编译标签,例如下面的编译标签可以在所有*nix平台上编译:

grep '+build' $(go env GOROOT)/src/os/exec/lp_unix.go
// +build darwin dragonfly freebsd linux netbsd openbsd

下面是可以在除了windows的所有平台下编译

% grep '+build' $HOME/go/src/pkg/os/types_notwin.go
grep '+build' $(go env GOROOT)/src/os/exec/types_notwin.go
// +build !windows

如果是其他的条件编译需求,可以.go文件中定义如下,这样两个go文件中可以存在同名含函数;

//go:build && vppapiclient
或者是
//go:build !vppapiclient
  • windows 平台
echo "$(go env GOROOT)\src\os\exec\lp_windows.go"

5. go build命令

构建编译由导入路径命名的包,以及它们的依赖关系,但它不会安装结果.

5.1. 使用

go build [-o 输出名] [-i] [编译标记] [包名]

如果参数为***.go文件或文件列表,则编译为一个个单独的包。
当编译单个main包(文件),则生成可执行文件。
当编译单个或多个包非主包时,只构建编译包,但丢弃生成的对象(.a),仅用作检查包可以构建。
当编译包时,会自动忽略'_test.go'的测试文件。

5.2. 参数

-o
    output 指定编译输出的名称,代替默认的包名。
-i
    install 安装作为目标的依赖关系的包(用于增量编译提速)。
    以下 build 参数可用在 build, clean, get, install, list, run, test
-a
    完全编译,不理会-i产生的.a文件(文件会比不带-a的编译出来要大?)
-n
    仅打印输出build需要的命令,不执行build动作(少用)。
-p n
    开多少核cpu来并行编译,默认为本机CPU核数(少用)。
-race
    同时检测数据竞争状态,只支持 linux/amd64, freebsd/amd64, darwin/amd64 和 windows/amd64.
-msan
    启用与内存消毒器的互操作。仅支持linux / amd64,并且只用Clang / LLVM作为主机C编译器(少用)。
-v
    打印出被编译的包名(少用).
-work
    打印临时工作目录的名称,并在退出时不删除它(少用)。
-x
    同时打印输出执行的命令名(-n)(少用).
-asmflags 'flag list'
    传递每个go工具asm调用的参数(少用)
-buildmode mode
    编译模式(少用)
    'go help buildmode'
-compiler name
    使用的编译器 == runtime.Compiler
    (gccgo or gc)(少用).
-gccgoflags 'arg list'
    gccgo 编译/链接器参数(少用)
-gcflags 'arg list'
    垃圾回收参数(少用).
-installsuffix suffix
    ??????不明白
    a suffix to use in the name of the package installation directory,
    in order to keep output separate from default builds.
    If using the -race flag, the install suffix is automatically set to race
    or, if set explicitly, has _race appended to it.  Likewise for the -msan
    flag.  Using a -buildmode option that requires non-default compile flags
    has a similar effect.
-ldflags 'flag list'
    '-s -w': 压缩编译后的体积
    -s: 去掉符号表
    -w: 去掉调试信息,不能gdb调试了
-linkshared
    链接到以前使用创建的共享库
    -buildmode=shared.
-pkgdir dir
    从指定位置,而不是通常的位置安装和加载所有软件包。例如,当使用非标准配置构建时,使用-pkgdir将生成的包保留在单独的位置。
-tags 'tag list'
    构建出带tag的版本.
-toolexec 'cmd args'
    ??????不明白
    a program to use to invoke toolchain programs like vet and asm.
    For example, instead of running asm, the go command will run
    'cmd args /path/to/asm <arguments for asm>'.

以上命令,单引号/双引号均可。

  • 对包的操作'go help packages'
  • 对路径的描述'go help gopath'
  • 对 C/C++ 的互操作'go help c'

注意
构建遵守某些约定('go help gopath'),但不是所有的项目都遵循这些约定,当使用自己的惯例或使用单独的软件构建系统时可以选择使用较低级别的调用go tool compile和go tool link来避免一些构建工具的开销和设计决策

5.3. 设置包内常量

如果你要定义package foo中的常量Bar,在编译的时候,可以在go build或go install是生命一个特殊的flag:

$ go install -ldflags='-X foo.Bar="my super cool string"'

6. 总结

这篇文章主要关注所有可以被go tool编译的go源文件,编译标签和文件后缀名(也包括了.c 和.s文件)

Go的标准库里包含了很多的样例,特别是runtime,syscall,os和net包,读者可以通过这些包来学习

Test文件也支持编译标签和文件后缀条件编译,并且作用方式与go源文件相同。可以在不同平台下有条件的包含一些测试样例。同样,标准库也包含了大量的例子

最后,这篇文件是讲如何用go tool来达到条件编译,但是条件编译不限于go tool,你可以用go/build包编写自己的条件编译工具

发表评论

邮箱地址不会被公开。 必填项已用*标注