定制Go Package的Go Get导入路径

近期Go team的组员Jaana B. Dogan,网名:rakyll开源了一个小工具:Go Vanity URLs。这个小工具可以帮助你快速为你的Go package定制Go get的导入路径(同样也是package被使用时的import路径)。

说到go package的go get导入路径,我们最常见和常使用的domain name就是github.com了,比如:beego包的go get导入路径就是 go get github.com/astaxie/beego。我们还经常看到一些包,它们的导入路径很特殊,比如:go get golang.org/x/net、go get gopkg.in/yaml.v2等(虽然net、yaml这些包实际的repo也是存在于github.com上的),这些就是定制化的package import path,它们有诸多好处:

  • 可以为package设置canonical import path ,即权威导入路径

    这是在Go 1.4版本中加入的概念。Go package多托管在几个知名的代码管理网站,比如:github.com、bitbucket.org等,这样默认情况下package的import path就是github.com/xxx/package、bitbucket.org/xxx/package等。一旦某个网站关门大吉了,那package代码势必要迁移到其他站点,这样package的import path就要发生改变,这会给package的用户造成诸多不便,比如之前的code.google.com关闭就给广大的gopher带来了很大的“伤害”。canonical import path就可以解决这个问题。package的用户只需要使用package的canonical import path,这样无论package的实际托管网站在哪,对package的用户都不会带来影响。

  • 便于组织和个人对package的管理

    组织和个人可以将其分散托管在不同代码管理网站的package统一聚合到组织的官网名下或个人的域名下,比如:golang.org/x/net、gopkg.in/xxx等。

  • package的import路径可以更短、更简洁

    有些时候,github.com上的go package的import path很长、很深,并不便于查找和书写,通过定制化import path,我们可以使用更短、更简洁的域名来代替github.com仓库下的多级路径。

不过rakyll提供的govanityurls仅能运行于Google的app engine上,这对于国内的Gopher们来说是十分不便的,甚至是不可用的,于是这里fork了rakyll的repo,并做了些许修改,让govanityurls可以运行于普通的vps主机上。

一、govanityurls原理

govanityurls的原理十分简单,它本身就好比一个“导航”服务器。当go get将请求发送给govanityurls时,govanityurls将请求中的repo的真实地址返回给go get,后续go get再从真实的repo地址获取package数据。

img{512x368}

可以看出go get第一步是尝试获取自定义路径的包的真实地址,govanityurls将返回一个类似如下内容的http应答(针对go get tonybai.com/gowechat请求):

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="tonybai.com/gowechat git https://github.com/bigwhite/gowechat">
<meta name="go-source" content="tonybai.com/gowechat ">
<meta http-equiv="refresh" content="0; url=https://godoc.org/tonybai.com/gowechat">
</head>
<body>
Nothing to see here; <a href="https://godoc.org/tonybai.com/gowechat">see the package on godoc</a>.
</body>
</html>

二、使用govanityurls

关于govanityurls的使用,可以参考其README.md,这里以一个demo来作为govanityurls的使用说明。

1、安装govanityurls

安装方法:

$go get github.com/bigwhite/govanityurls

$govanityurls
govanityurls is a service that allows you to set custom import paths for your go packages

Usage:
     govanityurls -host [HOST_NAME]

  -host string
        custom domain name, e.g. tonybai.com

和rakyll提供的govanityurls不同的是,这里的govanityurls需要外部传入一个host参数(比如:tonybai.com),而在原版中这个host是由google app engine的API提供的。

2、配置vanity.yaml

vanity.yaml中配置了host下的自定义包的路径以及其真实的repo地址:

/gowechat:
        repo: https://github.com/bigwhite/gowechat

上面这个配置中,我们实际上为gowechat这个package定义了tonybai.com/gowechat这个go get路径,其真实的repo存放在github.com/bigwhite/gowechat。当然这个vanity.yaml可以配置N个自定义包路径,也可以定义多级路径,比如:

/gowechat:
        repo: https://github.com/bigwhite/gowechat

/x/experiments:
        repo: https://github.com/bigwhite/experiments

3、配置反向代理

govanityurls默认监听的是8080端口,这主要是考虑到我们通常会使用主域名定制路径,而在主域名下面一般情况下都会有其他一些服务,比如:主页、博客等。通常我们都会用一个反向代理软件做路由分发。我们针对gowechat这个repo定义了一条nginx location规则:

// /etc/nginx/conf.d/default.conf
server {
        listen 80;
        listen 443 ssl;
        server_name tonybai.com;

        ssl_certificate           /etc/nginx/cert.crt;
        ssl_certificate_key       /etc/nginx/cert.key;
        ssl on;

        location /gowechat {
                proxy_pass http://10.11.36.23:8080;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }
}

这里为了方便,我既在80端口提供http服务,也在443端口提供了https服务。这里的10.11.36.23就是我真正部署govanityurls的host(一台thinkcenter PC)。/etc/nginx/cert.key和/etc/nginx/cert.crt可以通过下面命令生成:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt

CN填tonybai.com

注意:修改两个文件的owner权限,将其owner改为nginx worker process的user,我这里是www-data(chown www-data:www-data /etc/nginx/cert.*)。

4、测试govanityurls

我在我的mac上修改了一下/etc/hosts,添加一条路由:

10.11.36.23 tonybai.com

我们来go get tonybai.com/gowechat:

$go get -v -insecure tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
https fetch failed: Get https://tonybai.com/gowechat?go-get=1: EOF
Fetching http://tonybai.com/gowechat?go-get=1
Parsing meta tags from http://tonybai.com/gowechat?go-get=1 (status code 200)
get "tonybai.com/gowechat": found meta tag main.metaImport{Prefix:"tonybai.com/gowechat", VCS:"git", RepoRoot:"https://github.com/bigwhite/gowechat"} at http://tonybai.com/gowechat?go-get=1
tonybai.com/gowechat (download)
package tonybai.com/gowechat: no buildable Go source files in /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat

$ls /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat
LICENSE        README.md    mp/        pb/        qy/

我们可以看到tonybai.com/gowechat被成功get到本地,并且import path为tonybai.com/gowechat,其他包可以按照这个定制的gowechat的导入路径import gowechat package了。

上面例子中,我们给go get传入了一个-insecure的参数,这样go get就会通过http协议去访问tonybai.com/gowechat了。我们试试去掉-insecure,不过再次执行前需先将本地的tonybai.com/gowechat包删除掉。

$go get -v tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
https fetch failed: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority
package tonybai.com/gowechat: unrecognized import path "tonybai.com/gowechat" (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority)

虽然我已经关掉了git的http.sslVerify,但go get的执行过程还是检查了server端证书是未知CA签署的并报错,原来这块的verify是go get自己做的。关于httpskey和证书(.crt)的相关知识,我在《Go和HTTPS》一文中已经做过说明,不是很熟悉的童鞋可以移步那篇文章。

我们来创建CA、创建server端的key(cert.key),并用创建的CA来签署server.crt:

$ openssl genrsa -out rootCA.key 2048
$ openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=*.tonybai.com" -days 5000 -out rootCA.pem
$ openssl genrsa -out cert.key 2048
$ openssl req -new -key cert.key -subj "/CN=tonybai.com" -out cert.csr
$ openssl x509 -req -in cert.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out cert.crt -days 5000

# ls
cert.crt  cert.csr  cert.key  rootCA.key  rootCA.pem  rootCA.srl

我们将cert.crt和cert.key拷贝到ubuntu的/etc/nginx目录下,重启nginx,让其加载新的cert.crt和cert.key。然后将rootCA.pem拷贝到/etc/ssl/cert目录下,这个目录是ubuntu下存放CA公钥证书的标准路径。在测试go get前,我们先用curl测试一下:

# curl https://tonybai.com/gowechat
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="tonybai.com/gowechat git https://github.com/bigwhite/gowechat">
<meta name="go-source" content="tonybai.com/gowechat ">
<meta http-equiv="refresh" content="0; url=https://godoc.org/tonybai.com/gowechat">
</head>
<body>
Nothing to see here; <a href="https://godoc.org/tonybai.com/gowechat">see the package on godoc</a>.
</body>
</html>

curl测试通过!

我们再来看看go get:

# go get tonybai.com/gowechat
package tonybai.com/gowechat: unrecognized import path "tonybai.com/gowechat" (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority)

问题依旧!难道go get无法从/etc/ssl/cert中选取适当的ca证书来做server端的cert.crt的验证么?就着这个问题我在go官方发现了一个类似的issue: #18519 。从中得知,go get仅仅会在不同平台下参考以下几个certificate files:

$GOROOT/src/crypto/x509/root_linux.go

package x509

// Possible certificate files; stop after finding one.
var certFiles = []string{
    "/etc/ssl/certs/ca-certificates.crt",                // Debian/Ubuntu/Gentoo etc.
    "/etc/pki/tls/certs/ca-bundle.crt",                  // Fedora/RHEL 6
    "/etc/ssl/ca-bundle.pem",                            // OpenSUSE
    "/etc/pki/tls/cacert.pem",                           // OpenELEC
    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
}

在ubuntu上,/etc/ssl/certs/ca-certificates.crt是其参考的数字证书。因此要想go get成功,我们需要将我们rootCA.pem加入到/etc/ssl/certs/ca-certificates.crt中去,最简单的方法就是:

$ cat rootCA.pem >> /etc/ssl/certs/ca-certificates.crt

当然,ubuntu也提供了管理根证书的命令update-ca-certificates,可以看其manual学学如何更新/etc/ssl/certs/ca-certificates.crt,这里就不赘述了。

更新后,我们再来go get:

# go get -v tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
Parsing meta tags from https://tonybai.com/gowechat?go-get=1 (status code 200)
get "tonybai.com/gowechat": found meta tag main.metaImport{Prefix:"tonybai.com/gowechat", VCS:"git", RepoRoot:"https://github.com/bigwhite/gowechat"} at https://tonybai.com/gowechat?go-get=1
tonybai.com/gowechat (download)
package tonybai.com/gowechat: no buildable Go source files in /root/go/src/tonybai.com/gowechat

go get成功!

三、小结

  • 使用govanityurls可以十分方便的为你的go package定制go get的导入路径;
  • 一般使用nginx等反向代理放置在govanityurls前端,便于同域名下其他服务的开展;
  • go get默认采用https访问,自签署的ca和server端的证书问题要处理好。如果有条件的话,还是用用letsencrypt等提供的免费证书吧。

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

也谈Go的可移植性

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。

img{512x368}

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 <stdio.h>

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

通过otool和nm的输出结果我们惊讶的看到:默认采用“静态链接”的Go程序怎么也要依赖外部的动态链接库,并且也包含了许多“未定义”的符号了呢?问题在于cgo。

三、cgo对可移植性的影响

默认情况下,Go的runtime环境变量CGO_ENABLED=1,即默认开始cgo,允许你在Go代码中调用C代码,Go的pre-compiled标准库的.a文件也是在这种情况下编译出来的。在$GOROOT/pkg/darwin_amd64中,我们遍历所有预编译好的标准库.a文件,并用nm输出每个.a的未定义符号,我们看到下面一些包是对外部有依赖的(动态链接):

=> crypto/x509.a
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 ... ...
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 ... ...
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __cgo_topofstack
                 U _kCFAllocatorDefault
                 U _memcmp
                 U _sysctlbyname

=> net.a
                 U ___error
                 U __cgo_topofstack
                 U _free
                 U _freeaddrinfo
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _malloc

=> os/user.a
                 U __cgo_topofstack
                 U _free
                 U _getgrgid_r
                 U _getgrnam_r
                 U _getgrouplist
                 U _getpwnam_r
                 U _getpwuid_r
                 U _malloc
                 U _realloc
                 U _sysconf

=> plugin.a
                 U __cgo_topofstack
                 U _dlerror
                 U _dlopen
                 U _dlsym
                 U _free
                 U _malloc
                 U _realpath$DARWIN_EXTSN

=> runtime/cgo.a
                 ... ...
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _fwrite
                 U _malloc
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 ... ...
                 U _setenv
                 U _strerror
                 U _unsetenv

=> runtime/race.a
                 U _OSSpinLockLock
                 U _OSSpinLockUnlock
                 U __NSGetArgv
                 U __NSGetEnviron
                 U __NSGetExecutablePath
                 U ___error
                 U ___fork
                 U ___mmap
                 U ___munmap
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __dyld_get_image_header
                .... ...

我们以os/user为例,在CGO_ENABLED=1,即cgo开启的情况下,os/user包中的lookupUserxxx系列函数采用了c版本的实现,我们看到在$GOROOT/src/os/user/lookup_unix.go中的build tag中包含了+build cgo。这样一来,在CGO_ENABLED=1,该文件将被编译,该文件中的c版本实现的lookupUser将被使用:

// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// +build cgo

package user
... ...
func lookupUser(username string) (*User, error) {
    var pwd C.struct_passwd
    var result *C.struct_passwd
    nameC := C.CString(username)
    defer C.free(unsafe.Pointer(nameC))
    ... ...
}

这样来看,凡是依赖上述包的Go代码最终编译的可执行文件都是要有外部依赖的。不过我们依然可以通过disable CGO_ENABLED来编译出纯静态的Go程序:

$CGO_ENABLED=0 go build -o server_cgo_disabled server.go

$otool -L server_cgo_disabled
server_cgo_disabled:
$nm server_cgo_disabled |grep " U "

如果你使用build的 “-x -v”选项,你将看到go compiler会重新编译依赖的包的静态版本,包括net、mime/multipart、crypto/tls等,并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下,然后再静态连接这些版本。

四、internal linking和external linking

问题来了:在CGO_ENABLED=1这个默认值的情况下,是否可以实现纯静态连接呢?答案是可以。在$GOROOT/cmd/cgo/doc.go中,文档介绍了cmd/link的两种工作模式:internal linking和external linking。

1、internal linking

internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时,cmd/link默认使用internal linking,而无需启动外部external linker(如:gcc、clang等),不过由于cmd/link功能有限,仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的,那么编译出来的最终二进制文件依旧是动态链接的,即便在go build时传入-ldflags ‘extldflags “-static”‘亦无用,因为根本没有使用external linker:

$go build -o server-fake-static-link  -ldflags '-extldflags "-static"' server.go
$otool -L server-fake-static-link
server-fake-static-link:
    /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)

2、external linking

而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中,再将其交给外部的链接器,比如gcc或clang去做最终链接处理。如果此时,我们在cmd/link的参数中传入-ldflags ‘extldflags “-static”‘,那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker,还是以server.go的编译为例:

$go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# command-line-arguments
/Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)

可以看到,cmd/link调用的clang尝试去静态连接libc的.a文件,但由于我的mac上仅仅有libc的dylib,而没有.a,因此静态连接失败。我找到一个ubuntu 16.04环境:重新执行上述构建命令:

# go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# ldd server-static-link
    not a dynamic executable
# nm server-static-link|grep " U "

该环境下libc.a和libpthread.a分别在下面两个位置:

/usr/lib/x86_64-linux-gnu/libc.a
/usr/lib/x86_64-linux-gnu/libpthread.a

就这样,我们在CGO_ENABLED=1的情况下,也编译构建出了一个纯静态链接的Go程序。

如果你的代码中使用了C代码,并依赖cgo在go中调用这些c代码,那么cmd/link将会自动选择external linking的机制:

//testcgo.go
package main

//#include <stdio.h>
// void foo(char *s) {
//    printf("%s\n", s);
// }
// void bar(void *p) {
//    int *q = (int*)p;
//    printf("%d\n", *q);
// }
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    var s = "hello"
    C.foo(C.CString(s))

    var i int = 5
    C.bar(unsafe.Pointer(&i))

    var i32 int32 = 7
    var p *uint32 = (*uint32)(unsafe.Pointer(&i32))
    fmt.Println(*p)
}

编译testcgo.go:

# go build -o testcgo-static-link  -ldflags '-extldflags "-static"' testcgo.go
# ldd testcgo-static-link
    not a dynamic executable

vs.
# go build -o testcgo testcgo.go
# ldd ./testcgo
    linux-vdso.so.1 =>  (0x00007ffe7fb8d000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000)
    /lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000)

五、小结

本文探讨了Go的可移植性以及哪些因素对Go编译出的程序的移植性有影响:

  • 你的程序用了哪些标准库包?如果仅仅是非net、os/user等的普通包,那么你的程序默认将是纯静态的,不依赖任何c lib等外部动态链接库;
  • 如果使用了net这样的包含cgo代码的标准库包,那么CGO_ENABLED的值将影响你的程序编译后的属性:是静态的还是动态链接的;
  • CGO_ENABLED=0的情况下,Go采用纯静态编译;
  • 如果CGO_ENABLED=1,但依然要强制静态编译,需传递-linkmode=external给cmd/link。

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

外星人为什么还没降落到地球上?

这周五午间休息时无意中看了36kr发布的一篇文章:《开电脑与外星人尬聊?搜寻地外文明项目SETI@home的极客简史》,这是一篇译文,原文发表在《大西洋月刊》,题为“A Brief History of SETI@Home”,文章科普了SETI@HOME这一项目的简史。

SETI是“Search for Extraterrestrial Intelligence”,字面意思就是搜寻外星的智慧生命体。这个项目从技术本质上看就是一个超级庞大的分布式系统,它利用加入“志愿计算”计划的志愿者的PC上的空闲计算能力来分析射电望远镜观测到的信号,旨在从这些信号中过滤并识别出可能的外星生命发出的信号。SETI@home项目于1999年就启动了,不过令人感到尴尬的是至今这个项目仍然是一无所获:外星人的毛儿都没找到。虽然结果不如预期,但这不能否定这一项目的伟大。国内的地外文明爱好者、ET/alien爱好者们,如果你想贡献出自己的一份薄力,就去SETI@Home项目官网下载一个软件,加入到这个庞大的计算网络中吧。

不过这里并不想和大家探讨SETI@Home的技术细节,而是要考量另外一个问题:外星人为什么还没降落到地球上?

1、人类探索宇宙的历程简述

在探索地外生命的道路上,人类不可谓不努力,探索的时间也不可谓不长。从已知的、公认的人类探索宇宙的大事件资料中,我们知道:

  • 公元前500-400年,中国人就开始制作木鸟并试验原始飞行器;
  • 起源于古代中国的风筝于约公园14世纪传到欧洲;
  • 1903年12月14日至17日,由莱特兄弟设计制造的“飞行者”1号飞机,在人类航空史上首次实现了自主操纵飞行。从此人类进入航空新纪元;
  • 1909年世界第一架轻型飞机在法国诞生;
  • 1947年10月14日美国著名试飞员查尔斯·耶格尔驾驶X-1飞机实现了突破音障飞行;
  • 1957年10月4日,前苏联发射世界第一颗人造地球卫星。半年后,美国的人造卫星上天;从此人类进入近地轨道探索的时代;
  • 1969年7月20日22时56分20秒,美国宇航员阿姆斯特朗乘坐“阿波罗”11号飞船成功登陆月球,成为人类踏上月球的第一人,也是人类踏上地外星球的第一人,从此人类开启了太阳系内探索新时代;
  • 1970年12月15日,前苏联“金星”7号探测器首次在金星上着陆;
  • 1971年12月2日,前苏联“火星”3号探测器在火星表面着陆。5年后,美国的“海盗”火星探测器登陆火星;
    … …

人类在探索近地轨道和太阳系内行星的同时,不忘太阳系外的深层空间的探索。不过限于人类在技术方面的局限,人类文明仅仅能将少量信息带出太阳系。这其中尤为值得注目的是美国的先驱者系列和旅行者系列深空探测器的发射,它们分别携带了人类送给地外文明的“礼物”:

img{512x368}

两艘“先驱者号”各携带一张相同的镀金铝板(见上图),长22.9厘米,宽15.2厘米,其上刻有一男一女的画像,那位男人正在招手致意。还有象征太阳系的信息,以及一些表示这艘星际飞船来历的符号。

img{512x368}

两艘“旅行者号”各携带一块相同的镀金唱片(见上图)和一枚金刚石唱针。唱片名为《地球之音》,直径有22.9厘米,上面录制了人类向外星文明发出的55种问候语(包括中国的现代标准汉语、闽南方言、粤语和
吴语)、长达90分钟的27首各国著名乐曲(包括中国传统名曲《高山流水》等)录音,还有115幅地球上各种事物和情景的图片。这两张唱片可以在宇宙中保存10亿年之久。

目前四个飞行器均已飞出了太阳系,成为茫茫宇宙中流浪的四位地球信使。

除了发射探测器,人类还在不断加大投入,增强自己的观察能力- 建造“地球的眼睛”。

  • 1990年4月25日,美国航空航天局NASA使用航天飞机将哈勃太空望远镜送入预定轨道,从此人类将在近地轨道拥有了自己看宇宙的“眼睛”。由于没有大气湍流的干扰,它所获得的图像和光谱具有极高的稳定性和可重复性,其清晰度是地面天文望远镜的10倍以上。

img{512x368}
哈勃望远镜

  • 2016年9月25日,位于中国贵州黔南州平塘县大窝凼的世界最大单口径射电望远镜——500米口径球面射电望远镜(FAST)全部竣工并投入使用。

img{512x368}
FAST:世界最大单口径射电望远镜

不过,虽然发射出去如此多的探测器,建造了如此先进的“千里眼”,尴尬的是人类文明到目前为止依然没有得到地外文明的任何蛛丝马迹。我们不禁再次发问:外星人究竟在哪?为何还没降落到地球?

对于这样一个问题,从技术角度来说,人类肯定无法给出答案。但这并不影响我们去管窥到这个问题答案的一角,因为我们还有想象力和逻辑推理能力。

2、关于“文明”

“文明” ,对应的英文单词是civilization,英英释义是“the way people live together in a certain place at a certain time“,中文字面意思的理解就是一群人在一定的时间、在一定的地点生活在一起的方式,这是一个极具弹性的定义。时间、地点、人群数量的跨度都是可大可小、可长可短的。

按照这个定义,我们可以简单粗暴地将文明分类为:局部文明(比如:中国文明、西方文明、玛雅文明等)、星球文明(比如:地球文明)、星系文明(比如:银河系文明)、宇宙文明….。之所以给出这个分类,是因为我们采用“以己为鉴、以史为镜”的方法来推测地外文明。

谈到人类对宇宙中文明形态、行为的推测,我们不得不提到中国科幻大师刘慈欣在其巨著《三体》系列中提出的“宇宙社会学”的概念和大胆推测:

基本公理:
一、生存是文明的第一需要。
二、文明不断增长和扩张,但宇宙中的物质总量保持不变。

猜疑链:
一个文明无法判断另一个文明对自己是善意或恶意的;
一个文明无法判断另一个文明认为自己是善意或恶意的;
一个文明无法判断另一个文明判断自己对她是善意或恶意的;
一个文明不能判断另一个文明是善文明还是恶文明;
一个文明不能判断另一个文明是否会对本文明发起攻击。

黑暗森林法则:宇宙就是一座黑暗森林,每个文明都是带枪的猎人,像幽灵般潜行与林间,轻轻拨开挡路的树枝,竭力不让脚步发出一点儿声音,连呼吸都必须小心翼翼......他必须小心,因为林中到处都有与他一样
潜行的猎人,如果他发现了别的生命,不管是不是猎人,不管是天使还是魔鬼,不管是娇嫩的婴儿还是步履蹒跚的老人,也不管是天仙般的少女还是天神般的男孩,能做的只有一件事:开枪消灭之?在
这片森林中,他人就是地狱,就是永恒的威胁,任何暴露自己存在的生命都将很快被消灭,这就是宇宙文明的图景,这就是对费米悖论的解释。

自己时常思考这样的一个问题,人类发展的终极目标是什么?从古代农耕布织、到18世纪的以蒸汽机器代替手工的第一次工业革命、从19世纪中后的以电气时代为特征的第二次工业革命、到20世纪的以核能、计算机为特征的第三次工业革命、再到近来开启的以互联网产业、人工智能为代表的“第四次工业革命”,人类都在追求生产力提升、生产效率的提升。当这种生产力提升到一个极大值之后,人类的下一个目标是什么呢?很多人会说,飞出地球,在其他星球建立殖民地,但这种行为背后的目的又是什么呢?地球文明的延伸?从根本上来说,这也是为了地球文明的存续,不管这是主动的(比如:为了商业目的的、政治和军事目的的)还是被动的(比如说:当地球环境将在未来一段时间后不再适合人类生存)。这基本符合大刘提到的基本公理第一条,但这不仅是第一需要,更是终极目标

在明确了自己文明的终极目标之后,在对外其他文明这个问题上,大刘给出了一个“黑暗”的推理 – 黑暗森林法则。关于这个法则,不得不说大刘的超一流的想象力,但同样是针对这一法则,争论也是存在的。”黑暗森林”法则的一个不可忽视的前提:“黑暗”。大刘直接给出设定:宇宙就是一座黑暗森林。这种设定本身就是“黑暗”的!难道文明存续的必须以毫无顾忌的消灭其他文明作为手段么?真的没有一个善良的文明么?真的不存在“光明森林”么?宇宙这么大,真不见得。如果人类拥有了毁灭一个星系的能力、并且观测到一个存在文明的行星,人类会毫不犹豫地直接将其毁灭吗?至少我们这个文明的大多数人不会赞同这么做。

3、关于文明间的“交互”

如果我们抛弃“黑暗森林”这一设定,我们生活在一个“光明森林”的宇宙中,那么“文明”间又是如何“交互(我也不是很确定,使用交互这个词是否准确)”的呢?

我们假设有两个独立发展的文明A和B,我们简单的为这两种文明定义了两种能力:观察能力和到达能力。在两个文明真正交互之前,文明的发展可能是这样的:

img{512x368}

  • a) 文明独立进化

初始阶段,两个文明独立进化,就像人类文明一样,可能从低级生命进化为高级生命,学会工具使用,智能提升,生产力提升,形成持续的、稳定的繁衍能力,技术水平也在不断提升。但这个阶段尚未具备宇宙观察能力,更没有通过观察确认文明存在的能力,尚未发展出航天技术。

  • b) 文明拥有初步观察能力和到达能力

随着两个文明的进化,各自发展出了对宇宙的观察能力和一定范围的到达能力。但这些能力有限,还不足以发现和确认对方文明的存在。

  • c) 一个文明具备了观察并确定对方文明存在的能力

一个文明A出现了技术爆炸,观察能力大幅提升,观察到并确认了B文明的存在。但此时A文明尚不具备到达B文明的能力。

  • d) 一个文明具备了到达对方文明的能力

A文明的技术爆炸继续,并发展出了到达B文明的能力。

接下来A文明会怎么做?这是需要重点探讨的。在大刘的“黑暗森林”下,三体星人出动了“质子”抑制地球科技发展、派出三体舰队意图灭绝人类;”歌者”更为直接地祭出了“二向箔”降维攻击武器毁灭了太阳系。但如果是在“光明森林”中呢?我想大刘的“猜疑链”依旧有效。A文明无法判断B文明的善与恶。于是A可能做出以下几个动作:

  • 远距离观察:虽然具备了到达能力,但A文明可能继续原则远距离观察B文明的先进程度,如果这是可以的话;

  • 近距离观察:如果A文明能基本确认B文明的技术水平远低于自己,A文明可能会派出“观察者”,到达B文明,并以一种“隐身”的方式近距离观察B文明,就像《星际迷航》中企业号所做的那样。

  • 接触:在确认了B文明足够善意的基础上,无论出于什么目的,A文明可能会主动接触B文明了。但必须肯定的是这种接触是十分危险的,即便这种接触的初衷是善意的。回顾人类内部的不同局部文明间的接触史,我们可以推测出这种接触很大可能是需要付出血的代价的 ,战争也许不可避免(我们只能以人类文明内部的接触作为参照物,否则还能怎么样呢)。

  • 融合:融合可能会发生,虽然可能是以一方文明付出惨重损失为代价的。但文明的存续的目的得以实现。

4、结论

地球文明目前尚处于具有一定观察能力和一定到达能力的阶段,非常初级。如果按照上面的文明发展阶段,应该处于b)阶段早期。地球文明的观察能力有限,关键是无法确认文明存在,到达能力也更是很有限的。好了,现在我们来分析文章开头提出的问题:外星文明为何没有降落到地球?有了我们之前的铺垫,针对这个问题,我们可以有几种可能的答案:

  • 不存在另外一个文明。从个人情感出发希望不存在这种情况,否则我们真成为宇宙中的孤独文明了;
  • 其他文明都和我们文明的发展阶段差不多,无法观察到我们,更无法到达我们;
  • 某个或某些高等文明在远距离观察我们或近距离以“隐身”的方式观察我们,尚在评估是否与我们接触。

从我们还存在这一事实,似乎证明宇宙并不“黑暗”,否则地球这么折腾(向深空发射飞行器等),也许早就被灭了。但如果宇宙法则真的是遵循大刘的“黑暗森林”法则,那么我们应该庆幸我们还没有被高等文明所发现。人类应该做的,唯有韬光养晦,努力发展科技,争取早日进入“技术爆炸”阶段,这才是存续地球文明的基础。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多