标签 Opensource 下的文章

使用Golang开发微信公众平台-接收文本消息

一旦接入验证成功,成为正式开发者,你可能会迫不及待地想通过手机微信发送一条"Hello, Wechat”到你的公众号服务器。不过上一篇的那个程序还无法处理手机提交的文本消息,本篇将介绍如何用Golang编写公众号程序来接收手机端发送的 文本消息以及回复响应消息。

根据微信公众平台开发文档中描述:“当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上”。我们 用一个示意图展示一下这个消息流程:

微信服务器通过一个HTTP Post请求将终端用户发送的消息转发给公众号服务器,消息内容被包装在HTTP Post Request的Body中。数据包以XML格式存储,文本类消息XML格式样例如下(引自微信公众平台开发文档):

数据包中各个字段的含义都显而易见,我们重点关注的时Content这个字段填写的内容,也就是终端用户发送的消息内容。为了得到这个字段值,我 们需要解析微信服务器发来的HTTP Post包的Body。

在“接入验证”一文中我们提到过,微信服务器发起的请求都带有验证字段,可被公众号服务用于验证HTTP Request是否来自于微信服务器,避免恶意请求。这些用于验证来源的信息,不仅仅在接入验证阶段会发给公众号服务器,在后续微信服务器与公众号服务器 的消息交互过程中,HTTP Request中也都会携带这些信息(注意:没有echostr参数了)。

下面我们来看接收文本消息的Golang程序。

一、接收文本消息

公众号所用的HTTP Server可以沿用“接入验证”一文中的那个main中的Server,我们需要修改的是procRequest函数。

在procRequest函数中,我们保留validateUrl,用于校验请求是否来自于微信服务器。

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }
        log.Println("Wechat Service: validateUrl Ok!")
       
        … …//在此解析HTTP Request Body
}

通过验证后,我们开始解析HTTP Request的Body,Body中的数据是XML格式的,我们可以通过Golang标准库encoding/xml包中提供的函数对Body进行解 析。encoding/xml根据xml字段名与struct字段名或struct tag(struct中每个字段后面反单引号引用的内容,比如xml: "xml")的对应关系将xml数据中的字段值解析到struct的字段中,因此我们需要根据这个xml包的组成定义出对应该格式的struct,这个 struct定义如下:

type TextRequestBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   string
        FromUserName string
        CreateTime   time.Duration
        MsgType      string
        Content      string
        MsgId        int
}

其中FromUserName是发送方账号,这是一个OpenID,每个微信用户针对某个关注的公众号都有唯一OpenID。举个例 子:"tonybai"这个微信用户,关注了"GoNuts"和"GoDev"两个公众号,则"tonybai"发给GoNuts的消息中的 OpenID是“tonybai-gonuts”,而tonybai发给GoDev的消息中的OpenID则是“tonybai-godev”。

MsgId是一个64位整型,可用于消息排重。对于一个HTTP Post,微信服务器在五秒内如果收不到响应会断掉连接,并且针对该消息重新发起请求,总共重试三次。严谨的公众号服务端实现是应该实现消息排重功能的。

通过encoding/xml包中的Unmarshal函数,我们将上面的xml数据转换为一个TextRequestBody实例,具体代码如 下:

//recvtextmsg_unencrypt.go
func parseTextRequestBody(r *http.Request) *TextRequestBody {
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
                log.Fatal(err)
                return nil
        }
        fmt.Println(string(body))
        requestBody := &TextRequestBody{}
        xml.Unmarshal(body, requestBody)
        return requestBody
}

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }

        if r.Method == "POST" {
                textRequestBody := parseTextRequestBody(r)
                if textRequestBody != nil {
                        fmt.Printf("Wechat Service: Recv text msg [%s] from user [%s]!",
                                textRequestBody.Content,
                                textRequestBody.FromUserName)
                }
        }
}

构建并执行该程序:

$>sudo ./recvtextmsg_unencrypt
2014/12/19 08:03:27 Wechat Service: Start!

通过手机微信或公众开发平台提供的页面调试工具发送"Hello, Wechat",我们可以看到如下输出:

2014/12/19 08:05:51 Wechat Service: validateUrl Ok!
Wechat Service: Recv text msg [Hello, Wechat] from user [oBQcwuAbKpiSAbbvd_DEZg7q27QI]!

上述接收"Hello, Wechat"文本消息的Http抓包分析文本如下(Copy from wireshark output):

POST /?signature=9b8233c4ef635eaf5b9545dc196da6661ee039b0&timestamp=1418976343&nonce=1368270896 HTTP/1.0\r\n
User-Agent: Mozilla/4.0\r\n
Accept: */*\r\n
Host: wechat.tonybai.com\r\n
Pragma: no-cache\r\n
Content-Length: 286\r\n
Content-Type: text/xml\r\n

公众号服务器给微信服务器返回的HTTP Post Response为:

HTTP/1.0 200 OK\r\n
Date: Fri, 19 Dec 2014 08:05:51 GMT\r\n
Content-Length: 0\r\n
Content-Type: text/plain; charset=utf-8\r\n

二、响应文本消息

上面的例子中,终端用户发送"Hello, Wechat",虽然公众号服务器成功接收到了这段内容,但终端用户并没有得到响应,这显然不那么友好!这里我们来给终端用户补发一个文本消息的响 应:Hello,用户OpenID。

这类响应消息可以通过HTTP Post Request的Response包携带,将数据放入Response包的Body中,当然也可以单独向微信公众平台发起请求(后话)。微信公众平台开发 文档中关于被动的文本消息响应的定义如下:

这与前面的接收消息结构极其类似,字段含义也不说自明。Golang encoding/xml中的Marshal(和MarshalIndent)函数提供了将struct编码为XML数据流的功能,它是 Unmarshal的逆过程,Golang实现回复 文本响应消息的代码如下:

type TextResponseBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   string
        FromUserName string
        CreateTime   time.Duration
        MsgType      string
        Content      string
}

func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {
        textResponseBody := &TextResponseBody{}
        textResponseBody.FromUserName = fromUserName
        textResponseBody.ToUserName = toUserName
        textResponseBody.MsgType = "text"
        textResponseBody.Content = content
        textResponseBody.CreateTime = time.Duration(time.Now().Unix())
        return xml.MarshalIndent(textResponseBody, " ", "  ")
}

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }

        if r.Method == "POST" {
                textRequestBody := parseTextRequestBody(r)
                if textRequestBody != nil {
                        fmt.Printf("Wechat Service: Recv text msg [%s] from user [%s]!",
                                textRequestBody.Content,
                                textRequestBody.FromUserName)
                        responseTextBody, err := makeTextResponseBody(textRequestBody.ToUserName,
                                textRequestBody.FromUserName,
                                "Hello, "+textRequestBody.FromUserName)
                        if err != nil {
                                log.Println("Wechat Service: makeTextResponseBody error: ", err)
                                return
                        }
                        fmt.Fprintf(w, string(responseTextBody))
                }
        }
}

编译执行上面程序后,通过手机微信或网页调试工具发送一条"Hello, Wechat"到公众号,公众号会响应如下信息:“Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI",手机端微信会正确接收该响应。

上述响应的抓包分析如下。公众号服务器给微信服务器返回的HTTP Post Response为:

HTTP/1.0 200 OK\r\n
Date: Fri, 19 Dec 2014 09:03:55 GMT\r\n
Content-Length: 220\r\n
Content-Type: text/plain; charset=utf-8\r\n

\r\n
<xml><ToUserName>oBQcwuAbKpiSAbbvd_DEZg7q27QI</ToUserName><FromUserName>gh_xxxxxxxx</FromUserName><CreateTime>1418979835</CreateTime><MsgType>text</MsgType><Content>Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI</Content></xml>

三、关于Content-Type设置

虽然Content-Type为:text/plain; charset=utf-8的 响应信息可以被微信平台正确解析,但通过抓取微信平台给公众号服务器发送的HTTP Post Request来看,在发送xml数据时微信服务器用的Content-Type为Content-Type: text/xml。我们的响应信息Body也是xml数据包,我们能否为响应信息重新设置Content-Type为 text/xml呢?我们可以通过如下代码设置:

w.Header().Set("Content-Type", "text/xml")
fmt.Fprintf(w, string(responseTextBody))

不过奇怪的是我通过AWS EC2上抓包得到的Content-Type始终是“text/plain; charset=utf-8”。但利用ngrok映射到本地端口后抓包看到的却是正确的"text/xml",在AWS本地用 curl -d xxx.xxx.xxx.xxx测试公众号服务程序而抓到的包也是正确的。通过代码没看出什么端倪,因为逻辑上显式设置Header的Content- Type后,Go标准库不会在sniff内容的格式了。

通过ngrok映射本地80端口后,得到的HTTP Post Response抓包分析文字:

HTTP/1.1 200 OK\r\n
Content-Type: text/xml\r\n
Date: Sat, 20 Dec 2014 04:29:16 GMT\r\n
Content-Length: 220\r\n

xml数据包这里忽略。

四、CDATA的使用

从抓包可以看到,我们回复的响应中的XML数据包是不带CDATA,即便这样微信客户端接收也没有问题。但这并未严遵循协议样例。

XML下CDATA含义是:在标记CDATA下,所有的标记、实体引用都被忽略,而被XML处理程序一视同仁地当做字符数据看待,CDATA的形 式如下:

<![CDATA[文本内容]]>

我们尝试加上为每个文本类型的字段值上直接添加CDATA标记。

func value2CDATA(v string) string {
        return "<![CDATA[" + v + "]]>"
}

func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {
        textResponseBody := &TextResponseBody{}
        textResponseBody.FromUserName = value2CDATA(fromUserName)
        textResponseBody.ToUserName = value2CDATA(toUserName)
        textResponseBody.MsgType = value2CDATA("text")
        textResponseBody.Content = value2CDATA(content)
        textResponseBody.CreateTime = time.Duration(time.Now().Unix())
        return xml.MarshalIndent(textResponseBody, " ", "  ")
}

这样修改后,我们试着发一条消息给微信公众号平台,不过结果并不正确。手机微信无法收到响应信息,并显示“该公众号暂时无法提供服务,请稍后再 试”。通过Println输出Body可以看到:

<xml><ToUserName>&lt;![CDATA[oBQcwuAbKpiSAbbvd_DEZg7q27QI]]&gt;&lt;![CDATA[gh_1fd4719f81fe]]&gt;</FromUserName><CreateTime>1419051400</CreateTime><MsgType>&lt;![CDATA[text]]&gt;</MsgType><Content>&lt;![CDATA[Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI]]&gt;</Content></xml>

可以看到左右尖括号分别被转义为&lt;和&gt;了,这显然不是我们想要的结果。那如何加入CDATA标记呢。Golang并 不直接显式支持生成CDATA字段的xml流,我们只能间接实现。前面提到过struct定义时的struct tag,golang xml包规定:"a field with tag ",innerxml" is written verbatim, not subject to the usual marshalling procedure"。 大致的意思是如果一个字段的struct tag是",innerxml",则Marshal时字段值原封不动,不提交给通常的marshalling程序。我们就利用innerxml来实现 CDATA标记。

type TextResponseBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   CDATAText
        FromUserName CDATAText
        CreateTime   time.Duration
        MsgType      CDATAText
        Content      CDATAText
}

type CDATAText struct {
        Text string `xml:",innerxml"`
}

func value2CDATA(v string) CDATAText {
        return CDATAText{"<![CDATA[" + v + "]]>"}
}

编译程序后测试,这回CDATA标记正确了,微信客户端也收到的响应信息。

五、用ngrok在本地调试微信公众平台接口

在“接入验证”一文中,我们建议申请诸如AWS EC2来应对微信公众平台接口开发,但其方便程度毕竟不如本地。网上一开源工具ngrok可以帮助我们实现本地调试微信公众平台接口。

使用ngrok的步骤如下:

1、下载ngrok
 ngrok也是使用golang实现的,因此主流平台都支持。ngrok下载后就是一个可执行的二进制文件,可直接执行(放在PATH路径 下)。

2、注册ngrok
到ngrok.com上注册一个账号,注册成功后,就能看到ngrok.com为你分配的auth token,把这个auth token放到~/.ngrok中:

auth_token:YOUR_AUTH_TOKEN

3、执行ngrok

$ngrok 80

ngrok                                                                                                                                                           (Ctrl+C to quit)

Tunnel Status                 online
Version                       1.7/1.6
Forwarding                    http://xxxxxxxx.ngrok.com -> 127.0.0.1:80
Forwarding                    https://xxxxxxxx.ngrok.com -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        1
Avg Conn Time                 1.90ms

其中"xxxxxxxx.ngrok.com"就是ngrok为你分配的子域名。

在你的微信开发者中心将这个地址配置到URL字段中,提交验证,验证消息就会顺着ngrok建立的隧道流到你的local机器的80端口上。

另外本地调试抓包,要用loopback网口,比如:
$sudo tcpdump -w http.cap -i lo0 tcp port 80

本篇文章涉及的代码在这里可以找到。

godep的一个“坑”

很多人学习和使用Golang一段时间后,都会被golang的第三方包依赖版本搞得有些烦躁,golang设计者最初过于乐观的设计使得今天大 家不得不各自想办法解决这个问题。godep就是综合了多年第三方包依赖问题的解决方案后的一个趋向统一的方案,至少是在go get的设计没有进化前的一个比较不错的方案。

今天试用了一把godep,不过“体验”并不理想,这缘于我遇到了godep的一个“坑”,不过是那种你在正式项目中不一定遇到的“坑”,这里来说到说到。

按照godep官方使用说明的第一步,先下载godep:

$ go get github.com/tools/godep
$godep
Godep is a tool for managing Go package dependencies.

Usage:

    godep command [arguments]

The commands are:

    save     list and copy dependencies into Godeps
    go       run the go tool in a sandbox
    get      download and install packages with specified dependencies
    path     print sandbox path for use in a GOPATH
    restore  check out listed dependency versions in GOPATH
    update   use different revision of selected packages

Use "godep help [command]" for more information about a command.

确认正确下载后,我们来准备一个测试例子,目录如下:

$GOPATH/
    src/
        tonybai.com/
                foolib/
                   foo.go
                fooapp/
                   main.go
       
   
//foo.go
package foo

func Add(a, b int) int {
        return a + b
}

//main.go
package main

import (
        "fmt"
        foo "tonybai.com/foolib"
)

func main() {
        fmt.Println(foo.Add(1, 3))
}

fooapp下,编译执行程序:

$go run main.go
4

接下来godep登场,根据godep文档中得步骤,接下来我们应该在一个构建依赖关系完整的项目中执行godep save以保存依赖关系以及依赖的当前版本第三方包:

$godep save
godep: directory "/Users/tony/Test/GoToolsProjects/src" is not using a known version control system
godep: error loading dependencies

出错了!godep提示$GOPATH/src目录没有使用任何版本控制系统(not using a known version control system)。 奇怪啊!这个错误什么意思呢?难道使用godep还需要将$GOPATH/src整体作为一个Project纳入git or subversion repository中?无奈之下,我只能先这么做,再作观察。我在$GOPATH下执行git init,建立一个local git repository,然后将src add到这个repository中。

回到fooapp下,再次执行godep save,居然依旧是同样地错误结果。于是到godep的issues中去查,看看是否有人和我遇到了同样地问题!godep的#116 issue中提到的问题恰恰和我的一致,不过这个issue一 直是open状态,也没有人comments。接着翻看一下godep的源码,godep依赖一些第三方包,save这个命令在分析版本控制工具库时也是 调用了多层外部包实现的,短时间内无法定位问题。

静想一下,godep是管理第三方包依赖关系的,而第三方包多是go get下载的,是不是foolib要放到repository中才行呢?于是尝试在foolib中建立git repository并做一次commit。第三次在fooapp下执行godep save,错误依旧!

难道fooapp也必须放在repository中?试试吧。在fooapp下init一个git repository,将fooapp下的main.go提交到repository中。再执行godep save:

$godep save
$ls -l
total 8
drwxr-xr-x  5 tony  staff  170 10 30 22:01 Godeps/
-rw-r–r–  1 tony  staff  103 10 30 21:44 main.go

这回成功了!godep save在fooapp下建立了Godeps目录,其结构如下:

$ls -R
Godeps.json    Readme        _workspace/

./_workspace:
src/

./_workspace/src:
tonybai.com/

./_workspace/src/tonybai.com:
foolib/

./_workspace/src/tonybai.com/foolib:
foolib.go

godep将当前版本的foolib copy到Godeps/_workspace下了。

Godeps.json记录了fooapp对foolib的依赖关系:

{
        "ImportPath": "fooapp",
        "GoVersion": "go1.3",
        "Deps": [
                {
                        "ImportPath": "tonybai.com/foolib",
                        "Rev": "20a9c2a682537813d37847f2f270bf929672cc84"
                }
        ]
}

godep记录了foolib的当前revision number,这个number恰是我最新一次commit的hash code:

~/Test/GoToolsProjects/src/tonybai.com/foolib]$git log
commit 20a9c2a682537813d37847f2f270bf929672cc84
Author: Tony Bai <bigwhite.cn@gmail.com>
Date:   Thu Oct 30 22:00:25 2014 +0800

    init

到这里让我觉得godep的设计思路有些与我的buildcC程序辅助构建工具)的思路有些类似,只是godep做得更彻底:

    1、godep将项目依赖统统放到项目的私有_workspace下,而buildc是共享的,通过project下的版本号配置区分依赖
    2、godep将依赖管理到revision(修订号)级别,buildc只是根据version来区分依赖。

godep的辅助构建原理(godep go build main.go)通过一条命令即可看出来:

$godep go env
GOARCH="amd64"
GOBIN="/usr/local/go/bin"
GOCHAR="6"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tony/Test/GoToolsProjects/src/fooapp/Godeps/_workspace:/Users/tony/Test/GoToolsProjects"

godep临时将_workspace放在GOPATH列表的前面,这样gc在编译时就会按顺序先在_workspace下面找依赖包,这样fooapp的私有依赖就会理所当然的被gc用到,即便在其他GOPATH路径下有同名包(可能是不同版本的)。

显然这也算是godep的一个小bug吧(或者是godep依赖的包的bug,目前不确认),毕竟提示的路径是不正确的,不应该提示"/Users/tony/Test/GoToolsProjects/src" is not using a known version control system,而应该是"/Users/tony/Test/GoToolsProjects/src/tonybai.com/foolib或"/Users/tony/Test/GoToolsProjects/src/fooapp没有版本控制系统的repository留存。

另外觉得godep的author应该把这个“坑”作为一个使用godep的前提进行说明,并在github主页给出明确展示,即便这个“坑”多数人可能不会遇到。

Golang跨平台交叉编译

近期在某本书上看到Go跨平台交叉编译的强大功能,于是想自己测试一下。以下记录了测试过程以及一些结论,希望能给大家带来帮助。

我的Linux环境如下:

uname -a
Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ go version
go version go1.3.1 linux/amd64

跨平台交叉编译涉及两个重要的环境变量:GOOS和GOARCH,分别代表Target Host OS和Target Host ARCH,如果没有显式设置这些环境变量,我们通过go env可以看到go编译器眼中这两个环境变量的当前值:

$ go env
GOARCH="amd64"
GOOS="linux"

GOHOSTARCH="amd64"
GOHOSTOS="linux"

… …

这里还有两个变量GOHOSTOS和GOHOSTARCH,分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的,因此默认情况下,这两组环境变量的值都是来自当前主机的信息。

现在我们就来交叉编译一下:在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序,样例程序如下:

//testport.go
package main

import (
        "fmt"
        "os/exec"
        "bytes"
)

func main() {
        cmd := exec.Command("uname", "-a")
        var out bytes.Buffer
        cmd.Stdout = &out

        err := cmd.Run()
        if err != nil {
                fmt.Println("Err when executing uname command")
                return
        }

        fmt.Println("I am running on", out.String())
}

在Linux/amd64下编译运行:

$ go build -o testport_linux testport.go
$ testport_linux
I am running on Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

接下来,我们来尝试在Linux/amd64上编译一个可以运行在darwin/amd64上的程序。我只需修改GOOS和GOARCH两个标识目标主机OS和ARCH的环境变量:

$ GOOS=darwin GOARCH=amd64 go build -o testport_darwin testport.go
go build runtime: darwin/amd64 must be bootstrapped using make.bash

编译器报错了!提示darwin/amd64必须通过make.bash重新装载。显然,通过安装包安装到linux/amd64下的Go编译器还无法直接交叉编译出darwin/amd64下可以运行的程序,我们需要做一些准备工作。我们找找make.bash在哪里!

我们到Go的$GOROOT路径下去找make.bash,Go的安装路径下的组织很简约,扫一眼便知make.sh大概在$GOROOT/src下,打开make.sh,我们在文件头处看到如下一些内容:

# Environment variables that control make.bash:
#
# GOROOT_FINAL: The expected final Go root, baked into binaries.
# The default is the location of the Go tree during the build.
#
# GOHOSTARCH: The architecture for host tools (compilers and
# binaries).  Binaries of this type must be executable on the current
# system, so the only common reason to set this is to set
# GOHOSTARCH=386 on an amd64 machine.
#
# GOARCH: The target architecture for installed packages and tools.
#
# GOOS: The target operating system for installed packages and tools.

… …

make.bash头并未简要说明文件的用途,但名为make.xx的文件想必是用来构建Go编译工具的。这里提到几个环境变量可以控制 make.bash的行为,显然GOARCH和GOOS更能引起我们的兴趣。我们再回过头来输出testport.go编译过程的详细信息:

$ go build -x -o testport_linux testport.go
WORK=/tmp/go-build286732099
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

我们发现Go实际上用的是$GOROOT/pkg/tool/linux_amd64下的6g(编译器)和6l(链接器)来完成整个编译过程的,看到6g 和6l所在目录名为linux_amd64,我们可以大胆猜测编译darwin/amd64 go程序应该使用的是$GOROOT/pkg/tool/darwin_amd64下的工具。不过在我在$GOROOT/pkg/tool下没有发现 darwin_amd64目录,也就是说我们通过安装包安装的Go仅自带了for linux_amd64的编译工具,要想交叉编译出for darwin_amd64的程序,我们需要通过make.bash来手工编译出这些工具。

tonybai@ubuntu-Server-14:/usr/local/go/pkg$ ls
linux_amd64  linux_amd64_race  obj  tool

tonybai@ubuntu-Server-14:/usr/local/go/pkg/tool$ ls
linux_amd64

根据前面make.bash的用法说明,我们来尝试构建一下:

cd $GOROOT/src
sudo GOOS=darwin GOARCH=amd64 ./make.bash

# Building C bootstrap tool.
cmd/dist

# Building compilers and Go bootstrap tool for host, linux/amd64.
… …
cmd/cc
cmd/gc
cmd/6l
cmd/6a
cmd/6c
cmd/6g
pkg/runtime
… …
cmd/go
pkg/runtime (darwin/amd64)

# Building packages and commands for host, linux/amd64.
runtime
… …
text/scanner

# Building packages and commands for darwin/amd64.
runtime
errors
… …
testing/quick
text/scanner


Installed Go for darwin/amd64 in /usr/local/go
Installed commands in /usr/local/go/bin

编译后,我们再来试试编译for darwin_amd64的程序:

$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go
WORK=/tmp/go-build972764136
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_darwin -L $WORK -extld=gcc $WORK/command-line-arguments.a

将文件copy到我的Mac Air下执行:

$chmod +x testport_darwin
$testport_darwin
I am running on Darwin TonydeMacBook-Air.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64

编译虽然成功了,但从-x输出的详细编译过程来看,Go编译连接使用的工具依旧是linux_amd64下的6g和6l,为什么没有使用darwin_amd64下的6g和6l呢?原来$GOROOT/pkg/tool/darwin_amd64下根本就没有6g和6l:

/usr/local/go/pkg/tool/darwin_amd64$ ls
addr2line  cgo  fix  nm  objdump  pack  yac
c

但查看一下pkg/tool/linux_amd64/下程序的更新时间:

/usr/local/go/pkg/tool/linux_amd64$ ls -l
… …
-rwxr-xr-x 1 root root 2482877 10月 20 15:12 6g
-rwxr-xr-x 1 root root 1186445 10月 20 15:12 6l
… …

我们发现6g和6l都是被刚才的make.bash新编译出来的,我们可以得出结论:新6g和新6l目前既可以编译本地程序(linux/amd64),也可以编译darwin/amd64下的程序了,例如重新编译testport_linux依旧ok:

$ go build -x -o testport_linux testport.go
WORK=/tmp/go-build636762567
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

如果我们还想给Go编译器加上交叉编译windows/amd64程序的功能,我们再执行一次make.bash:

sudo GOOS=windows GOARCH=amd64 ./make.bash

编译成功后,我们来编译一下Windows程序:

$ GOOS=windows GOARCH=amd64 go build -x -o testport_windows.exe testport.go
WORK=/tmp/go-build626615350
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_windows.exe -L $WORK -extld=gcc $WORK/command-line-arguments.a

把testport_windows.exe扔到Windows上执行,结果:

Err when executing uname command

显然Windows下没有uname命令,提示执行出错。

至此,我的Go编译器具备了在Linux下编译windows/amd64和darwin/amd64的能力。如果你还想增加其他平台的能力,就像上面那样操作执行make.bash即可。

如果在go源文件中有与C语言的交互代码,那么交叉编译功能是否还能奏效呢?毕竟C在各个平台上的运行库、链接库等都是不同的。我们先来看看这个例子,我们使用之前在《探讨docker容器对共享内存的支持情况》一文中的一个例子:

//testport_cgoenabled.go
package main

//#include <stdio.h>
//#include <sys/types.h>
//#include <sys/mman.h>
//#include <fcntl.h>
//
//#define SHMSZ     27
//
//int shm_rd()
//{
//      char c;
//      char *shm = NULL;
//      char *s = NULL;
//      int fd;
//      if ((fd = open("./shm.txt", O_RDONLY)) == -1)  {
//              return -1;
//      }
//
//      shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);
//      if (!shm) {
//              return -2;
//      }
//
//      close(fd);
//      s = shm;
//      int i = 0;
//      for (i = 0; i < SHMSZ – 1; i++) {
//              printf("%c ", *(s + i));
//      }
//      printf("\n");
//
//      return 0;
//}
import "C"

import "fmt"

func main() {
        i := C.shm_rd()
        if i != 0 {
                fmt.Println("Mmap Share Memory Read Error:", i)
                return
        }
        fmt.Println("Mmap Share Memory Read Ok")
}

我们先编译出一个本地可运行的程序:

$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go
WORK=/tmp/go-build977176241
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
CGO_LDFLAGS="-g" "-O2" /usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ — -I $WORK/command-line-arguments/_obj/ testport_cgoenabled.go
/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_defun.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_defun.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_main.o -c $WORK/command-line-arguments/_obj/_cgo_main.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_export.o -c $WORK/command-line-arguments/_obj/_cgo_export.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -c $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_cgo_.o $WORK/command-line-arguments/_obj/_cgo_main.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2
/usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ -dynimport $WORK/command-line-arguments/_obj/_cgo_.o -dynout $WORK/command-line-arguments/_obj/_cgo_import.c
/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_import.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_import.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_all.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2 -Wl,-r -nostdlib /usr/lib/gcc/x86_64-linux-gnu/4.8/libgcc.a
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -D _/home/tonybai/Test/Go/porting -I $WORK -pack $WORK/command-line-arguments/_obj/_cgo_gotypes.go $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo1.go
pack r $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/_cgo_import.6 $WORK/command-line-arguments/_obj/_cgo_defun.6 $WORK/command-line-arguments/_obj/_all.o # internal
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_cgoenabled_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

输出了好多日志!不过可以看出Go编译器先调用CGO对Go源码中的C代码进行了编译,然后才是常规的Go编译,最后通过6l链接在一起。Cgo似乎直接使用了Gcc。我们再来试试跨平台编译:

$ GOOS=darwin GOARCH=amd64 go build -x -o testport_cgoenabled_darwin testport_cgoenabled.go
WORK=/tmp/go-build124869433
can't load package: no buildable Go source files in /home/tonybai/Test/Go/porting

当我们编译for Darwin/amd64平台的程序时,Go无法像之前那样的顺利完成编译,而是提示错误。从网上给出的资料来看,如果Go源码中包含C互操作代码,那么 目前依旧无法实现交叉编译,因为cgo会直接使用各个平台的本地c编译器去编译Go文件中的C代码。默认情况下,make.bash会置 CGO_ENABLED=0。

如果你非要将CGO_ENABLED设置为1去编译go的话,至少我得到了如下错误,导致无法编译通过:

$ sudo CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash –no-clean
… …
# Building packages and commands for darwin/amd64.
… …
37: error: 'AI_MASK' undeclared (first use in this function)

 

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!




这里是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


文章

评论

  • 正在加载...

分类

标签

归档











更多