正文
Go有很多优点,比如:简单、原生支持并发等,而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗?本着“知其然,亦要知其所以然”的精神,本文我们就来探究一下Go良好可移植性背后的原理。
一、Go的可移植性
说到一门编程语言可移植性,我们一般从下面两个方面考量:
-
语言自身被移植到不同平台的容易程度;
-
通过这种语言编译出来的应用程序对平台的适应性。
在Go 1.7及以后版本中,我们可以通过下面命令查看Go支持OS和平台列表:
$ go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
从上述列表我们可以看出:从
linux/arm64
的嵌入式系统到
linux/s390x
的大型机系统,再到Windows、linux和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系,Go对各种平台和操作系统的支持不可谓不广泛。
Go官方似乎没有给出明确的porting guide,关于将Go语言porting到其他平台上的内容更多是在golang-dev这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看,Go的porting还是相对easy的。从个人对Go的了解来看,这一定程度上得益于Go独立实现了runtime。
runtime是支撑程序运行的基础。我们最熟悉的莫过于libc(C运行时),它是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布,它的功能大致有如下几个:
-
提供基础库函数调用,比如:strncpy;
-
封装syscall(注:syscall是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行),并提供同语言的库函数调用,比如:malloc、fread等;
-
提供程序启动入口函数,比如:linux下的__libc_start_main。
libc等c runtime lib是很早以前就已经实现的了,甚至有些老旧的libc还是单线程的。一些从事c/c 开发多年的程序员早年估计都有过这样的经历:那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外,c runtime的版本也参差不齐。这样的c runtime状况完全不能满足go语言自身的需求;另外Go的目标之一是原生支持并发,并使用goroutine模型,c runtime对此是无能为力的,因为c runtime本身是基于线程模型的。综合以上因素,Go自己实现了runtime,并封装了syscall,为不同平台上的go
user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。
独立实现的go runtime层将Go user-level code与OS syscall解耦,把Go porting到一个新平台时,将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些);同时,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。
以下测试试验环境为:darwin amd64 Go 1.8。
二、默认”静态链接”的Go程序
我们先来写两个程序:hello.c和hello.go,它们完成的功能都差不多,在stdout上输出一行文字:
//hello.c
#include
int main() {
printf("%s\n", "hello, portable c!");
return 0;
}
//hello.go
package main
import "fmt"
func main() {
fmt.Println("hello, portable go!")
}
我们采用“默认”方式分别编译以下两个程序:
$cc -o helloc hello.c
$go build -o hellogo hello.go
$ls -l
-rwxr-xr-x 1 tony staff 8496 6 27 14:18 helloc*
-rwxr-xr-x 1 tony staff 1628192 6 27 14:18 hellogo*
从编译后的两个文件helloc和hellogo的size上我们可以看到hellogo相比于helloc简直就是“巨人”般的存在,其size近helloc的200倍。略微学过一些Go的人都知道,这是因为hellogo中包含了必需的go runtime。我们通过otool工具(linux上可以用ldd)查看一下两个文件的对外部动态库的依赖情况:
$otool -L helloc
helloc:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
$otool -L hellogo
hellogo:
通过otool输出,我们可以看到hellogo并不依赖任何外部库,我们将hellog这个二进制文件copy到任何一个mac amd64的平台上,均可以运行起来。而helloc则依赖外部的动态库:/usr/lib/libSystem.B.dylib,而libSystem.B.dylib这个动态库还有其他依赖。我们通过nm工具可以查看到helloc具体是哪个函数符号需要由外部动态库提供:
$nm helloc
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
U _printf
U dyld_stub_binder
可以看到:_printf和dyld_stub_binder两个符号是未定义的(对应的前缀符号是U)。如果对hellog使用nm,你会看到大量符号输出,但没有未定义的符号。
$nm hellogo
00000000010bb278 s $f64.3eb0000000000000
00000000010bb280 s $f64.3fd0000000000000
00000000010bb288 s $f64.3fe0000000000000
00000000010bb290 s $f64.3fee666666666666
00000000010bb298 s $f64.3ff0000000000000
00000000010bb2a0 s $f64.4014000000000000
00000000010bb2a8 s $f64.4024000000000000
00000000010bb2b0 s $f64.403a000000000000
00000000010bb2b8 s $f64.4059000000000000
00000000010bb2c0 s $f64.43e0000000000000
00000000010bb2c8 s $f64.8000000000000000
00000000010bb2d0 s $f64.bfe62e42fefa39ef
000000000110af40 b __cgo_init
000000000110af48 b __cgo_notify_runtime_init_done
000000000110af50 b __cgo_thread_start
000000000104d1e0 t __rt0_amd64_darwin
000000000104a0f0 t _callRet
000000000104b580 t _gosave
000000000104d200 T _main
00000000010bbb20 s _masks
000000000104d370 t _nanotime
000000000104b7a0 t _setg_gcc
00000000010bbc20 s _shifts
0000000001051840 t errors.(*errorString).Error
00000000010517a0 t errors.New
.... ...
0000000001065160 t type..hash.time.Time
0000000001064f70 t type..hash.time.zone
00000000010650a0 t type..hash.time.zoneTrans
0000000001051860 t unicode/utf8.DecodeRuneInString
0000000001051a80 t unicode/utf8.EncodeRune
0000000001051bd0 t unicode/utf8.RuneCount
0000000001051d10 t unicode/utf8.RuneCountInString
0000000001107080 s unicode/utf8.acceptRanges
00000000011079e0 s unicode/utf8.first
$nm hellogo|grep " U "
Go将所有运行需要的函数代码都放到了hellogo中,这就是所谓的“静态链接”。是不是所有情况下,Go都不会依赖外部动态共享库呢?我们来看看下面这段代码:
//server.go
package main
import (
"log"
"net/http"
"os"
)
func main() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
srv := &http.Server{
Addr: ":8000", // Normally ":443"
Handler: http.FileServer(http.Dir(cwd)),
}
log.Fatal(srv.ListenAndServe())
}
我们利用Go标准库的net/http包写了一个fileserver,我们build一下该server,并查看它是否有外部依赖以及未定义的符号:
$go build server.go
-rwxr-xr-x 1 tony staff 5943828 6 27 14:47 server*
$otool -L server
server:
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
$nm server |grep " U "
U _CFArrayGetCount
U _CFArrayGetValueAtIndex
U _CFDataAppendBytes
U _CFDataCreateMutable
U _CFDataGetBytePtr
U _CFDataGetLength
U _CFDictionaryGetValueIfPresent
U _CFEqual
U _CFNumberGetValue
U _CFRelease
U _CFStringCreateWithCString
U _SecCertificateCopyNormalizedIssuerContent
U _SecCertificateCopyNormalizedSubjectContent
U _SecKeychainItemExport
U _SecTrustCopyAnchorCertificates
U _SecTrustSettingsCopyCertificates
U _SecTrustSettingsCopyTrustSettings
U ___error
U ___stack_chk_fail
U ___stack_chk_guard
U ___stderrp
U _abort
U _fprintf
U _fputc
U _free
U _freeaddrinfo
U _fwrite
U _gai_strerror
U _getaddrinfo
U _getnameinfo
U _kCFAllocatorDefault
U _malloc
U _memcmp
U _nanosleep
U _pthread_attr_destroy
U _pthread_attr_getstacksize
U _pthread_attr_init
U _pthread_cond_broadcast
U _pthread_cond_wait
U _pthread_create
U _pthread_key_create
U _pthread_key_delete
U _pthread_mutex_lock
U _pthread_mutex_unlock
U _pthread_setspecific
U _pthread_sigmask
U _setenv
U _strerror
U _sysctlbyname
U _unsetenv