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

No Comments

一旦接入验证成功,成为正式开发者,你可能会迫不及待地想通过手机微信发送一条"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.Marshal(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

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

使用Golang开发微信公众平台-接入验证

1 Comment

今年我涉猎的领域有些“广泛”,并且有那么一点“跳跃”:从上半年的终端(游戏)开发到下半年golangdocker以及目前将要提及的微信公众平台 接口开发,似乎有些远离了老本行C以及技术管理的内容。但在这个转型以及创新驱动的时代,这显然是顺势而为。寻求与新兴领域的主动接轨,在实打实的实践 中,扩大了自己的视野,并可以进一步甄别发现适合自己的领域。

移动互联网时代,微信平台一枝独秀,是社交领域的巨人,但其诞生也才不到4年。微信平台的发展前景十分广阔,企鹅公司将其打造为人与人、人与物、物与物的统一、万能入口之雄心不变,因此围绕微信平台广大开发者依旧有诸多机会。

微信公众平台接口应该算是微信平台首批对外开放的接口吧。公众平台相对成熟,但其业务模式依旧在演进和创新。公众平台接口的开发并非不难,上手几个月就可 以写成一本诸如“微信公众平台应用开发实践”的事情就发生在你我眼前,因此这里后续有关微信公众平台接口开发的文章也都是一些入门级的,我个人也是边学 习,边实践,边记录,边分享,就像上半年写Cocos2d-x文章那样。

一、公众号申请(可选

本着“再小的个体,也有自己的品牌”的微信公众平台产品哲学,只要你是合法自然人类,你就可以到https://mp.weixin.qq.com/上申请一个公众号,一般对于个体而言,只能申请订阅号。

对于具有开发能力的订阅号拥有者,你可以在订阅号的“开发者中心”,启用开发者账号。并且“一旦启用并设置服务器配置后,用户发给公众号的消息以及开发者需要的事件推送,将被微信转发到该URL中”。

不过此时即便你填写相关信息并提交,你也不会通过验证。这正是本篇要告诉你的事情,如何写程序实现微信公众平台的接入验证,后续道来。

二、测试号申请(可选)

正式的订阅号申请有些繁琐,需要提交个人信息,需要审核,不会立即生效。并且未认证的订阅号所能使用的功能接口有限(只能使用普通消息接口),而认证又需 要一笔费用(现价300rmb/次)。对于学习者而言,也许真的没有必要。于是我们在学习开发的过程中可以申请测试号来替代真正的公众号。

测试号是一种体验账号,有效期一年,具有各种功能接口体验权限。测试号可以在http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login下申请,申请时有一个类似公众号开发者配置的页面需要你填写服务器配置。同样,你需要进行接入验证(后续道来)。

一旦申请成功,可以用终端微信app扫一扫测试号的QR,关注微信平台测试号,用于后续平台接口开发测试。

以上公众号和测试号二选一

三、公众号服务器

为何需要公众号服务器,这就要谈及微信公众平台的架构了。

很多人觉得微信公众平台的业务模式有些类似于若干年前火爆一时的短信增值业务模式-SP/CP模式:

【终端用户】 <—-短信—> 【移动运营商移动增值业务网关】 <—-> 【SP/CP服务器】

微信公众号时代,这个业务模式变成了:

【终端用户】 <—-微信消息—> 【微信公众平台】 <—–> 【公众号服务器】

短信变成了微信,SP/CP变成了公众号。微信公众平台将终端用户发给公众号的信息转发至公众号配置的公众号服务器URL,公众号服务器做业务处理后,将响应信息通过微信公众平台再发给终端用户。因此我们需要实现公众号业务逻辑的公众号服务器。

本文标题里所说的“接入验证”,指的就是微信公众平台对公众号服务器提供服务的URL有效性的验证。我们在填写开发者中心的“接口配置信 息”并提交时,微信公众平台会向配置的公众号服务器的URL发送验证Request,只有公众号服务存在,且按要求返回包含特定信息的Response, 我们才能真正通过微信公众平台的验证,“接口配置信息”才真正生效。

因此我们需要一台放在公网的主机。如果采用Golang开发公众号服务的话,这样的主机只能是独立的VPS,像国内新浪提供的app engine主机不能运行Golang,无法满足要求(当然如果你使用其他语言开发的话,比如PHP,那么可用的主机范围就很广泛了)。

这里建议申请一个亚马逊免费EC2主机(t2.micro型,免费一年,学习够用)用作学习测试使用或者购买像LinodeDigitalOcean的VPS。关于如何申请亚马逊主机可以咨询谷歌和度娘,这里不赘述。

注意:Amazon EC2实例默认采用的时动态IP,instance重启后IP会发生变化。因此可申请分配一个Elastic IP,并绑定在你的EC2实例上,目前绑定instance的Elastic IP是免费的,这个IP在instance重启后不会变更。当你EC2主机到期后,记得释放这个IP,否则就收费了。

四、接入验证逻辑

前面提到过,无论是公众号还是测试号,当你提交配置URL时会收到提交失败的信息,这是微信公众平台接入验证失败所致。在公众平台开发者文档中,关于URL验证逻辑如下:

开发者提交信息(包括URL、Token)后,微信服务器将发送Http Get请求到填写的URL上,GET请求携带四个参数:signature、timestamp、nonce和echostr。公众号服务程序应该按如下要求进行接入验证:

1. 将token、timestamp、nonce三个参数进行字典序排序
2. 将三个参数字符串拼接成一个字符串进行sha1加密
3. 将加密后获得的字符串与signature对比,如果一致,说明该请求来源于微信
4. 如果请求来自于微信,则原样返回echostr参数内容

以上完成后,接入验证就会生效,开发者配置提交就会成功。

列出Http抓包分析后的文本,理解起来就更容易些:

微信服务器发出的验证Request如下:

GET /?signature=d01007dcff994c555bc51d22e154956ccdc61ec5&timestamp=1418970951&nonce=484765335&echostr=qwe1235 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: 0\r\n

应答返回如下:
HTTP/1.0 200 OK\r\n
Date: Fri, 19 Dec 2014 06:35:59 GMT\r\n
Content-Length: nn\r\n
Content-Type: text/plain; charset=utf-8\r\n

qwe1235

五、参考实现

环境:AWS t2.micro ubuntu 14.04 x86_64 Server
     go 1.4

Go语言标准库提供了一个强大的http server,我们直接利用这个server来处理微信平台的Url验证请求。另外微信平台发给公众平台服务器的http request都是请求到"/"下的,这样我们的service无需设置太多http route。

//urlvalidation.go
package main

import (
        "crypto/sha1"
        "fmt"
        "io"
        "log"
        "net/http"
        "sort"
        "strings"
)

const (
        token = "wechat4go"
)

func makeSignature(timestamp, nonce string) string {
        sl := []string{token, timestamp, nonce}
        sort.Strings(sl)
        s := sha1.New()
        io.WriteString(s, strings.Join(sl, ""))
        return fmt.Sprintf("%x", s.Sum(nil))
}

func validateUrl(w http.ResponseWriter, r *http.Request) bool {
        timestamp := strings.Join(r.Form["timestamp"], "")
        nonce := strings.Join(r.Form["nonce"], "")
        signatureGen := makeSignature(timestamp, nonce)

        signatureIn := strings.Join(r.Form["signature"], "")
        if signatureGen != signatureIn {
                return false
        }
        echostr := strings.Join(r.Form["echostr"], "")
        fmt.Fprintf(w, echostr)
        return true
}

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!")
}

func main() {
        log.Println("Wechat Service: Start!")
        http.HandleFunc("/", procRequest)
        err := http.ListenAndServe(":80", nil)
        if err != nil {
                log.Fatal("Wechat Service: ListenAndServe failed, ", err)
        }
        log.Println("Wechat Service: Stop!")
}

编译这个go源码,执行urlvalidation。

$> urlvalidation
2014/12/18 17:48:10 Wechat Service: Start!
2014/12/18 17:48:10 Wechat Service: ListenAndServe failed, listen tcp :80: bind: permission denied

程序提示没有权限绑定80端口。80端口只有管理员权限才能绑定,因此我们需要通过sudo方式执行validation。

$ sudo ./urlvalidation
2014/12/18 09:56:29 Wechat Service: Start!

接下来我们回到订阅号开发者中心配置页面或测试号服务器配置页面,点击提交。在我们的公众号服务器后台可以看到如下日志:

2014/12/18 09:56:52 Wechat Service: validateUrl Ok!

同时你的提交也会显示成功,Url已经验证通过,你将正式成为开发者。

如果我们随意构造一个http get 请求发给validate程序,比如:

curl -s http://wechat.tonybai.com(比如我的URL为http://wechat.tonybai.com)

那么我们将看到validation输出如下错误日志:

2014/12/18 10:02:07 Wechat Service: this http request is not from Wechat platform!

以上源码文件在这里可以下载。

处于安全考虑,后续订阅号平台均需要对收到的http request进行验证,以确保请求来源于微信公众平台。

将Blog迁移到DigitalOcean的VPS上

7 Comments

自从2012年初将Blog从Blogbus搬出来放到同事代理的虚拟主机上后,Blog运行一直很稳定,我也算 是比较满意。但同事的主机代理生意这两年来每况愈下,这促使他在前些时候做出了在今年年末放弃这门生意的决定,于是我又不得不为Blog另找落脚儿地了。

这次不想再单纯的买Wordpress虚拟主机了,一来功能有限,二来国外的入门级VPS价格已经与虚拟主机价格逐渐缩小,尤其是像 DigitalOcean这样的后起之秀,5$/mon的入门级配置VPS基本可以满足我的应用。于是DigitalOcean VPS就成为了我的购买目标。DigitalOcean这两年推广力度大,其Promo code的优惠有时可达20$以上,去年黑色星期五当天就给出了50$的优惠码。于是我期望着今天(2014黑色星期五)DigitalOcean的 50$优惠码能再现江湖。

但事与愿违,当时间走入美国当地时间星期五后,网上哪些所谓50$的Promo code依旧无法正常使用。无奈只能退而求次,使用"SHIPITFAST10"这个10$的优惠码,对于入门级VPS来说,10$也够试用两个月的了。

Digital Ocean VPS的注册和购买流程非常简单,按照官方提示一步一步做即可。这里要注意的是如果选择信用卡支付,务必一次填对信用卡信息,否则account就会短暂 无法使用,你需要fill out一个Form,提交给客服人工验证才能解除对你account的封锁。

接下来就是稍详细的说明Wordpress blog迁移到Digital Ocean VPS的步骤了,希望能对大家有所帮助。

一、备份WordPress Blog

网上关于迁移WordPress的方法有许多方案,之前在测试将WordPress迁移到Docker容器中时,我采用的是数据表导出导入+WordPress程序覆盖的方式,这次我依旧采用此方法。

现有的Blog用的是DirectAdmin的后台管理面板,支持全站备份,备份后的文件为:backup-Nov-27-2014-1.tar.gz。这个压缩包中有两个重要的组件(解压后你就可以看到):

    – backup/tonybai_db.sql
    – domains/tonybai.com/public_html/

   
我们要迁移的就是这两个组件。第一个.sql文件就是我们导出的数据库表,需要导入到新主机中的新库中。而第二个则是Wordpress安装后的文件集合,用于直接覆盖目标主机上对应的Wordpress文件包的。

二、创建Digital Ocean VPS Droplet

在填写完信用卡,利用优惠码充值账户成功后,就可以创建Droplet了。Droplet是DO的术语,理解成一个VPS实例即可。Droplet的创建 体验不错,DO已经准备好了各种VPS常用的应用组合以及OS供选择。我选择了5$/mon的Ubuntu 14.04 x64 + WordPress的组合,机房选择San Francisco 1。确认后,DO会开始创建Droplet操作,不到1分钟,Droplet就创建完毕了。如果不用ssh key,则VPS的root密码会发到你的注册邮箱中。有了root和密码,我们就可以通过"ssh root@YOUR_VPS_IP"访问你的VPS了。

首次后台登陆VPS,VPS会强制你修改root登陆密码。

三、初始安装WordPress

现在我们的VPS上已经安装好了WordPress运行所需要的所有软件了,包括apache2、mysql等。修改/etc/hosts,将自己的域名tonybai.com映射为VPS IP。

访问tonybai.com,WordPress的自安装程序启动,按照提示一步一步即可安装好Wordpress,这里带的Wordpress是4.0.0版本(注意:我们后续是要覆盖掉这个 WordPress的)。

安装好后,再访问tonybai.com就可以看到默认安装后的一篇example blog了。

现在我们进入tonybai.com/wp-admin页面,Apache弹出一个登陆框,在DO官方文档提到过,/wp-admin初始情况使用了 apache的.htaccess credential保护机制了,我们需要输入用户名密码才能进入wp-admin页面。这个用户名密码就在/root/WORDPRESS里。

四、导表

接下来,我们先将backup/tonybai_db.sql导入mysql数据库。

mysql的数据库访问密码在/root/.my.cnf中,用户名是root。

管理mysql我们更多使用phpmyadmin工具,于是通过apt-get install phpmyadmin -y安装一个。

为了通过Web页面访问到phpmyadmin,我们还需执行以下两个步骤:

 在/etc/apache2/apache2.conf尾部添加一行:
        Include /etc/phpmyadmin/apache.conf

 重启apache2:service apache2 restart

之后通过tonybai.com/phpmyadmin访问phpmyadmin工具。登录时使用mysql的root和密码即可。

进入phpmyadmin后,我们可以看到前面的Wordpress安装过程在mysql中建立了名为wordpress的数据库以及名为 wordpress的数据库用户。但我之前的blog使用的数据库用户和数据库并非wordpress,而是tonybai_user和tonybaidb,于是我们需要自己创建 tonybaidb数据库以及tonybai_user这个数据库账号。

创建tonybaidb时,注意使用utf8_general_ci字符集。

创建tonybai_user数据库账户时,注意其权限仅局限于localhost发起的访问以及tonybaidb这个数据库,其密码设置为原blog wp-config.php中的数据库密码。

由于phpmyadmin导入的文件不能超过2M,因此我们只能通过后台导表:

    mysql -u root -p
    mysql> use tonybai_db
    database changed
    mysql> source ./tonybai_db.sql

五、替换Wordpress安装文件

默认下wordpress安装到了/var/www下。我们需要将domains/tonybai.com/public_html替换掉/var/www目录:

cd /var
mv www www.bak

将domain/tonybai.com/public_html cp到/var/下,改名为www

chown -R www-data www
chgrp -R www-data www

剩下的就是访问tonybai.com即可。

是不是熟悉的页面和风格又展现在你眼前了!

六、创建SnapShot

DO提供两种备份方式Snapshot和Backups,其中Snapshot目前还是免费的,但backup服务是要付费的。Snapshot创建的前提是先stop这个Droplet。建议导入blog、访问正常后,马上建立一个Droplet的Snapshot。

七、其它

由于是入门型VPS,其内存仅有512M,并且默认情况下Ubuntu 14.04 VPS没有创建Swap,考虑到VPS的高可用性,我们还是需要自己动手创建一些swap空间,以供不时之需,创建步骤很简单,执行下面命令即可:

fallocate -l 512M /swapfile
mkswap /swapfile
swapon /swapfile

swapon -s  查看一下当前swap,可以看到:
Filename                                Type            Size    Used    Priority
/swapfile                               file            524284  0       -1

另外调试过程中发现访问tonybai.com/feed出现如下错误:
Forbidden:
    You don't have permission to access /feed/ on this server.

Google、Baidu许久才发现真正问题所在:我的旧Blog目录下有一个feed子目录,把这个目录删除即可。

Goroutine是如何工作的

1 Comment

golangweekly的第36期Go Newsletter中我发现一篇短文"How Goroutines Work" ,其作者在参考了诸多资料后,简短概要地总结了一下 Goroutine的工作原理,感觉十分适合刚入门的Gophers(深入理解Goroutine调度的话,可以参考Daniel Morsing的" The Go scheduler" )。这里粗译如下。

一、Go语言简介

如果你是Go语言新手,或如果你对"并发(Concurrency)不是并行(parallelism)"这句话毫无赶脚,那么请看一下Rob Pike大神关于这个主题的演讲吧,演讲共30分 钟,我敢保证你在这个演讲上花费30分钟是绝对值得的。

总结一下两者(Concurrency和Parallelism)的不同:"当人们听到并发(Concurrency)这个词时,总是会想起并行 (Parallelism),它们之间有相关性,但却是两个明显不同的概念。在编程领域,并发(Concurrency)是独立的执行过程 (Process)的组合,而并行(Parallelism)则是计算(可能是相关联的)的同时执行。并发(Concurrency)是关于同时 应对很多事情(deal with lots of things),而并行(Parallelism)则是同时做许多事情(do lots of things)"。(Rob Pike的“Concurrency is not parallelism")

Go语言支持我们编写并发(Concurrent)的程序。它提供了Goroutine以及更重要的在Goroutines之间通信的能力。这里 我们将聚焦在前者(译注:指并发)。

二、Goroutines和Threads

Goroutine是一个简单的模型:它是一个函数,与其他Goroutines并发执行且共享相同地址空间。Goroutines的通常用法是根据需要创建尽可 能的Groutines,成百上千甚至上万的。这种用法对于那些习惯了使用C++或Java的程序员来讲可能会有些奇怪。创建这么多 goroutines势必要付出不菲的代价?一个操作系统线程使用固定大小的内存作为它的执行栈,当线程数增多时,线程间切换的代价也是相当的 高。这也是每处理一个request就创建一个新线程的服务程序方案被诟病的原因。

不过Goroutine完全不同。它们由Go运行时初始化并调度,操作系统根本看不到Goroutine的存在。所有的goroutines都是 活着的,并且以多路复用的形式运行于操作系统为应用程序分配的少数几个线程上。创建一个Goroutine并不需要太多内存,只需要8K的栈空间 (在Go 1.3中这个Size发生了变化)。它们根据需要在堆上分配和释放内存以实现自身的增长。

Go运行时负责调度Goroutines。Goroutines的调度是协作式的,而线程不是。这意味着每次一个线程发生切换,你都需要保存/恢 复所有寄存器,包括16个通用寄存器、PC(程序计数器)、SP(栈指针)、段寄存器(segment register)、16个XMM寄存器、FP协处理器状态、X AVX寄存器以及所有MSR等。而当另一个Goroutine被调度时,只需要保存/恢复三个寄存器,分别是PC、SP和DX。Go调度器和任何现代操作 系统的调度器都是O(1)复杂度的,这意味着增加线程/goroutines的数量不会增加切换时间,但改变寄存器的代价是不可忽视的。

由于Goroutines的调度是协作式的,一个持续循环的goroutine会导致运行于同一线程上的其他goroutines“饿死”。在 Go 1.2中,这个问题或多或少可以通过在进入函数前间或地调用Go调度器来缓解一些,因此一个包含非内联函数调用的循环是可以被调度器抢占的。

三、Goroutine阻塞

只要阻塞存在,它在OS线程中就是不受欢迎的,因为你拥有的线程数量很少。如果你发现大量线程阻塞在网络操作或是Sleep操作上,那就是问题, 需要修正。正如前面提到的那样,Goroutine是廉价的。更关键地是,如果它们在网络输入操作、Sleep操作、Channel操作或 sync包的原语操作上阻塞了,也不会导致承载其多路复用的线程阻塞。如果一个goroutine在上述某个操作上阻塞,Go运行时会调度另外一 个goroutine。即使成千上万的Goroutine被创建了出来,如果它们阻塞在上述的某个操作上,也不会浪费系统资源。从操作系统的视角来看,你的程序的行为就像是一个事件驱动的C程序似的。

四、最后的想法

就是这样,Goroutines可以并发的运行。不过和其他语言一样,组织两个或更多goroutine同时访问共享资源是很重要的。最好采用Channel在不同Goroutine间传递数据。

最后,虽然你无法直接控制Go运行时创建的线程的数量,但可以通过调用runtime.GOMAXPROCS(n)方法设置变量GOMAXPROCS来设 定使用的处理器核的数量。提高使用的处理器核数未必能提升你的程序的性能,这取决于程序的设计。程序剖析诊断工具(profiling tool)可以用来检查你的程序使用处理器核数的真实情况。

Go语言的有效错误处理

No Comments

中午闲暇翻看Daniel Morsing的“The Go scheduler”时,发现其另外一篇短文“Effective error handling in Go”,文章不长,但感觉对Go中错误处理方法总结的还是比较到位的,这里译之供大家参考。

一、简介

Go语言受到诟病最多的一项就是其错误处理机制。如果显式地检查和处理每个error,这恐怕的确会让人望而却步。你可以试试这里列出的几个方法,以避免你走入错误处理方法的误区当中去。

二、在缩进区处理错误

当使用Go语言编写代码时,首选下面这样的错误处理方法:

f, err := os.Open(path)
if err != nil {
    // handle error
}
// do stuff

而不是下面这样的:

f, err := os.Open(path)
if err == nil {
    // do stuff
}
// handle error

按照上面的方法处理错误,处理正常情况的代码读起来就显得通篇连贯了。

三、定义你自己的errors

做好如何正确进行错误处理的第一步就是要了解error是什么。如果你设计实现的包会因某种原因发生某种错误,你的包用户将会对错误的原因很感兴趣。为了满足用户的需求,你需要实现error接口,简单做起来就像这样:

type Error string
func (e Error) Error() string { return string(e) }

现在,你的包用户通过执行一个type assertion就可以知道是否是你的包导致了这个错误:

result, err := yourpackage.Foo()
if ype, ok := err.(yourpackage.Error); ok {
    // use ype to handle error
}

通过这个方法,你还可以向你的包用户暴露更多地结构化错误信息:

type ParseError struct {
    File  *File
    Error string
}

func (oe *ParseError) Error() string {//译注:原文中这里是OpenError
    // format error string here
}

func ParseFiles(files []*File) error {
    for _, f := range files {
        err := f.parse()
        if err != nil {
            return &ParseError{ //译注:原文中这里是OpenError
                File:  f,
                Error: err.Error(),
            }
        }
    }
}

通过这种方法,你的用户就可以明确地知道到底哪个文件出现解析错误了。(译注:从这里看到的go语言error设计之内涵,让我想起了Rob Pike大神的一篇Blog:"少即是级数级的多")

不过包装error时要小心,当你将一个error包装起来后,你可能会丢失一些信息:

var c net.Conn
f, err := DownloadFile(c, path)
switch e := err.(type) {
default:
    // this will get executed if err == nil
case net.Error:
    // close connection, not valid anymore
    c.Close()
    return e
case error:
    // if err is non-nil
    return err
}
// do other things.

如果你包装了net.Error,上面这段代码将无法知道是由于网络问题导致的失败,会继续使用这条无效的链接。

有一条经验规则:如果你的包中使用了一个外部interface,那么不要对这个接口中方法返回的任何错误,使用你的包的用户可能更关心这些错误,而不是你包装后的错误。

四、将错误作为状态

有时,当遇到一个错误时,你可能会停下来等等。这或是因为你将延迟报告错误,又或是因为你知道如果这次报告后,后续你会再报告同样的错误。

第一种情况的一个例子就是bufio包。当一个bufio.Reader遇到一个错误时,它将停下来保持这个状态,直到buffer已经被清空。只有在那时它才会报告错误。

第二种情况的一个例子是go/loader。当你通过某些参数调用它导致错误时,它会停下来保持这个状态,因为它知道你很可能会使用同样地参数再次调用它。

五、使用函数以避免重复代码

如果你有两段重复的错误处理代码,你可以将它们放到一个函数中去:

func handleError(c net.Conn, err error) {
    // repeated error handling
}

func DoStuff(c net.Conn) error {
    f, err := downloadFile(c, path)
    if err != nil {
        handleError(c, err)
        return err
    }

    f, err := doOtherThing(c)
    if err != nil {
        handleError(c, err)
        return err
    }
}

优化后的实现方法如下:

func handleError(c net.Conn, err error) {
    if err == nil {
        return
    }
    // repeated error handling
}

func DoStuff(c net.Conn) error {
    defer func() { handleError(c, err) }()
    f, err := downloadFile(c, path)
    if err != nil {
        return err
    }

    f, err := doOtherThing(c)
    if err != nil {
        return err
    }
}

这就是全部了。就Go语言错误处理而言,我知道的就这么多了。

Go,5周年

6 Comments

2014年11月10日(美国当地时间),Golang官方博客 放出了Andrew Gerrand的一篇博文《Half a decade with Go》来纪念Go语言发布五周年。文章按时间顺序简要描述了Golang这五年来发展的 点点滴滴,并让全世界Gopher看到了Go可期的光明未来。考虑到这篇文章在墙外,不便于国内Gopher阅读,这里给出中文翻译版,希望能给中国大陆 的Gophers带来些帮助!

五年前,我们启动了Go语言项目。我们准备发布第一版时的一幕仿佛就发生在昨天似的:我们的官方站点用的是一种可爱的黄色色调,我们将Go语言称为一门 “系统编程语言”,你需要使用分号作为语句结束标志,使用Makefile来构建你的代码。我们不知道Go语言是否能被大家接受。人们会分享我们的目标和 愿景吗?人们会发现Go语言有用吗?

起初,我们的发布引起了一阵关注。Google发布了一门新的编程语言,每个人都渴望探究它一番。一些程序员因为Go相对保守的功能特性集合而选择了放 弃,Go给他们的第一印象就是:没有什么新鲜玩意儿!但另外一小群程序员则看到了这个为软件工程师量身定做的生态系统的开端。这少数人将组成Go语言社区 的核心。

第一版发布后,我们花了些时间向社区传达Go语言背后的目标和设计理念。Rob Pike在官方的《Go at Google: Language Design in the Service of Software Engineering》一文中对此进行了生动地表达,并 在其个人博客文章《Less is exponentially more》中做了进一步的阐述。Andrew Gerrand的《Code that grows with grace》(Slides在这里)和《Go for Gophers》(Slides在这里)对Go的设计哲学又给出了更有深度和技术性的说明。

随着时间的推移,积少成多。这个项目的转折点出现在2012年3月Go 1发布时。Go 1为程序员们提供了可以信赖的稳定的语言和标准库。到2014年,Go项目拥有了上百的核心贡献者,其生态圈中拥有了数不尽的第三方库和工具 ,并由成千上万的开发者维护着。正在发展壮大的社区拥有许多极具热情的成员(或者就如我们所称呼 的:Gophers)。今天,就我们目前的统计分析,Go社区的成长速度远远超出了我们的预期。

Gophers们在哪里可以得到这些呢?全世界目前有很多有关Go语言的“大事”发生。今年我们看到了几个专门的Go技术大会:在丹佛和巴黎举行的首次 GopherCondotGo大 会。FOSDEM的Go DevRoom以及在东京举行的一年两次的GoCon。每次会上来自全球各地的Gophers们都踊跃地展示他们开发的Go项目。对于Go语言开发组来 说,我们很高兴能满足这些分享我们愿景和兴奋的程序员的需求。

在世界各地,还有数十个社区驱动运行的“Go用户组”。如果你还没有造访过你当地的用户组,可以考虑去尝试一下。如果你当地尚没有这类用户组,也许你可以考虑发起一个

今天,Go在云端找到了用武之地。Go出现在了工业向云计算转型的时刻。并且我们兴奋地看到Go正在快速成为这个运动的一个重要组成部分。简单、高效、内 置并发原语和现代的标准库让Go语言尤其适合云端软件开发(毕竟它就是为此而设计的)。一些重量级的开源云项目,诸如Docker和Kubernetes 都是用Go语言实现的,一些运作基础设置的公司,诸如Google、CloudFlare、Canonical、Digital Ocean、Github、Heroku以及微软也都在使用Go语言开发一些重量级的项目。

那么将来会怎样呢?我们认为2015年将是Go语言大爆发的一年。

Go 1.4,除了其新增的特性和bug修正外,它为实现一个新的低延迟垃圾收集器以及支 持在移动终端上运行Go奠定了基础。 预计Go1.4将在2014年12月1日正式发布。我们期望在Go 1.5中能出现新GC的身影,Go 1.5预计在2015年6月1日发布,它将使Go适合更加广泛的应用开发。我们迫不及待的想看到哪些领域的开发者会接受它。

接下来会有更多的Go大事发生。11月15日,GothamGo将在纽约如期举行。2014年1月31日到 2月1日,布鲁塞尔将举行另一次Go DevRoot at FOSDEM。2015年2月19日到21日,在印度班加罗尔将举行GopherCon India大会。最初的GopherCon将在2015年7月份回到丹佛。2015年11月 dotGo大会将再次来到巴黎。

Go团队将向届时到场的所有gophers表示衷心的感谢。为Go语言的下一个五年!

为了庆祝Go诞生5周年,在未来的一个月里,Gopher Academy将会发布一系列由知名Go users撰写的文章,务必要去看看哦。

Golang开发环境搭建-Vim篇

评论关闭

虽说sublimetext3+gosublime+gocode是目前较为 流行的Golang开发环境组合,但作为一名VIMer,没有一套得心应手的Vim for Golang dev心里总是过不去的。Golang虽然年轻,但即便是从Go 1版本发布(2012年3月28日)算起,掐指算来也有小三年了。全世界的开发者已经为Golang贡献了较为成熟的Vim插件了。有了这些插件,搭建出 一套高效的Golang开发环境还是不难的,网上也有大量的资料可以参考,其中就有vim-go作者自己发表的一篇文章《Go development environment for Vim》。不过看别人 写的与自己搭建体验的还是有大不同的,于是想来想去还是把整个过程记录下来。

一、一个干净的环境

找个干净的基础环境,方便确认每个搭建步骤后的效果:

Ubuntu 14.04 x86_64
vim version 7.4.52
go version go1.4beta1 linux/amd64

再准备一个编辑Go源码的测试源文件:

//hellogolang.go

package main

import "fmt"

func main() {
        fmt.Println("Hello Golang!")
}

用于验证每个搭建步骤后的变化。

二、严格按照vim-go的官方说明逐一搭建

Vim-go是当前使用最为广泛的用于搭建Golang开发环境的vim插件,这里我同样使用vim-go作为核心和基础进行环境搭建的。vim-go利 用开源Vim插件管理器安装,gmarik/Vundle.vim是目前被推荐次数更多的Vim插件管理器,超过了pathogen。这里我们 就用vundle来作为Vim的插件管理工具。

1、安装Vundle.vim

Vundle.vim的安装步骤如下:

mkdir ~/.vim/bundle
git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim   
                                                                  

创建~/.vimrc文件(如果你没有这个文件的话),在文件顶部添加有关Vundle.vim的配置:

set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

此时Vim仅安装了Vundle.vim这一个插件。编辑hellogolang.go时与编辑普通文本文件无异,一切都还是Vim的默认属性。

2、安装Vim-go

编辑~/.vimrc,在vundle#beginvundle#end间增加一行:

Plugin 'fatih/vim-go'

在Vim内执行 :P luginInstall

Vundle.vim会在左侧打开一个Vundle Installer Preview子窗口,窗口下方会提示:“Processing 'fatih/vim-go'”,待安装完毕后,提示信息变 成“Done!”。

这时,我们可以看到.vim/bundle下多了一个vim-go文件夹:

$ ls .vim/bundle/
vim-go/  Vundle.vim/

此时,再次编辑hellogolang.go,语法高亮有了, 保存时自动format(利用$GOBIN/gofmt)也有了,但其他高级功能,比如自动import缺失的 package、自动补齐仍然没有,我们还要继续安装一些东东。

3、安装go.tools Binaries

vim-go安装说明中提到所有必要的binary需要先安装好,比如gocode、godef、goimports等。

通过:GoInstallBinaries,这些vim-go依赖的二进制工具将会自动被下载,并被安装到$GOBIN下或$GOPATH/bin下。(这个工具需要依赖git或hg,需要提前安装到你的OS中。)

:GoInstallBinaries的执行是交互式的,你需要回车确认:

vim-go: gocode not found. Installing github.com/nsf/gocode to folder /home/tonybai/go/bin
vim-go: goimports not found. Installing code.google.com/p/go.tools/cmd/goimports to folder /home/tonybai/go/bin/
vim-go: godef not found. Installing code.google.com/p/rog-go/exp/cmd/godef to folder /home/tonybai/go/bin/
vim-go: oracle not found. Installing code.google.com/p/go.tools/cmd/oracle to folder /home/tonybai/go/bin/
vim-go: gorename not found. Installing code.google.com/p/go.tools/cmd/gorename to folder /home/tonybai/go/bin/
vim-go: golint not found. Installing github.com/golang/lint/golint to folder /home/tonybai/go/bin/

vim-go: errcheck not found. Installing github.com/kisielk/errcheck to folder /home/tonybai/go/bin/

不过这些代码多在code.google.com上托管,因此由于众所周知的原因,vim-go的自动安装很可能以失败告终,这样就需要你根据上 面日志中提到的各个工具的源码地址逐一去下载并本地安装。无法搭梯子的,可以通过http://gopm.io 下载相关包。

安装后,$GOBIN下的新增Binaries如下:
-rwxr-xr-x  1 tonybai tonybai  5735552 11??  7 11:03 errcheck*
-rwxr-xr-x  1 tonybai tonybai  9951008 11??  7 10:33 gocode*
-rwxr-xr-x  1 tonybai tonybai  5742800 11??  7 11:07 godef*
-rwxr-xr-x  1 tonybai tonybai  4994120 11??  7 11:00 goimports*
-rwxr-xr-x  1 tonybai tonybai  5750152 11??  7 11:03 golint*
-rwxr-xr-x  1 tonybai tonybai  6381832 11??  7 11:01 gorename*
-rwxr-xr-x  1 tonybai tonybai  2954392 11??  7 10:38 gotags*
-rwxr-xr-x  1 tonybai tonybai  9222856 11??  7 11:01 oracle*

安装好这些Binaries后,我们来看看哪些特性被支持了。

再次编辑hellogolang.go

         - 新起一行输入fmt.,然后ctrl+x, ctrl+o,Vim 会弹出补齐提示下拉框,不过并非实时跟随的那种补齐,这个补齐是由gocode提供的。
    – 输入一行代码:time.Sleep(time.Second),执行:GoImports,Vim会自动导入time包。
    – 将光标移到Sleep函数上,执行:GoDef或命令模式下敲入gd,Vim会打开$GOROOT/src/time/sleep.go中 的Sleep函数的定义。执行:b 1返回到hellogolang.go。
    – 执行:GoLint,运行golint在当前Go源文件上。
    – 执行:GoDoc,打开当前光标对应符号的Go文档。
    – 执行:GoVet,在当前目录下运行go vet在当前Go源文件上。
    – 执行:GoRun,编译运行当前main package。
    – 执行:GoBuild,编译当前包,这取决于你的源文件,GoBuild不产生结果文件。
    – 执行:GoInstall,安装当前包。
    – 执行:GoTest,测试你当前路径下地_test.go文件。
    – 执行:GoCoverage,创建一个测试覆盖结果文件,并打开浏览器展示当前包的情况。
    – 执行:GoErrCheck,检查当前包种可能的未捕获的errors。
    – 执行:GoFiles,显示当前包对应的源文件列表。
    – 执行:GoDeps,显示当前包的依赖包列表。
    – 执行:GoImplements,显示当前类型实现的interface列表。
    – 执行:GoRename [to],将当前光标下的符号替换为[to]。

三、其他插件

到目前为止,我们还有若干特性没能实现,重点是:

    – 实时跟随的代码补齐
    – Code Snippet support

1、安装YCM(Your Complete Me)

在~/.vimrc中添加一行:

Plugin 'Valloric/YouCompleteMe'

保存退出后,再打开~/.vimrc并执行 :P luginInstall

安装完后,下面的提示栏提示:

ycm_client_support.[so|pyd|dll] and ycm_core.[so|pyd|dll] not detected; you need to compile YCM before using it. Read the docs!

似乎YCM是用了C++编写的模块对性能进行优化了,于是需要手工编译YCM的support库。步骤如下:

sudo apt-get install build-essential cmake python-dev
cd ~/.vim/bundle/YouCompleteMe
./install.sh

构建(编译C++很慢,需要耐心的等一会)ok后,再打开hellogolang.go,逐字的实时补全功能就具备了!Cool!

2、安装 UltiSnips

Vim-go默认是用ultisnips引擎插件,但这个插件需要单独安装。

同样,我们利用vundle来安装它,在~/.vimrc中添加一行:

Plugin 'SirVer/ultisnips'

snippet和snippet引擎是分开的。ultisnips是引擎,vim-go的go snippet定义在这里

https://github.com/fatih/vim-go/blob/master/gosnippets/snippets/go.snip

编辑hellogolang.go,按照go.snip中的说明,我们输入func后敲击tab键,我们发现期待的:

func name(params) type {
       
}

并没有出现。反倒是YCM的下拉提示显示在那里让你选择。似乎是ultisnips和YCM的键组合冲突了。ultisnips官方说明也的确如 此。ultisnips默认是用Tab展开snippet的,而YCM中的Tab用来选择补齐项,我们可以通过设置来避免这些。

我们在.vimrc中添加如下setting:

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips setting
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

这样让YCM通过回车和向下的箭头来做list item正向选择,通过向上箭头做反向选择。通过ctrl+space来原地触发补齐提示。

而ultisnips则是用tab做snippet展开,ctrl+b正向切换占位符,ctrl+z反向切换占位符。

3、安装molokai theme

Molokai theme是TextMate的theme的vim port,看着截图挺不错的,于是也安装了一下。

    mkdir ~/.vim/colors
    下载或copy https://github.com /fatih/molokai/blob/master/colors/molokai.vim到~/.vim /colors目录下
    在.vimrc添加一行:colorscheme molokai

四、.vimrc

前面讲到了vim-go有许多命令,在:xx模式下执行多显不便,于是你可以定义一些Mappings,比如:

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

这样我们在命令模式下,输入<,>+<r>就是运行 当前main包,以此类推。

另外下面这个配置使得我们在save file时既可以格式化代码,又可以自动插入包导入语句(或删除不用的包导入语句)。

" vim-go settings
let g:go_fmt_command = "goimports"

到这里,我们的Vim Golang开发环境就基本搭建好了。snippet+实时补齐让你Coding如飞!

五、附录:.vimrc文件

下面是截至目前为止全量.vimrc文件的内容:

set nocompatible              " be iMproved, required
filetype off                  " required
colorscheme molokai

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'
Plugin 'fatih/vim-go'
Plugin 'Valloric/YouCompleteMe'

Plugin 'SirVer/ultisnips'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

" vim-go settings
let g:go_fmt_command = "goimports"

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['', '']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips settings
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

六、Mac OS X下Vim配置

1、MacVim替换

Mac OS X下的配置方法稍有不同,因为Mac下系统自带的Vim是7.3版本,YCM要求Vim 7.3.584+版本,因此我们需要安装MacVim以替代自带的Vim,目前MacVim最新版本是version 7.4.258,完全满足要求。在这里https://github.com/b4winckler/macvim/releases可以下载到最新的MacVim,下载后的MacVim可以通过如下步骤替换原Vim。

原Vim安装到/usr/bin/vim下。

MacVim解压后如下:

[tony@tonydeair ~/Downloads/MacVim-snapshot-73]$ls
MacVim.app/    README.txt    mvim*

我们执行以下步骤即可完成vim替换工作:

sudo mv /usr/bin/vim /usr/bin/vim.bak //备份一下原vim
cp mvim /usr/local/bin/
sudo ln -s /usr/local/bin/mvim /usr/bin/vim

2、插件安装和配置

按照上面Linux Vim的插件安装步骤和配置方法我们来配置MacVim,配置后,我们发现除了molokai的colorscheme没有生效外,其余插件工作均正常。而所有.go文件打开,均无molokai方案的颜色高亮,甚至连一般的颜色高亮都没有了。经过不断调试,发现了一个解决方法,在~/.vimrc中添加几行代码即可:

syntax on
au BufRead,BufNewFile *.go set filetype=go
colorscheme molokai

但这几行配置代码如果放在~/.vimrc的前面,则UltiSnips会无法工作,我将其移到~/.vimrc文件的末尾,这样就不存在冲突了(看来.vimrc的插件配置的先后顺序会对插件功能的正常使用有影响)。漂亮的molokai colorscheme也会展现出来!

Older Entries