WordPress迁移到Docker容器

No Comments

目前的Blog托管在同事的一个共享主机上,由于种种原因,这个主机即将无法使用,我只能再次迁移我的WordPress,不得不感叹:铁打的Wordpress,流水的主机啊!

这次迁移前,我仔细考量了一番,如何能让以后可能出现的Wordpress迁移最简化呢?虽然现在的迁移也不是特别复杂。我想到了近期研究的 Docker。目前很多国外的VPS都已经支持了Docker,我只需要在本地制作好Docker容器导出,再导入目标VPS的Docker中即可完成迁 移。在真正做迁移前,我打算在实验环境下测试一下。以下是将Wordpress迁移到Docker容器的测试过程。

一、容器准备

1、下载镜像

WordPress主要就是两个部分组成:wordpress程序 + mysql数据库。Docker官方registery提供了Wordpress和MySQL的image,我们可以直接pull使用。考虑到外站速度较 慢,这里我使用了国内镜像站点dockerpool.com提供的镜像了。

sudo docker pull dl.dockerpool.com:5000/wordpress:4.0.0
sudo docker pull dl.dockerpool.com:5000/mysql:5.6.20

考虑到使用phpmyadmin操作mysql数据的方便性,我又找了一个phpmyadmin的image:

sudo docker pull corbinu/docker-phpmyadmin

2、启动容器

mysql作为数据库数据存储镜像,在启动是会被wordpress和phpmyadmin link的,这样后两者在各自容器内才能顺利访问mysql服务和数据库。

按顺序启动容器(mysql为最先启动):
sudo docker run –name blogmysql -e MYSQL_ROOT_PASSWORD=root -d dl.dockerpool.com:5000/mysql:5.6.20
sudo docker run –name blogwordpress –link blogmysql:mysql -p 80:80 -d dl.dockerpool.com:5000/wordpress:4.0.0
sudo docker run –name blogphpmyadmin -e MYSQL_USERNAME=root –link blogmysql:mysql -p 8000:80 -d corbinu/docker-phpmyadmin

三个容器均可以顺利启动。mysql数据库的访问方式是root/root。wordpress默认为80端口,phpmyadmin用8000端口访问,你可以试试http://localhost:80和http://localhost:8000。

$ sudo docker ps
CONTAINER ID        IMAGE                                 COMMAND                CREATED             STATUS              PORTS                  NAMES
a30540120b5d        corbinu/docker-phpmyadmin:latest      "/bin/sh -c phpmyadm   5 seconds ago       Up 3 seconds        0.0.0.0:8000->80/tcp   blogphpmyadmin     
a88c456bf840        dl.dockerpool.com:5000/wordpress:4    "/entrypoint.sh apac   37 seconds ago      Up 35 seconds       0.0.0.0:80->80/tcp     blogwordpress      
1b6f84f428e3        dl.dockerpool.com:5000/mysql:5.6.20   "/entrypoint.sh mysq   45 seconds ago      Up 44 seconds       3306/tcp               blogmysql  

访问phpmyadmin,使用root/root登录后,我们可以看到主页上得数据库信息:

Database server

Server: 172.17.0.2 via TCP/IP
Server type: MySQL
Server version: 5.6.20 – MySQL Community Server (GPL)
Protocol version: 10
User: root@172.17.0.4
Server charset: UTF-8 Unicode (utf8)
Web server

nginx/1.7.1
Database client version: libmysql – 5.6.20
PHP extension: mysqli Documentation
phpMyAdmin

Version information: 4.2.7.1, latest stable version: 4.2.10.1

3、install wordpress

第一次通过http://locahost:80访问wordpress,便进入了wordpress安装流程,这个镜像中携带的版本是wordpress 4.0.0。

step1:
    选择安装语言:简体中文

step2:
    填写站点名称、用户名、密码、邮件等。

安装ok后,你就进入到了Wordpress 4.0.0的管理后台。
 

二、迁移

1、导出备份

目前的Blog使用的是DirectAdmin管理面板。通过DirectAdmin我们可以将当前的Wordpress站点整个下载,下载包中包含:

$ls
backup/        domains/

domains/tonybai.com/public_html下就是你的wordpress程序以及相关配置、插件等数据。
backup/xxx.sql就是数据库导出文件。

我们需要将这两部分恢复到Docker中。

2、数据表导入

我曾经试过通过WP-DB-Backup导出sql文件,再通过phpmyadmin导入的方法,但WP-DB-Backup导出的sql文件较大(>2M),无法满足phpmyadmin对导入文件的要求(<=2m),于是总是失败。之后决定通过mysql直接执行sql脚本导入。

首先通过phpmyadmin在mysql中建立数据库xx_db,编码:utf8_general_ci。

然后将上面的backup/xxx.sql拷贝到blogmysql容器中,比如就放在/root/下。然后通过nscenter进入到blogmysql容器中,切换到/usr/local/mysql/bin下,执行如下命令:

$ ./mysql -u root -p
输入root,进入mysql

在mysql下执行:
mysql > use xx_db
mysql > source /root/xxx.sql

如无意外,数据将顺利导入mysql的xx_db下。

3、wordpress导入

通过docker nsenter工具进入blogwordpress容器,进入/var/www目录下:

$ cp -r html html.bak
$ rm -fr html/*

再将上面提到的public_html下面的文件悉数copy到html下。

打开浏览器,输入http://localhost:80,会出现错误提示:"您在 wp-config.php 文件中提供的数据库用户名和密码可能不正确,或者无法连接到 localhost 上的数据库服务器"。

我们需要修改wp-config.php中的数据库连接信息。

define('DB_NAME', 'bigwhite_db');
define('DB_USER', 'root');
define('DB_PASSWORD', 'root');
define('DB_HOST', 'mysql');

再次刷新浏览器,依旧无法正确打开。重启blogwordpress容器后,wordpress终于可以打开了,但打开的wordpress管理页面连接 的数据库不对,我的文章并没有出现在文章列表中,这是一个新库。于是通过docker logs查看blogwordpress日志,进入blogwordpress查看wp-config.php的内容,我惊奇的发现:wp- config.php中的DB_NAME被改回'wordpress'了,而没有使用导入的"bigwhite_db"。

多次修改回bigwhite_db并重启容器后,这个值均被改为wordpress。无奈只好通过phpmyadmin删除wordpress库,重新创 建wordpress库并采用同样方法导入xx.sql,使得wordpress这个db与bigwhite_db拥有同样地内容。这回再打开 wordpress,一切尽在眼前。

迁移实验成功!这样我们将迁移后的容器导出,再导入你的支持docker的VPS中,无需任何其余操作即可完成真正的迁移。目前Digital Ocean已经支持了Docker,Aliyun据说也拥抱了Docker。

godep的一个“坑”

No Comments

很多人学习和使用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主页给出明确展示,即便这个“坑”多数人可能不会遇到。

VirtualBox虚拟机下Windows登录密码破解方法

No Comments

近两年虚拟机的发展给开发人员带来了极大便利,安装一个新环境,只需从别人那里copy一份虚拟机文件即可,分分钟搞定。我之前一直在Ubuntu下工 作,Windows偶尔使用,于是在Ubuntu VirtualBox下安装了一个Windows 7。今年将工作环境迁移到Mac Air下了,但偶尔也有Windows的使用需求,于是直接从我原来的Ubuntu下将Win7的Vdi文件Copy到Air上,便直接可以使用Win7 了,省去了重新安装Win7以及庞大的Office组件的工作。

前两天,打开Mac Air下的VirtualBox,启动Win7虚拟机,在Win7登录界面输入密码后,系统提示我密码错误。反复输入多次,将我常用的密码都试了一遍依旧 无法进入。我只能在原Ubuntu下临时用用Win7。但毕竟在Air上没有Win7十分不便,一些Word, PPT文档需要在两天机器上传来传去。无奈下,我都由了重新在Air下安装一个Win7甚至是Win8的打算了。

今天又有一个PPT编写的task,这件事再次被提上日程。我换了下思维:能不能破解一下Win7登录密码呢?于是求助度娘(谷哥离去好久了)。还别说, 还真是有破解方法,多数是通过PE工具盘快速修改登录密码。但PE工具盘挺大(几百兆,公司下载不便),我的又是虚拟机环境,这种方法不是我的菜啊。于是 又看到另外一种思路:通过某个Linux livecd或安装盘引导,mount windows分区,将C:\Windows\System32\cmd.exe改名为osk.exe。osk.exe是虚拟键盘程序。在Win7登录页 面的左下角可以启动这个虚拟键盘程序。一旦我替换成功,启动虚拟键盘程序就变成了启动Win7命令行程序。有了命令行,我们就可以通过net user命令查看当前账户列表、重置某个用户的password了。思路很清晰,是我的菜。

在我的Ubuntu机器上倒是有几个Linux发行版的live cd iso文件,比如ubuntu 14.04.1 desktop, centos7 desktop,不过个头都太大了,传到我的Air上还是很费劲的。我想最好有一个tiny的linux发行版。度娘告诉我有很多选择。我首先选了Tiny Core Linux, TinyCore-5.4.iso才不到14M。于是打开Win7虚拟机的“设置”页面,将 TinyCore-5.4.iso作为虚拟iso“插入”IDE光驱。TinyCore的启动就是秒秒的事情。TinyCore的桌面风格模仿Mac OS,桌面下方放置了一个dock条。TinyCore自带mount tools,打开后,用鼠标点击sda2,sda2盘符由红变绿,说明mount成功。

打开TinyCore的Terminal程序,进入/mnt/sda2,本想安装方案修改cmd.exe的名字,但Tiny Core提示:这是个Read-Only Filesystem。显然这是个只读mount。于是各种尝试读写挂在(包括修改/etc/fstab、mount -a, mount -o remount等),都无法改变Read-Only Filesystem的事实,于是放弃。

换国人的发行版:CDLinux,这个发行版似乎已经不再更新,最新版本 CDlinux_mini-0.9.7.1.iso,发布时间是2012年3月18日。CDLinux的Size比TinyLinux稍大些,36M。 CDLinux启动略慢,并且只是Console Only(标准版带有桌面环境),没有图形桌面。进入命令行后,执行一下mount命令,发现/dev/sda2居然是rw方式挂载载/media /xxx下的,于是进入该目录,尝试touch test.txt,完全没有问题。

于是按照方案说明,将C:\Windows\System32下的osk.exe备份一下,将cmd.exe改名为osk.exe。

将光盘盘片删除,启动Win7,进入登陆页面时,点击左下角“轻松访问”按钮,选择“不使用键盘键入”,确定。命令行窗口弹出。

在命令行窗口执行net user 查看用户名列表。我的用户名是tonybai,再通过net user tonybai newpassword重置tonybai的密码。执行成功后,用新密码登陆,顺利进入Win7 Desktop。Crack成功!

Golang的演化历程

No Comments

本文来自Google的Golang语言设计者之一Rob Pike大神在GopherCon2014大会上的开幕主题演讲资料“Hello, Gophers!”。Rob大神在这次分 享中用了两个生动的例子讲述了Golang的演化历程,总结了Golang到目前为止的成功因素,值得广大Golang Programmer & Beginner学习和了解。这里也用了"Golang的演化历程"作为标题。

1、Hello Gophers!

package main

import "fmt"

func main() {
    fmt.Printf("Hello, gophers!\n")
}

Rob大神的见面礼,后续会有针对这段的演化历史的陈述。

2、历史

这是一个历史性的时刻。

Golang已经获得了一定的成功,值得拥有属于自己的技术大会。

3、成功

促成这份成功的因素有许多:

    – 功能
    – 缺少的功能
    – 功能的组合
    – 设计   
    – 人
    – 时间

4、案例学习:两段程序

我们来近距离回顾两段程序。

第一个是你见过的第一个Go程序,是属于你的历史时刻。
第二个是我们见过的第一个Go程序,是属于全世界所有Gophers的历史时刻。

先看第一个“hello, world”

5、hello.b

main( ) {
    extrn a, b, c;
    putchar(a); putchar(b); putchar(c); putchar('!*n');
}
a 'hell';
b 'o, w';
c 'orld';

上面这段代码首先出现在1972年Brian W. Kernighan的B语言教程中(也有另外一说是出现在那之前的BCPL语言中)。

6、hello.c

main()
{
    printf("hello, world");
}

上面这段代码出现在1974年Brian W. Kernighan编写的《Programming in C: A Tutorial》中。这份教程当时是作为Unix v5文档的一部分。

7、hello.c

main()
{
    printf("hello, world\n"); //译注:与上面的hello.c相比,多了个换行符\n输出
}

这段代码首次出现在1978年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》一书中。

8、hello.c, 标准C草案

#include <stdio.h> //译注:与上面hello.c相比, 多了这个头文件包含

main()
{
    printf("hello, world\n");
}

这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》第二版一书中,基于标准C草案。

9、hello.c,标准C89

#include <stdio.h>

main(void) //译注:与上面hello.c相比,多了个void
{
    printf("hello, world\n");
}

这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》第二版第二次修订中。

10、一两代之后…

(省略所有中间语言)

关于Golang的讨论开始于2007年年末。

第一版语言规范起草于2008年3月份。

用于实验和原型目的的编译器开发工作已经展开。

最初的编译器输出的是C代码。

语言规范一形成,我们就重写了编译器,输出本地代码(机器码)。

11、hello.go, 2008年6月6日

package main

func main() int {
    print "hello, world\n";
    return 0;
}

针对首次提交代码的测试。

内置的print已经是当时的全部实现。main函数返回一个int类型值。
注意:print后面没有括号。

12、hello.go,2008年6月27日

package main

func main() {
    print "hello, world\n";
}

当main函数返回,程序调用exit(0)。

13、hello.go,2008年8月11日

package main

func main() {
    print("hello, world\n");
}

print调用加上了括号,这时print是一个函数,不再是一个原语。

14、hello.go,2008年10月24日

package main

import "fmt"

func main() {
    fmt.printf("hello, world\n");
}

我们熟知并喜欢的printf来了。

15、hello.go,2009年1月15日

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n");
}

头母大写的函数名用作才是导出的符号。

16、hello.go, 2009年12约11日

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

不再需要分号。

这是在2009年11月10日Golang开发发布后的一次重要改变。

这也是当前版本的hello, world

我们花了些时间到达这里(32年!)

都是历史了!

17、不仅仅有C

我们从"C"开始,但Go与C相比有着巨大的不同。

其他一些语言影响和贯穿于Go的设计当中。

    C: 语句和表达式语法
    Pascal: 声明语法
    Modula 2, Oberon 2:包
    CSP, Occam, Newsqueak, Limbo, Alef:  并发
    BCPL: 分号规则
    Smalltalk: 方法(method)
    Newsqueak: <-, :=
    APL: iota

   
等等。也有一些是全新发明的,例如defer、常量。

还有一些来自其他语言的优点和缺点:
    C++, C#, Java, JavaScript, LISP, Python, Scala, …

18、hello.go,Go 1版

将我们带到了今天。

package main

import "fmt"

func main() {
    fmt.Println("Hello, Gophers (some of whom know 中文)!")
}

我们来深入挖掘一下,把这段代码做一个拆解。

19、Hello, World的16个tokens

package
main
import
"fmt"
func
main
(
)
{
fmt
.
Println
(
"Hello, Gophers (some of whom know 中文)!"
)
}

20、package

早期设计讨论的主要话题:扩展性的关键

package是什么?来自Modula-2等语言的idea

为什么是package?

    – 拥有编译构建所需的全部信息
    – 没有循环依赖(import)
    – 没有子包
    – 包名与包路径分离
    – 包级别可见性,而不是类型级别
    – 在包内部,你拥有整个语言,在包外部,你只拥有包许可的东西。

21、main

一个C语言遗留风范尽显之处
最初是Main,原因记不得了。
主要的包,main函数
很特别,因为它是初始化树(initialization tree)的根(root)。

22、import

一种加载包的机制
通过编译器实现(有别于文本预处理器。译注:C语言的include是通过preprocessor实现的)
努力使其高效且线性
导入的一个包,而不是一个标识符(identifiers)集合(译注:C语言的include是将头文件里的标识符集合引入)
至于export,它曾经是一个关键字。

23、"fmt"

包路径(package path)只是一个字符串,并非标识符的列表。
让语言避免定义它的含义 – 适应性。(Allows the language to avoid defining what it means—adaptability)
从一开始就想要一个URL作为一个选项。(译注:类似import "github.com/go/tools/xxx)
可以应付将来的发展。

24、func

一个关键字,用于引入函数(类型、变量、常量),易于编译器解析。
对于函数字面量(闭包)而言,易于解析非常重要。
顺便说一下,最初这个关键字不是func,而是function。

小插曲:

Mail thread from February 6, 2008
From: Ken Thompson <ken@google.com>
To: gri, r
larry and sergey came by tonight. we 
talked about go for more than an hour. 
they both said they liked it very much.
p.s. one of larrys comments was "why isnt function spelled func?"

From: Rob Pike <r@google.com>

To: ken, gri
fine with me. seems compatible with 'var'.
anyway we can always say, "larry said to call it 'func'"

25、main

程序执行的起点。除非它不是。(译注:main不是起点,rob大神的意思是不是指下列情形,比如go test测试包,在google app engine上的go程序不需要main)
将初始化与正常执行分离,早期计划之中的。
初始化在哪里发生的?(译注:说的是package内的func init() {..}函数吧)
回到包设计。(译注:重温golang的package设计思想)

26、()

看看,没有void
main没有返回值,由运行时来处理main的返回后的事情。
没有函数参数(命令行选项通过os包获取)
没有返回值

返回值以及语法

27、{

用的是大括号,而不是空格(译注:估计是与python的空格缩进对比)
同样也不是方括号。
为什么在括号后放置换行符(newline)?

28、fmt

所有导入的标识符均限定于其导入的包。(All imported identifiers are qualified by their import.)
每个标识符要么是包或函数的本地变量,要么被类型或导入包限定。
对代码可读性的重大影响。

为什么是fmt,而不是format?

29、.

句号token在Go中有多少使用?(很多)
a.B的含义需要使用到类型系统
但这对于人类来说非常清晰,读起来也非常容易。
针对指针的自动转换(没有->)。

30、Println

Println,不是println,头母大写才是导出符号。
知道它是反射驱动的(reflection-driven)
可变参数函数
参数类型是(…); 2010年2月1日变成(…interface{})

31、(

传统函数语法

32、"Hello, Gophers (some of whom know 中文)!"

UTF-8编码的源码输入。字符串字面量也自动是utf8编码格式的。

但什么是字符串(string)呢?

首批写入规范的语法规则,今天很难改变了。(blog.golang.org/strings)

33、)

没有分号
在go发布后不久我们就去除了分号
早期曾胡闹地尝试将它们(译注:指得是括号)去掉
最终接受了BCPL的方案

34、}

第一轮结束。

旁白:还没有讨论到的

    – 类型
    – 常量
    – 方法
    – interface
    – 库
    – 内存管理
    – 并发(接下来将讨论)
   
外加工具,生态系统,社区等。
语言是核心,但也只是我们故事的一部分。

35、成功

要素:
    – 站在巨人的肩膀上(building on history)
    – 经验之作(building on experience) 译注:最初的三个神级语言设计者
    – 设计过程
    – 早期idea提炼到最终的方案中
    – 由一个小团队专门集中精力做
   
最终:承诺
    Go 1.0锁定了语言核心与标准库。

36、另一轮

让我们看第二个程序的类似演化过程。

37、问题:素数筛(Prime sieve)

问题来自于Communicating Sequential Processes, by C. A. R. Hoare, 1978。

“问题:以升序打印所有小于10000的素数。使用一个process数组:SIEVE,其中每个process从其前驱元素输入一个素数并打印它。接下 来这个process从其前驱元素接收到一个升序数字流并将它们传给其后继元素,这个过程会剔除掉所有是最初素数整数倍的数字。

38、解决方案

在1978年的CSP论文中。(注意不是Eratosthenes筛)

这个优美的方案是由David Gries贡献出来的。

39、CSP

在Hoare的CSP论文中:

[SIEVE(i:1..100)::
    p,mp:integer;
    SIEVE(i - 1)?p;
    print!p;
    mp := p; comment mp is a multiple of p;
*[m:integer; SIEVE(i - 1)?m →
    *[m > mp → mp := mp + p];
    [m = mp → skip
    ||m < mp → SIEVE(i + 1)!m
]   ]
||SIEVE(0)::print!2; n:integer; n := 3;
    *[n < 10000 → SIEVE(1)!n; n := n + 2]
||SIEVE(101)::*[n:integer;SIEVE(100)?n → print!n]
||print::*[(i:0..101) n:integer; SIEVE(i)?n → ...]
]

没有channel。能处理的素数的个数是在程序中指定的。

40、Newsqueak

circa 1988。

Rob Pike语言设计,Tom Cargill和Doug McIlroy实现。

使用了channels,这样个数是可编程的。(channel这个idea从何而来?)

counter:=prog(end: int, c: chan of int)
{
    i:int;
    for(i=2; i<end; i++)
        c<-=i;
};

filter:=prog(prime: int, listen: chan of int, send: chan of int)
{
    i:int;
    for(;;)
        if((i=<-listen)%prime)
            send<-=i;
};

sieve:=prog(c: chan of int)
{
    for(;;){
        prime:=<-c;
        print(prime, " ");
        newc:=mk(chan of int);
        begin filter(prime, c, newc);
        c=newc;
    }
};

count:=mk(chan of int);

begin counter(10000, count);
begin sieve(count);
"";

41、sieve.go,2008年3月5日

使用go规范编写的第一个版本,可能是第二个由go编写的重要程序。

>用于发送;<用于接收。Channel是指针。Main是头字母大写的。

package Main

// Send the sequence 2, 3, 4, … to channel 'ch'.
func Generate(ch *chan> int) {
    for i := 2; ; i++ {
        >ch = i;    // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in *chan< int, out *chan> int, prime int) {
    for ; ; {
        i := <in;    // Receive value of new variable 'i' from 'in'.
        if i % prime != 0 {
            >out = i;    // Send 'i' to channel 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes together.
func Sieve() {
    ch := new(chan int);  // Create a new channel.
    go Generate(ch);      // Start Generate() as a subprocess.
    for ; ; {
        prime := <ch;
        printf("%d\n", prime);
        ch1 := new(chan int);
        go Filter(ch, ch1, prime);
        ch = ch1;
    }
}

func Main() {
    Sieve();
}

42. sieve.go,2008年7月22日

-<用于发送;-<用于接收。Channel仍然是指针。但现在main不是大写字母开头的了。

package main

// Send the sequence 2, 3, 4, … to channel 'ch'.
func Generate(ch *chan-< int) {
    for i := 2; ; i++ {
        ch -< i    // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in *chan<- int, out *chan-< int, prime int) {
    for {
        i := <-in;    // Receive value of new variable 'i' from 'in'.
        if i % prime != 0 {
            out -< i    // Send 'i' to channel 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes together.
func Sieve() {
    ch := new(chan int);  // Create a new channel.
    go Generate(ch);      // Start Generate() as a subprocess.
    for {
        prime := <-ch;
        printf("%d\n",    prime);
        ch1 := new(chan int);
        go Filter(ch, ch1, prime);
        ch = ch1
    }
}

func main() {
    Sieve()
}

43、sieve.go,2008年9月17日

通信操作符现在是<-。channel仍然是指针。

package main

// Send the sequence 2, 3, 4, … to channel 'ch'.
func Generate(ch *chan <- int) {
    for i := 2; ; i++ {
        ch <- i  // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in *chan <- int, out *<-chan int, prime int) {
    for {
        i := <-in;  // Receive value of new variable 'i' from 'in'.
        if i % prime != 0 {
            out <- i  // Send 'i' to channel 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes together.
func Sieve() {
    ch := new(chan int);  // Create a new channel.
    go Generate(ch);      // Start Generate() as a subprocess.
    for {
        prime := <-ch;
        print(prime, "\n");
        ch1 := new(chan int);
        go Filter(ch, ch1, prime);
        ch = ch1
    }
}

func main() {
    Sieve()
}

44、sieve.go,2009年1月6日

引入了make内置操作符。没有指针。编码错误!(有个*被留下了,错误的参数类型)

package main

// Send the sequence 2, 3, 4, … to channel 'ch'.
func Generate(ch chan <- int) {
    for i := 2; ; i++ {
        ch <- i  // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func Filter(in chan <- int, out *<-chan int, prime int) {
    for {
        i := <-in;  // Receive value of new variable 'i' from 'in'.
        if i % prime != 0 {
            out <- i  // Send 'i' to channel 'out'.
        }
    }
}

// The prime sieve: Daisy-chain Filter processes together.
func Sieve() {
    ch := make(chan int);  // Create a new channel.
    go Generate(ch);       // Start Generate() as a subprocess.
    for {
        prime := <-ch;
        print(prime, "\n");
        ch1 := make(chan int);
        go Filter(ch, ch1, prime);
        ch = ch1
    }
}

func main() {
    Sieve()
}

45、sieve.go,2009年9月25日

第一个正确的现代版本。同样,大写头母不见了,使用了fmt。

package main

import "fmt"

// Send the sequence 2, 3, 4, … to channel 'ch'.
func generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i;    // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func filter(src <-chan int, dst chan<- int, prime int) {
    for i := range src {    // Loop over values received from 'src'.
        if i%prime != 0 {
            dst <- i;    // Send 'i' to channel 'dst'.
        }
    }
}

// The prime sieve: Daisy-chain filter processes together.
func sieve() {
    ch := make(chan int);  // Create a new channel.
    go generate(ch);       // Start generate() as a subprocess.
    for {
        prime := <-ch;
        fmt.Print(prime, "\n");
        ch1 := make(chan int);
        go filter(ch, ch1, prime);
        ch = ch1;
    }
}

func main() {
    sieve();
}

46、sieve.go,2009年12月10日

分号不见了。程序已经与现在一致了。

package main

import "fmt"

// Send the sequence 2, 3, 4, … to channel 'ch'.
func generate(ch chan<- int) {
    for i := 2; ; i++ {
        ch <- i  // Send 'i' to channel 'ch'.
    }
}

// Copy the values from channel 'src' to channel 'dst',
// removing those divisible by 'prime'.
func filter(src <-chan int, dst chan<- int, prime int) {
    for i := range src {  // Loop over values received from 'src'.
        if i%prime != 0 {
            dst <- i  // Send 'i' to channel 'dst'.
        }
    }
}

// The prime sieve: Daisy-chain filter processes together.
func sieve() {
    ch := make(chan int)  // Create a new channel.
    go generate(ch)       // Start generate() as a subprocess.
    for {
        prime := <-ch
        fmt.Print(prime, "\n")
        ch1 := make(chan int)
        go filter(ch, ch1, prime)
        ch = ch1
    }
}

func main() {
    sieve()
}

这个优美的方案来自于几十年的设计过程。

47、旁边,没有讨论到的

select

真实并发程序的核心连接器(connector)
最初起源于Dijkstra的守卫命令(guarded command)
在Hoare的CSP理论实现真正并发。
经过Newsqueak、Alef、Limbo和其他语言改良后

2008年3月26日出现在Go版本中。

简单,澄清,语法方面的考虑。

48、稳定性

Sieve程序自从2009年末就再未改变过。– 稳定!

开源系统并不总是兼容和稳定的。

但,Go是。(兼容和稳定的)

这是Go成功的一个重要原因。

49、趋势

图数据展示了Go 1.0发布后Go语言的爆发。

50、成功

Go成功的元素:

    显然的:功能和工具。

    * 并发
    * 垃圾回收
    * 高效的实现
    * 给人以动态类型体验的静态类型系统
    * 丰富但规模有限的标准库
    * 工具化
    * gofmt
    * 在大规模系统中的应用

    不那么显然的:过程

    * 始终聚焦最初的目标
    * 在冻结后的集中开发
    * 小核心团队易于取得一致
    * 社区的重要贡献
    * 丰富的生态系统
   
总之,开源社区共享了我们的使命,聚焦于为当今的世界设计一门语言。

Golang测试技术

No Comments

本篇文章内容来源于Golang核心开发组成员Andrew Gerrand在Google I/O 2014的一次主题分享“Testing Techniques”,即介绍使用Golang开发 时会使用到的测试技术(主要针对单元测试),包括基本技术、高级技术(并发测试、mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等)等,感觉总结的很全面,这里整理记录下来,希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下:Golang官方站的slide都是以一种特有的golang artical的格式放出的(用这个工具http://go-talks.appspot.com/可以在线观看),没法像pdf那样下载,在国内使用和传播极其不便。

一、基础测试技术

1、测试Go代码

Go语言内置测试框架。

内置的测试框架通过testing包以及go test命令来提供测试功能。

下面是一个完整的测试strings.Index函数的完整测试文件:

//strings_test.go (这里样例代码放入strings_test.go文件中)
package strings_test

import (
    "strings"
    "testing"
)

func TestIndex(t *testing.T) {
    const s, sep, want = "chicken", "ken", 4
    got := strings.Index(s, sep)
    if got != want {
        t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)//注意原slide中的got和want写反了
    }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

go test的-v选项是表示输出详细的执行信息。

将代码中的want常量值修改为3,我们制造一个无法通过的测试:

$go test -v strings_test.go
=== RUN TestIndex
— FAIL: TestIndex (0.00 seconds)
    strings_test.go:12: Index("chicken","ken") = 4; want 3
FAIL
exit status 1
FAIL    command-line-arguments    0.008s

2、表驱动测试

Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。

package strings_test

import (
        "strings"
        "testing"
)

func TestIndex(t *testing.T) {
        var tests = []struct {
                s   string
                sep string
                out int
        }{
                {"", "", 0},
                {"", "a", -1},
                {"fo", "foo", -1},
                {"foo", "foo", 0},
                {"oofofoofooo", "f", 2},
                // etc
        }
        for _, test := range tests {
                actual := strings.Index(test.s, test.sep)
                if actual != test.out {
                        t.Errorf("Index(%q,%q) = %v; want %v",
                             test.s, test.sep, actual, test.out)
                }
        }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

3、T结构

*testing.T参数用于错误报告:

t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)

也可以用于enable并行测试(parallet test):
t.Parallel()

控制一个测试是否运行:

if runtime.GOARCH == "arm" {
    t.Skip("this doesn't work on ARM")
}

4、运行测试

我们用go test命令来运行特定包的测试。

默认执行当前路径下包的测试代码。

$ go test
PASS

$ go test -v
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS

要运行工程下的所有测试,我们执行如下命令:

$ go test github.com/nf/…

标准库的测试:
$ go test std

注:假设strings_test.go的当前目录为testgo,在testgo目录下执行go test都是OK的。但如果我们切换到testgo的上一级目录执行go test,我们会得到什么结果呢?

$go test testgo
can't load package: package testgo: cannot find package "testgo" in any of:
    /usr/local/go/src/pkg/testgo (from $GOROOT)
    /Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)

提示找不到testgo这个包,go test后面接着的应该是一个包名,go test会在GOROOT和GOPATH下查找这个包并执行包的测试。

5、测试覆盖率

go tool命令可以报告测试覆盖率统计。

我们在testgo下执行go test -cover,结果如下:

go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo
FAIL    _/Users/tony/Test/Go/testgo [build failed]

显然通过cover参数选项计算测试覆盖率不仅需要测试代码,还要有被测对象(一般是函数)的源码文件。

我们将目录切换到$GOROOT/src/pkg/strings下,执行go test -cover

$go test -v -cover
=== RUN TestReader
— PASS: TestReader (0.00 seconds)
… …
=== RUN: ExampleTrimPrefix
— PASS: ExampleTrimPrefix (1.75us)
PASS
coverage: 96.9% of statements
ok      strings    0.612s

go test可以生成覆盖率的profile文件,这个文件可以被go tool cover工具解析。

在$GOROOT/src/pkg/strings下面执行:

$ go test -coverprofile=cover.out

会再当前目录下生成cover.out文件。

查看cover.out文件,有两种方法:

a) cover -func=cover.out

$sudo go tool cover -func=cover.out
strings/reader.go:24:    Len                66.7%
strings/reader.go:31:    Read                100.0%
strings/reader.go:44:    ReadAt                100.0%
strings/reader.go:59:    ReadByte            100.0%
strings/reader.go:69:    UnreadByte            100.0%
… …
strings/strings.go:638:    Replace                100.0%
strings/strings.go:674:    EqualFold            100.0%
total:            (statements)            96.9%

b) 可视化查看

执行go tool cover -html=cover.out命令,会在/tmp目录下生成目录coverxxxxxxx,比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html,即可以可视化的查看代码的测试覆盖情况。

 

关于go tool的cover命令,我的go version go1.3 darwin/amd64默认并不自带,需要通过go get下载。

$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover

下载后,cover安装在$GOROOT/pkg/tool/darwin_amd64下面。

二、高级测试技术

1、一个例子程序

outyet是一个web服务,用于宣告某个特定Go版本是否已经打标签发布了。其获取方法:

go get github.com/golang/example/outyet

注:
go get执行后,cd $GOPATH/src/github.com/golang/example/outyet下,执行go run main.go。然后用浏览器打开http://localhost:8080即可访问该Web服务了。

2、测试Http客户端和服务端

net/http/httptest包提供了许多帮助函数,用于测试那些发送或处理Http请求的代码。

3、httptest.Server

httptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。

type Server struct {
    URL      string // base URL of form http://ipaddr:port with no trailing slash
    Listener net.Listener

    // TLS is the optional TLS configuration, populated with a new config
    // after TLS is started. If set on an unstarted server before StartTLS
    // is called, existing fields are copied into the new config.
    TLS *tls.Config

    // Config may be changed after calling NewUnstartedServer and
    // before Start or StartTLS.
    Config *http.Server
}

func NewServer(handler http.Handler) *Server

func (*Server) Close() error

4、httptest.Server实战

下面代码创建了一个临时Http Server,返回简单的Hello应答:

    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        log.Fatal(err)
    }

    greeting, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", greeting)

5、httptest.ResponseRecorder

httptest.ResponseRecorder是http.ResponseWriter的一个实现,用来记录变化,用在测试的后续检视中。

type ResponseRecorder struct {
    Code      int           // the HTTP response code from WriteHeader
    HeaderMap http.Header   // the HTTP response headers
    Body      *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
    Flushed   bool
}

6、httptest.ResponseRecorder实战

向一个HTTP handler中传入一个ResponseRecorder,通过它我们可以来检视生成的应答。

    handler := func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "something failed", http.StatusInternalServerError)
    }

    req, err := http.NewRequest("GET", "http://example.com/foo", nil)
    if err != nil {
        log.Fatal(err)
    }

    w := httptest.NewRecorder()
    handler(w, req)

    fmt.Printf("%d – %s", w.Code, w.Body.String())

7、竞争检测(race detection)

当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。

为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。

通过传入-race选项,go tool就可以启动竞争检测。

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

注:一个数据竞争检测的例子

例子代码:

//testrace.go

package main

import "fmt"
import "time"

func main() {
        var i int = 0
        go func() {
                for {
                        i++
                        fmt.Println("subroutine: i = ", i)
                        time.Sleep(1 * time.Second)
                }
        }()

        for {
                i++
                fmt.Println("mainroutine: i = ", i)
                time.Sleep(1 * time.Second)
        }
}

$go run -race testrace.go
mainroutine: i =  1
==================
WARNING: DATA RACE
Read by goroutine 5:
  main.func·001()
      /Users/tony/Test/Go/testrace.go:10 +0×49

Previous write by main goroutine:
  main.main()
      /Users/tony/Test/Go/testrace.go:17 +0xd5

Goroutine 5 (running) created at:
  main.main()
      /Users/tony/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i =  2
mainroutine: i =  3
subroutine: i =  4
mainroutine: i =  5
subroutine: i =  6
mainroutine: i =  7
subroutine: i =  8

8、测试并发(testing with concurrency)

当测试并发代码时,总会有一种使用sleep的冲动。大多时间里,使用sleep既简单又有效。

但大多数时间不是”总是“。

我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。

9、使用静态分析工具vet查找错误

vet工具用于检测代码中程序员犯的常见错误:
    – 错误的printf格式
    – 错误的构建tag
    – 在闭包中使用错误的range循环变量
    – 无用的赋值操作
    – 无法到达的代码
    – 错误使用mutex
    等等。

使用方法:
    go vet [package]

10、从内部测试

golang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。

注:比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。

11、从外部测试

有些时候,你需要从被测包的外部对被测包进行测试,比如测试代码在package foo_test下,而不是在package foo下。

这样可以打破依赖循环,比如:

    – testing包使用fmt
    – fmt包的测试代码还必须导入testing包
    – 于是,fmt包的测试代码放在fmt_test包下,这样既可以导入testing包,也可以同时导入fmt包。

12、Mocks和fakes

通过在代码中使用interface,Go可以避免使用mock和fake测试机制。

例如,如果你正在编写一个文件格式解析器,不要这样设计函数:

func Parser(f *os.File) error

作为替代,你可以编写一个接受interface类型的函数:

func Parser(r io.Reader) error

bytes.Buffer、strings.Reader一样,*os.File也实现了io.Reader接口。

13、子进程测试

有些时候,你需要测试的是一个进程的行为,而不仅仅是一个函数。例如:

func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

为了测试上面的代码,我们将测试程序本身作为一个子进程进行测试:

func TestCrasher(t *testing.T) {
    if os.Getenv("BE_CRASHER") == "1" {
        Crasher()
        return
    }
    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
    cmd.Env = append(os.Environ(), "BE_CRASHER=1")
    err := cmd.Run()
    if e, ok := err.(*exec.ExitError); ok && !e.Success() {
        return
    }
    t.Fatalf("process ran with err %v, want exit status 1", err)
}

组织Golang代码

No Comments

本月初golang官方blog(需要自己搭梯子)上发布了一篇文章,简要介绍了近几个月Go在一 些技术会议上(比如Google I/O、Gopher SummerFest等)的主题分享并伴有slide链接。其中David Crawshaw的“Organizing Go Code”对Golang的代码风格以及工程组 织的最佳实践进行的总结很是全面和到位,这里按Slide中的思路和内容翻译和摘录如下(部分伴有我个人的若干理解)。

一、包 (Packages)

1、Golang程序由package组成

所有Go源码都是包得一部分。

每个Go源文件都起始于一条package语句。

Go应用程序的执行起始于main包。

package main

import "fmt"

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

对小微型程序而言,你可能只需要编写main包内的源码。

上面的HelloWorld程序import了fmt包。

函数Println定义在fmt包中。

2、一个例子:fmt包

// Package fmt implements formatted I/O.
package fmt

// Println formats using the default formats for its
// operands and writes to standard output.
func Println(a …interface{}) (n int, err error) {
    …
}

func newPrinter() *pp {
    …
}

Println是一个导出(exported)函数,它的函数名以大写字母开头,这意味着它允许其他包中的函数调用它。

newPrinter函数则并非导出函数,它的函数名以小写字母开头,它只能在fmt包内部被使用。

3、包的形态(Shape)

包是有关联关系的代码的集合,包规模可大可小,大包甚至可以横跨多个源文件。

同一个包的所有源文件都放在一个单一目录下面。

net/http包共由18个文件组成,导出了超过100个名字符号。

errors包仅仅由一个文件组成,并仅导出了一个名字符号。

4、包的命名

包的命名应该短小且有含义。
不要使用下划线,那样会导致包名过长;
不要过于概况,一个util包可能包含任何含义的代码;

    使用io/ioutil,而不是io/util
    使用suffixarray,而不是suffix_array

包名是其导出的类型名以及函数名的组成部分。

buf := new(bytes.Buffer)

仔细挑选包名

为用户选择一个好包名。

5、对包的测试

通过文件名我们可以区分出哪些是测试用源文件。测试文件以_test.go结尾。下面是一个测试文件的样例:

package fmt

import "testing"

var fmtTests = []fmtTest{
    {"%d", 12345, "12345"},
    {"%v", 12345, "12345"},
    {"%t", true, "true"},
}

func TestSprintf(t *testing.T) {
    for _, tt := range fmtTests {
        if s := Sprintf(tt.fmt, tt.val); s != tt.out {
            t.Errorf("…")
        }
    }
}

二、代码组织(Code organization)

1、工作区介绍(workspace)

你的Go源码被放在一个工作区(workspace)中。

一个workspace可以包含多个源码库(repository),诸如git,hg等。

Go工具知晓一个工作区的布局。

你无需使用Makefile,通过文件布局,我们可以完成所有事情。

若文件布局发生变动,则需重新构建。

$GOPATH/
    src/
        github.com/user/repo/
            mypkg/
                mysrc1.go
                mysrc2.go
            cmd/mycmd/
                main.go
    bin/
        mycmd

2、建立一个工作区

mkdir /tmp/gows
GOPATH=/tmp/gows

GOPATH环境变量告诉Go工具族你的工作区的位置。

go get github.com/dsymonds/fixhub/cmd/fixhub

go get命令从互联网网下载源代码库,并将它们放置在你的工作区中。

包的路径对Go工具来说很是重要,使用"github.com"意味着Go工具知道如何去获取你的源码库。

go install github.com/dsymonds/fixhub/cmd/fixhub

go install命令构建一个可执行程序,并将其放置在$GOPATH/bin/fixhub中。

3、我们的工作区

$GOPATH/
    bin/fixhub                              # installed binary
    pkg/darwin_amd64/                       # compiled archives
        code.google.com/p/goauth2/oauth.a
        github.com/…
    src/                                    # source repositories
        code.google.com/p/goauth2/
            .hg
            oauth                           # used by package go-github
            …
        github.com/
            golang/lint/…                 # used by package fixhub
                .git
            google/go-github/…            # used by package fixhub
                .git
            dsymonds/fixhub/
                .git
                client.go
                cmd/fixhub/fixhub.go        # package main

go get获取多个源码库。
go install使用这些源码库构建一个二进制文件。

4、为何要规定好文件布局

在构建时使用文件布局意味着可以更少的进行配置。

实际上,它意味着无配置。没有Makefile,没有build.xml。

在配置上花的时间少了,意味着在编程上可以花更多的时间。

Go社区中所有人都使用相同的布局,这会使得分享代码更加容易。

Go工具在一定程度上对Go社区的建设起到了帮助作用。

5、你的工作区在哪?

你可以拥有多个工作区,但大多数人只使用一个。那么你如何设置GOPATH这个环境变量呢?一个普遍的选择是:

GOPATH=$HOME

这样设置会将src、bin和pkg目录放到你的Home目录下。(这会很方便,因为$HOME/bin可能已经在你的PATH环境变量中了)。

6、在工作区下工作

CDPATH=$GOPATH/src/github.com:$GOPATH/src/code.google.com/p

$ cd dsymonds/fixhub
/tmp/gows/src/github.com/dsymonds/fixhub
$ cd goauth2
/tmp/gows/src/code.google.com/p/goauth2
$

将下面shell函数放在你的~/.profile中:

gocd () { cd `go list -f '{{.Dir}}' $1` }

$ gocd …/lint
/tmp/gows/src/github.com/golang/lint
$

三、依赖管理

1、在生产环境中,版本很重要

go get总是获取最新版本代码,即使这些代码破坏了你的构建。

这在开发阶段还好,但当你在发布阶段时,这将是一个问题。

我们需要其他工具。

2、版本管理

我最喜欢的技术:vendoring。

当构建二进制程序时,将你关心的包导入到一个_vendor工作区。
GOPATH=/tmp/gows/_vendor:/tmp/gows

注:
    1、在build时,我们通过构建脚本,临时修改GOPATH(GOPATH := ${PWD}/_vendor:${GOPATH}), 并将_vendor放置在主GOPATH前面,利用go build解析import包路径解析规则,go build优先得到_vendor下的第三方包信息,这样即便原GOPATH下有不同版本的相同第三方库,go build也会优先导入_vendor下的同名第三方库。
    2、go的相关工具在执行类似test这样的命令时会忽略前缀为_或.的目录,这样_vendor下的第三方库的test等操作将不会被执行。

当构建库时,将你关心的包导入你的源码库。重命名import为:
import "github.com/you/proj/vendor/github.com/them/lib"

长路径,不过对于自动化操作来说不算什么问题。写一个Go程序吧!

另外一种技术:gopkg.in。提供带版本的包路径:

gopkg.in/user/pkg.v3 -> github.com/user/pkg (branch/tag v3, v3.N, or v.3.N.M)

四、命名

1、命名很重要

程序源码中充满着各种名字。名字兼具代价和收益。

代价:空间与时间
    当阅读代码时,名字需要短时记忆
    你只能适应这么多,更长的名字需要占据更多的空间。

收益:信息
    一个好名字不仅仅是一个指代对象,它还能够传达某种信息。
    使用尽可能最短的名字用于在上下文中携带合理数量的信息。

在命名上花些时间(值得的)。

2、命名样式

使用camelCase,不要用下划线。

本地变量名字应该短小,通常由1到2个字符组成。

包名同行是一个小写词。

全局变量应该拥有长度更长的名字。

不要结巴!
 使用bytes.Buffer,不要用bytes.ByteBuffer
 使用zip.Reader,不要用zip.ZipReader
 使用errors.New,不要用errors.NewError
 使用r,不用bytesReader
 使用i,不用loopIterator

3、文档化注释

文档化注释放在导出标示符的声明之前:

// Join concatenates the elements of elem to create a single string.
// The separator string sep is placed between elements in the resulting string.
func Join(elem []string, sep string) string {

godoc工具可以解析出这些注释并将其展示在Web上:

func Join
    func Join (a []string, sep string) string

    Join concatenates the elements of  a to create a single string. The separetor string sep is placed between elements in the resulting string.

4、写文档化的注释

文档化的注释应用使用英文句子和段落。
除了为预定义格式进行的缩进外,没有其他特殊格式。

文档化注释应该以要描述的名词开头。

// Join concatenates…         good
// This function…             bad

包的文档应该放在包声明语句之前:

// Package fmt…
package fmt

在godoc.org上阅读Go世界的文档,比如:

godoc.org/code.google.com/p/go.tools/cmd/vet

Golang跨平台交叉编译

No Comments

近期在某本书上看到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)

 

Older Entries