Golang的构建模式(buildmode)指的是编译器如何编译源码构建出相关的对象文件,最常见的情况下就是生成一个可执行的二进制文件。然而,其实golang的buildmode还有很多有趣的用法……

在 go build 和 go install 命令中,我们可以指定 -buildmode 参数来让编译器构建出特定的对象文件。通过命令 go help buildmode,可以看到其支持的选项:

-buildmode=archive
-buildmode=c-archive
-buildmode=c-shared
-buildmode=default
-buildmode=shared
-buildmode=exe
-buildmode=pie
-buildmode=plugin

排除掉最常用的 default,接下来我们就挑选几个比较有意思的来详细讲解一下。

说明:

Build the listed main package, plus all packages it imports, into a C archive file. The only callable symbols will be those functions exported using a cgo //export comment. Requires exactly one main package to be listed.

c-archive 也就是将 package main 中导出的方法(// export 标记)编译成 .a 文件,这样其它 c 程序就可以静态链接该文件,并调用其中的方法。

Go示例:

首先写一个简单 add.go :

package main

import "fmt"
import "C"

func main(){}

//export Add
func Add(a, b int) int{
	fmt.Printf("%d + %d = %d\n", a, b, a+ b)
	return a+b
}

然后使用 -buildmode=c-archive 编译:

$ go build -buildmode=c-archive add.go

生成两个文件: add.a 和 add.h,查看一下文件的类型:

$ file add.a add.h
// output
add.a: current ar archive random library
add.h: c program text, ASCII text

 在 add.h 中我们看到 Add() 函数的定义:

#ifdef __cplusplus
extern "C" {
#endif

extern GoInt Add(GoInt p0, GoInt p1);

#ifdef __cplusplus
}
#endif

C示例:

接下来,我们再写一个简单的 C 程序:

# include "add.h"

int main(void) {
    Add(1, 2);
    return 0;
}

加上 add.a 文件编译一下:

$ cc myadd.c add.a

生成一个可执行文件 a.out,执行 ./a.out:

$ ./a.out
// output
1 + 2 = 3

说明:

Build the listed main package, plus all packages it imports, into a C shared library. The only callable symbols will
be those functions exported using a cgo //export comment. Requires exactly one main package to be listed.

c-shared 也就是将 package main 中导出的方法(// export 标记)编译成一个动态链接库(.so 或 .dll 文件),这样其它 c 程序就可以调用其中的方法。

Go示例:

继续使用上面的 add.go,改用 -buildmode=c-shared 来编译:

$ go build -buildmode=c-shared -o add.so add.go

这次生成了 add.so 和 add.h 两个文件:

$ file add.so add.h
// output
add.so: Mach-O 64-bit dynamically linked shared library x86_64
add.h:  c program text, ASCII text

C示例:

继续使用上面的 myadd.c进行编译:

$ cc myadd.c add.so

同样是生成了 a.out 可执行文件,但是留意一下前后两次的体积:

  • 使用 add.so 编译生成的 a.out 才 8.2KB;
  • 使用 add.a 编译生成的 a.out 有 1.8MB;

如果我们将 add.so 删除,再运行 a.out:

dyld: Library not loaded: add.so
  Referenced from: /Users/jachua/go/src/buildmode/c-archive/./a.out
  Reason: image not found
[1]    94945 abort      ./a.out

另外,我们也可以指定环境变量 LD_LIBRARY_PATH 告诉操作系统到哪里去寻找动态链接库:

// Linux
$ LD_LIBRARY_PATH=./lib ./out
// macOS
$ DYLD_LIBRARY_PATH=./lib ./out 

版本要求:

G0 1.5

-dynlink

G0 1.6

Go 1.10

c-sharedlinux/ppc64lewindows/386windows/amd64piedarwin/amd64pluginlinux/ppc64ledarwin/amd64

Go 1.11

c-sharedc-archivefreebsd/amd64

说明:

Combine all the listed non-main packages into a single shared library that will be used when building with the -linkshared option. Packages named main are ignored.

shared 与 c-shared 类似,不过它是用来给 golang 构建动态链接库的。它将 非main 的package 编译为动态链接库,并在构建其他 go程序时使用 -linkshared 参数指定。

示例:

首先我们写一个非常简单的 hello.go:

package main

import "fmt"

func main(){
	fmt.Println("hello world")
}

然后,我们需要将 golang 的所有标准库 std 编译安装为 shared:

// -buildmode=shared 暂不支持 macOS
$ go install -buildmode=shared std

接着再用 -linkshared 编译 hello.go:

$ go build -linkshared hello.go

可以看到生成的可执行文件体积才 20KB ,相比正常的 go build hello.go 生成的 1.9MB 小非常多。我们可以使用 ldd 命令来查看调用链:

$ ldd hello
 	linux-vdso.so.1 (0x00007ffcb0db9000)
	libstd.so => /usr/local/go/pkg/linux_amd64_dynlink/libstd.so (0x00007f2d5c1cb000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d5bdda000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2d5bbd6000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2d5b9b7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f2d5eb96000)

当然如果缺少了其中某个链接库或者版本不匹配,都将导致无法正常运行,所以一般情况下这种构建模式很少使用。

说明:

Build the listed main packages, plus all packages that they import, into a Go plugin. Packages not named main are ignored.

plugin 模式是 golang 1.8 才推出的一个特殊的构建方式,它将 package main 编译为一个 go 插件,并可在运行时动态加载。

示例:

首先,我们来写一个简单的 greeting:

package main

import "fmt"

type greeting string

func (g greeting) Greet() {
	fmt.Println("hello world")
}

var Greeter greeting

然后将其编译为一个 go 插件:

$ go build -buildmode=plugin -o greeter.so greeter.go
$ file greeter.so
greeter.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=f5729749535c706a6095994f9510da92ccc8f9c6, not stripped

再使用 golang 官方的 plugin 库来调用这个插件:

package main

import (
	"fmt"
	"os"
	"plugin"
)

type Greeter interface {
	Greet()
}

func main() {
	plug, err := plugin.Open("./greet/greeter.so")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	symGreeter, err := plug.Lookup("Greeter")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	var greeter Greeter
	greeter, ok := symGreeter.(Greeter)
	if !ok {
		fmt.Println(err)
		os.Exit(1)
	}
	greeter.Greet()
}

版本说明:

Go 1.8

pluginplugin

G0 1.10

c-sharedlinux/ppc64lewindows/386windows/amd64piedarwin/amd64pluginlinux/ppc64ledarwin/amd64

 

参考: