Golang的演化历程

前端之家收集整理的这篇文章主要介绍了Golang的演化历程前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

Golang的演化历程

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

1、Hello Gophers!

package main

import "fmt"

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

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

2、历史

这是一个历史性的时刻。

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

3、成功

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

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

4、案例学习:两段程序

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

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

先看第一个“hello,world”

5、hello.b

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

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

6、hello.c

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

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

7、hello.c

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

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

8、hello.c,标准C草案

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

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

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

9、hello.c,标准C89

#include <stdio.h>

main(void) //<span face="Courier

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

这段代码出现在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 {@H_502_38@ print "hello,world\n";@H_502_38@ return 0;@H_502_38@ }

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

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

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

package main

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

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

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

package main

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

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

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

package main

import "fmt"

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

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

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

package main

import "fmt"

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

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

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

package main

import "fmt"

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

不再需要分号。

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

这也是当前版本的hello,world

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

都是历史了!

17、不仅仅有C

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

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

C: 语句和表达式语法@H_502_38@ Pascal: 声明语法@H_502_38@ Modula 2,Oberon 2:包@H_502_38@ CSP,Occam,Newsqueak,Limbo,Alef: 并发@H_502_38@ BCPL: 分号规则@H_502_38@ Smalltalk: 方法(method)@H_502_38@ Newsqueak: <-,:=@H_502_38@ APL: iota@H_502_38@ @H_502_38@ 等等。也有一些是全新发明的,例如defer、常量。

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

18、hello.go,Go 1版

将我们带到了今天。

package main

import "fmt"

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

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

19、Hello,World的16个tokens

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

20、package

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

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

为什么是package?

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

21、main

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

22、import

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

23、"fmt"

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

24、func

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

小插曲:

Mail thread from February 6,2008@H_502_38@ From: Ken Thompson<ken@google.com>
@H_502_38@ To: gri,r@H_502_38@ larry and sergey came by tonight. we 
talked about go for more than an hour. 
they both said they liked it very much.@H_502_38@ p.s. one of larrys comments was "why isnt function spelled func?"@H_502_38@ —@H_502_38@ From: Rob Pike<r@google.com>@H_502_38@ 
To: ken,gri@H_502_38@ fine with me. seems compatible with 'var'.@H_502_38@ anyway we can always say,"larry said to call it 'func'"

25、main

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

26、()

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

返回值以及语法

27、{

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

28、fmt

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

为什么是fmt,而不是format?

29、.

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

30、Println

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

31、(

传统函数语法

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

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

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

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

33、)

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

34、}

第一轮结束。

旁白:还没有讨论到的

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

35、成功

要素:@H_502_38@ – 站在巨人的肩膀上(building on history)@H_502_38@ – 经验之作(building on experience) 译注:最初的三个神级语言设计者@H_502_38@ – 设计过程@H_502_38@ – 早期idea提炼到最终的方案中@H_502_38@ – 由一个小团队专门集中精力做@H_502_38@ @H_502_38@ 最终:承诺@H_502_38@ 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)::@H_502_38@ p,mp:integer;@H_502_38@ SIEVE(i - 1)?p;@H_502_38@ print!p;@H_502_38@ mp := p; comment mp is a multiple of p;@H_502_38@ *[m:integer; SIEVE(i - 1)?m →@H_502_38@ *[m > mp → mp := mp + p];@H_502_38@ [m = mp → skip@H_502_38@ ||m < mp → SIEVE(i + 1)!m@H_502_38@ ] ]@H_502_38@ ||SIEVE(0)::print!2; n:integer; n := 3;@H_502_38@ *[n < 10000 → SIEVE(1)!n; n := n + 2]@H_502_38@ ||SIEVE(101)::*[n:integer;SIEVE(100)?n → print!n]@H_502_38@ ||print::*[(i:0..101) n:integer; SIEVE(i)?n → ...]@H_502_38@ ]

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

40、Newsqueak

circa 1988。

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

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

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

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

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

count:=mk(chan of int);

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

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

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

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

package Main

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

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

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

func Main() {@H_502_38@ Sieve();@H_502_38@ }

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

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

package main

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

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

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

func main() {@H_502_38@ Sieve()@H_502_38@ }

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

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

package main

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

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

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

func main() {@H_502_38@ Sieve()@H_502_38@ }

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

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

package main

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

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

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

func main() {@H_502_38@ Sieve()@H_502_38@ }

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

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

package main

import "fmt"

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

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

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

func main() {@H_502_38@ sieve();@H_502_38@ }

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

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

package main

import "fmt"

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

// Copy the values from channel 'src' to channel 'dst',prime int) {@H_502_38@ for i := range src { // Loop over values received from 'src'.@H_502_38@ if i%prime != 0 {@H_502_38@ dst <- i // Send 'i' to channel 'dst'.@H_502_38@ }@H_502_38@ }@H_502_38@ }

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

func main() {@H_502_38@ sieve()@H_502_38@ }

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

47、旁边,没有讨论到的

select

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

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

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

48、稳定性

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

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

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

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

49、趋势

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

50、成功

Go成功的元素:

显然的:功能和工具。

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

不那么显然的:过程

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

Golang测试技术

暂无评论

本篇文章内容来源于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文件中)@H_502_38@ package strings_test

import (@H_502_38@ "strings"@H_502_38@ "testing"@H_502_38@ )

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

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

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

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

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

2、表驱动测试

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

package strings_test

import (@H_502_38@ "strings"@H_502_38@ "testing"@H_502_38@ )

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

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

3、T结构

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

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

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

控制一个测试是否运行:

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

4、运行测试

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

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

$ go test@H_502_38@ PASS

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

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

$ go test github.com/nf/…

标准库的测试:@H_502_38@ $ go test std

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

$go test testgo@H_502_38@ can't load package: package testgo: cannot find package "testgo" in any of:@H_502_38@ /usr/local/go/src/pkg/testgo (from $GOROOT)@H_502_38@ /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@H_502_38@ FAIL _/Users/tony/Test/Go/testgo [build Failed]

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

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

$go test -v -cover@H_502_38@ === RUN TestReader@H_502_38@ — PASS: TestReader (0.00 seconds)@H_502_38@ … …@H_502_38@ === RUN: ExampleTrimPrefix@H_502_38@ — PASS: ExampleTrimPrefix (1.75us)@H_502_38@ PASS@H_502_38@ coverage: 96.9% of statements@H_502_38@ 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@H_502_38@ strings/reader.go:24: Len 66.7%@H_502_38@ strings/reader.go:31: Read 100.0%@H_502_38@ strings/reader.go:44: ReadAt 100.0%@H_502_38@ strings/reader.go:59: ReadByte 100.0%@H_502_38@ strings/reader.go:69: UnreadByte 100.0%@H_502_38@ … …@H_502_38@ strings/strings.go:638: Replace 100.0%@H_502_38@ strings/strings.go:674: EqualFold 100.0%@H_502_38@ 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

注:@H_502_38@ 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 {@H_502_38@ URL string // base URL of form http://ipaddr:port with no trailing slash@H_502_38@ Listener net.Listener

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

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

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) {@H_502_38@ fmt.Fprintln(w,"Hello,client")@H_502_38@ }))@H_502_38@ defer ts.Close()

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

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

fmt.Printf("%s",greeting)

5、httptest.ResponseRecorder

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

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

6、httptest.ResponseRecorder实战

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

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

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

w := httptest.NewRecorder()@H_502_38@ 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@H_502_38@ $ go run -race mysrc.go // to run the source file@H_502_38@ $ go build -race mycmd // to build the command@H_502_38@ $ go install -race mypkg // to install the package

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

例子代码

//testrace.go

package main

import "fmt"@H_502_38@ import "time"

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

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

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

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

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

8、测试并发(testing with concurrency)

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

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

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

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

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

使用方法:@H_502_38@ go vet [package]

10、从内部测试

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

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

11、从外部测试

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

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

– testing包使用fmt@H_502_38@ – fmt包的测试代码还必须导入testing包@H_502_38@ – 于是,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() {@H_502_38@ fmt.Println("Going down in flames!")@H_502_38@ os.Exit(1)@H_502_38@ }

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

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

组织Golang代码

技术志暂无评论

本月初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() {@H_502_38@ fmt.Println("Hello,world!")@H_502_38@ }

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

上面的HelloWorld程序import了fmt包。

函数Println定义在fmt包中。

2、一个例子:fmt包

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

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

func newPrinter() *pp {@H_502_38@ …@H_502_38@ }

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

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

3、包的形态(Shape)

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

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

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

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

4、包的命名

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

使用io/IoUtil,而不是io/util@H_502_38@ 使用suffixarray,而不是suffix_array

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

buf := new(bytes.Buffer)

仔细挑选包名

用户选择一个好包名。

5、对包的测试

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

package fmt

import "testing"

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

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

二、代码组织(Code organization)

1、工作区介绍(workspace)

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

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

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

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

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

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

2、建立一个工作区

mkdir /tmp/gows@H_502_38@ 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/@H_502_38@ bin/fixhub # installed binary@H_502_38@ pkg/darwin_amd64/ # compiled archives@H_502_38@ code.google.com/p/goauth2/oauth.a@H_502_38@ github.com/…@H_502_38@ src/ # source repositories@H_502_38@ code.google.com/p/goauth2/@H_502_38@ .hg@H_502_38@ oauth # used by package go-github@H_502_38@ …@H_502_38@ github.com/@H_502_38@ golang/lint/… # used by package fixhub@H_502_38@ .git@H_502_38@ google/go-github/… # used by package fixhub@H_502_38@ .git@H_502_38@ dsymonds/fixhub/@H_502_38@ .git@H_502_38@ client.go@H_502_38@ cmd/fixhub/fixhub.go # package main

go get获取多个源码库。@H_502_38@ 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@H_502_38@ /tmp/gows/src/github.com/dsymonds/fixhub@H_502_38@ $ cd goauth2@H_502_38@ /tmp/gows/src/code.google.com/p/goauth2@H_502_38@ $

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

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

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

三、依赖管理

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

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

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

我们需要其他工具。

2、版本管理

我最喜欢的技术:vendoring。

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

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

当构建库时,将你关心的包导入你的源码库。重命名import为:@H_502_38@ 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、命名很重要

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

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

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

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

2、命名样式

使用camelCase,不要用下划线。

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

包名同行是一个小写词。

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

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

3、文档化注释

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

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

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

func Join@H_502_38@ 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、写文档化的注释

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

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

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

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

// Package fmt…@H_502_38@ package fmt

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

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

Golang跨平台交叉编译

技术志暂无评论

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

我的Linux环境如下:

uname -a@H_502_38@ 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@H_502_38@ go version go1.3.1 linux/amd64

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

$ go env@H_502_38@ GOARCH="amd64"@H_502_38@ GOOS="linux"@H_502_38@ GOHOSTARCH="amd64"@H_502_38@ GOHOSTOS="linux"@H_502_38@ … …

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

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

//testport.go@H_502_38@ package main

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

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

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

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

在Linux/amd64下编译运行:

$ go build -o testport_linux testport.go@H_502_38@ $ testport_linux@H_502_38@ 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@H_502_38@ 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:@H_502_38@ #@H_502_38@ # GOROOT_FINAL: The expected final Go root,baked into binaries.@H_502_38@ # The default is the location of the Go tree during the build.@H_502_38@ #@H_502_38@ # GOHOSTARCH: The architecture for host tools (compilers and@H_502_38@ # binaries). Binaries of this type must be executable on the current@H_502_38@ # system,so the only common reason to set this is to set@H_502_38@ # GOHOSTARCH=386 on an amd64 machine.@H_502_38@ #@H_502_38@ # GOARCH: The target architecture for installed packages and tools.@H_502_38@ #@H_502_38@ # GOOS: The target operating system for installed packages and tools.@H_502_38@ … …

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

$ go build -x -o testport_linux testport.go@H_502_38@ WORK=/tmp/go-build286732099@H_502_38@ mkdir -p $WORK/command-line-arguments/_obj/@H_502_38@ cd /home/tonybai/Test/Go/porting@H_502_38@ /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@H_502_38@ cd .@H_502_38@ /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@H_502_38@ linux_amd64 linux_amd64_race obj tool

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

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

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

# Building C bootstrap tool.@H_502_38@ cmd/dist

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

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

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

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

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

$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go@H_502_38@ WORK=/tmp/go-build972764136@H_502_38@ mkdir -p $WORK/command-line-arguments/_obj/@H_502_38@ cd /home/tonybai/Test/Go/porting@H_502_38@ /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@H_502_38@ cd .@H_502_38@ /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@H_502_38@ $testport_darwin@H_502_38@ 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@H_502_38@ addr2line cgo fix nm objdump pack yacc

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

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

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

$ go build -x -o testport_linux testport.go@H_502_38@ WORK=/tmp/go-build636762567@H_502_38@ mkdir -p $WORK/command-line-arguments/_obj/@H_502_38@ cd /home/tonybai/Test/Go/porting@H_502_38@ /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@H_502_38@ cd .@H_502_38@ /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@H_502_38@ WORK=/tmp/go-build626615350@H_502_38@ mkdir -p $WORK/command-line-arguments/_obj/@H_502_38@ cd /home/tonybai/Test/Go/porting@H_502_38@ /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@H_502_38@ cd .@H_502_38@ /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@H_502_38@ package main

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

import "fmt"

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

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

$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go@H_502_38@ WORK=/tmp/go-build977176241@H_502_38@ mkdir -p $WORK/command-line-arguments/_obj/@H_502_38@ cd /home/tonybai/Test/Go/porting@H_502_38@ 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@H_502_38@ /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@H_502_38@ gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name@H_502_38@ 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@H_502_38@ 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@H_502_38@ 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@H_502_38@ 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@H_502_38@ /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@H_502_38@ /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@H_502_38@ 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@H_502_38@ /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@H_502_38@ 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@H_502_38@ cd .@H_502_38@ /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@H_502_38@ WORK=/tmp/go-build124869433@H_502_38@ 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@H_502_38@ … …@H_502_38@ # Building packages and commands for darwin/amd64.@H_502_38@ … …@H_502_38@ 37: error: 'AI_MASK' undeclared (first use in this function)@H_502_38@

docker容器内服务程序的优雅退出

技术志暂无评论

近期在试验如何将我们的产品部署到docker容器中去,这其中涉及到一个技术环节,那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出,指的就是程序在退出前有清理资源(比如关闭文件描述符、关闭socket),保存必要中间状态,持久化内存数据 (比如将内存中的数据flush到文件中)的机会。docker作为目前最火的轻量级虚拟化技术,其在后台服务领域的应用是极其广泛的,其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。

一、优雅退出的原理

对于服务程序而言,一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal,比如SIGINT、SIGTERM等(通过kill -l命令你可以查看到几十种signal)。当我们使用kill + 进程号时,系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号,并在这个handler中完成相应的“优雅退出”操作。

与“优雅退出”对立的是“暴力退出”,也就是我们常说的使用kill -9,也就是kill -s SIGKILL + 进程号,这个行为不会给目标进程任何时间空隙,而是直接将进程杀死,无论进程当前在做何种操作。这种操作常常导致“不一致”状态的出现。SIGKILL这 个信号比较特殊,进程无法有效监听该信号,无法有效针对该信号设置handler,无法改变其信号的默认处理行为。

二、测试用“服务程序”

为了测试docker容器对优雅退出支持,我们编写如下“服务程序”用于放在docker容器中运行:

//dockerapp1.go

package main

import "fmt"@H_502_38@ import "time"@H_502_38@ import "os"@H_502_38@ import "os/signal"@H_502_38@ import "syscall"

type signalHandler func(s os.Signal,arg interface{})

type signalSet struct {@H_502_38@ m map[os.Signal]signalHandler@H_502_38@ }

func signalSetNew() *signalSet {@H_502_38@ ss := new(signalSet)@H_502_38@ ss.m = make(map[os.Signal]signalHandler)@H_502_38@ return ss@H_502_38@ }

func (set *signalSet) register(s os.Signal,handler signalHandler) {@H_502_38@ if _,found := set.m[s]; !found {@H_502_38@ set.m[s] = handler@H_502_38@ }@H_502_38@ }

func (set *signalSet) handle(sig os.Signal,arg interface{}) (err error) {@H_502_38@ if _,found := set.m[sig]; found {@H_502_38@ set.m[sig](sig,arg)@H_502_38@ return nil@H_502_38@ } else {@H_502_38@ return fmt.Errorf("No handler available for signal %v",sig)@H_502_38@ }

panic("won't reach here")@H_502_38@ }

func main() {@H_502_38@ go sysSignalHandleDemo()@H_502_38@ time.Sleep(time.Hour) // make the main goroutine wait!@H_502_38@ }

func sysSignalHandleDemo() {@H_502_38@ ss := signalSetNew()@H_502_38@ handler := func(s os.Signal,arg interface{}) {@H_502_38@ fmt.Printf("handle signal: %v\n",s)@H_502_38@ if s == syscall.SIGTERM {@H_502_38@ fmt.Printf("signal termiate received,app exit normally\n")@H_502_38@ os.Exit(0)@H_502_38@ }@H_502_38@ }

ss.register(syscall.SIGINT,handler)@H_502_38@ ss.register(syscall.SIGUSR1,handler)@H_502_38@ ss.register(syscall.SIGUSR2,handler)@H_502_38@ ss.register(syscall.SIGTERM,handler)

for {@H_502_38@ c := make(chan os.Signal)@H_502_38@ var sigs []os.Signal@H_502_38@ for sig := range ss.m {@H_502_38@ sigs = append(sigs,sig)@H_502_38@ }@H_502_38@ signal.Notify(c)@H_502_38@ sig := <-c

err := ss.handle(sig,nil)@H_502_38@ if err != nil {@H_502_38@ fmt.Printf("unknown signal received: %v,app exit unexpectedly\n",sig)@H_502_38@ os.Exit(1)@H_502_38@ }@H_502_38@ }@H_502_38@ }

关于Go语言对系统Signal的处理,可以参考《Go中的系统Signal处理》一文。

三、制作测试用docker image

在《Ubuntu Server 14.04安装docker》一文中,我们完成了在ubuntu 14.04上安装docker的步骤。要制作测试用docker image,我们首先需要pull一个base image。我们以CentOS6.5为例:

在Ubuntu 14.04上执行:@H_502_38@ sudo docker pull centos:centos6

docker会自动官方仓库下载一个制作好的docker image。下载成功后,我们可以run一下试试,像这样:

$> sudo docker run -t -i centos:centos6 /bin/bash

我们查看一下CentOS6的小版本:@H_502_38@ $> cat /etc/centos-release@H_502_38@ CentOS release 6.5 (Final)

这是一个极其精简的CentOS,各种工具均未安装:@H_502_38@ bash-4.1# telnet@H_502_38@ bash: telnet: command not found@H_502_38@ bash-4.1# ssh@H_502_38@ bash: ssh: command not found@H_502_38@ bash-4.1# ftp@H_502_38@ bash: ftp: command not found@H_502_38@ bash-4.1# echo $PATH@H_502_38@ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

如果你要安装一些必要的工具,可以直接使用yum install,默认的base image已经将yum配置好了,可以直接使用。如果通过公司代理访问外部网络,别忘了先export http_proxy。另外docker直接使用宿主机的/etc/resolv.conf作为容器的DNS,我们也无需额外设置DNS。

接下来,我们就制作我们的第一个测试用image。安装官方推荐的Best Practice,我们使用Dockerfile来bulid一个测试用image。步骤如下:

- 建立~/ImagesFactory目录@H_502_38@ - 将构建好的dockerapp1拷贝到~/ImagesFactory目录下@H_502_38@ - 进入~/ImagesFactory目录,创建Dockerfile文件,Dockerfile内容如下:

FROM centos:centos6@H_502_38@ MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ COPY ./dockerapp1 /bin@H_502_38@ CMD /bin/dockerapp1

- 执行docker build,结果如下:

$ sudo docker build -t="test:v1" ./@H_502_38@ Sending build context to Docker daemon 7.496 MB@H_502_38@ Sending build context to Docker daemon@H_502_38@ Step 0 : FROM centos:centos6@H_502_38@ —> 68edf809afe7@H_502_38@ Step 1 : MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ —> Using cache@H_502_38@ —> c617b456934a@H_502_38@ Step 2 : COPY ./dockerapp1 /bin@H_502_38@ 2014/10/09 16:05:25 lchown /var/lib/docker/aufs/mnt/fb0e864d3f07ca17ef8b6b69f034728e1f1158fd3f9c83fa48243054b2f26958/bin/dockerapp1: not a directory

居然build失败,提示什么not a directory。于是各种Search,终于发现问题所在,原来是“COPY ./dockerapp1 /bin”这条命令错了,少了个“/”,将" /bin"改为“/bin/”就OK了,Docker真是奇怪啊,这块明显应该做得更兼容些。新的Dockerfile如下:

FROM centos:centos6@H_502_38@ MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ COPY ./dockerapp1 /bin/@H_502_38@ CMD /bin/dockerapp1

构建结果如下:

$ sudo docker build -t="test:v1" ./@H_502_38@ Sending build context to Docker daemon 7.496 MB@H_502_38@ Sending build context to Docker daemon@H_502_38@ Step 0 : FROM centos:centos6@H_502_38@ —> 68edf809afe7@H_502_38@ Step 1 : MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ —> Using cache@H_502_38@ —> c617b456934a@H_502_38@ Step 2 : COPY ./dockerapp1 /bin/@H_502_38@ —> 20c3783c42ab@H_502_38@ Removing intermediate container cab639ab4321@H_502_38@ Step 3 : CMD /bin/dockerapp1@H_502_38@ —> Running in 31875d3c37f9@H_502_38@ —> 21a720a808a7@H_502_38@ Removing intermediate container 31875d3c37f9@H_502_38@ Successfully built 21a720a808a7

$ sudo docker images@H_502_38@ REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE@H_502_38@ test v1 21a720a808a7 59 seconds ago 214.6 MB

四、第一个测试容器

我们基于image "test:v1"启动一个测试容器:

$ sudo docker run -d "test:v1"@H_502_38@ daf3ae88fec23a31cde9f6b9a3f40057953c87b56cca982143616f738a84dcba

$ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ daf3ae88fec2 test:v1 "/bin/sh -c /bin/doc 17 seconds ago Up 16 seconds condescending_sammet

通过docker run命令,我们基于image"test:v1"启动了一个容器。通过docker ps命令可以看到容器成功启动,容器id:daf3ae88fec2,别名为:condescending_sammet。

根据Dockerfile我们知道,容器启动后将执行"/bin/dockerapp1"这个程序,dockerapp1退出,容器即退出。 run命令的"-d"选项表示容器将以daemon的形式运行,我们在前台无法看到容器的输出。那么我们怎么查看容器的输出呢?我们可以通过 docker logs + 容器id的方式查看容器内应用的标准输出或标准错误。我们也可以进入容器来查看。

进入容器有多种方法,比如用sudo docker attachdaf3ae88fec2。attach后,就好比将daemon方式运行的容器 拿到了前台,你可以Ctrl + C一下,可以看到如下dockerapp1的输出:

^Chandle signal: interrupt

另外一种方式是利用nsenter工具进入我们容器的namespace空间。ubuntu 14.04下可以通过如下方式安装该工具:

$ wgethttps://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz; tar xzvf util-linux-2.24.tar.gz@H_502_38@ $ cd util-linux-2.24@H_502_38@ $ ./configure –without-ncurses && make nsenter@H_502_38@ $ sudo cp nsenter /usr/local/bin

安装后,我们通过如下方式即可进入上面的容器:

$ echo $(sudo docker inspect –format "{{ .State.Pid }}" daf3ae88fec2)@H_502_38@ 5494@H_502_38@ $ sudo nsenter –target 5494 –mount –uts –ipc –net –pid@H_502_38@ -bash-4.1# ps -ef@H_502_38@ UID PID PPID C STIME TTY TIME CMD@H_502_38@ root 1 0 0 09:20 ? 00:00:00 /bin/dockerapp1@H_502_38@ root 16 0 0 09:32 ? 00:00:00 -bash@H_502_38@ root 27 16 0 09:32 ? 00:00:00 ps -ef@H_502_38@ -bash-4.1#

进入容器后通过ps命令可以看到正在运行的dockerapp1程序。在容器内,我们可以通过kill来测试dockerapp1的运行情况:

-bash-4.1# kill -s SIGINT 1

通过前面的attach窗口,我们可以看到dockerapp1输出:

handle signal: interrupt

如果你发送SIGTERM信号,那么dockerapp1将终止运行,容器也就停止了。

-bash-4.1# kill 1

attach窗口显示

signal termiate received,app exit normally

我们可以看到容器启动后默认执行的时Dockerfile中的CMD命令,如果Dockerfile中有多行CMD命令,Docker在启动容器 时只会执行最后一条CMD命令。如果在docker run中指定了命令,docker则会执行命令行中的命令而不会执行dockerapp1,比如:

$ sudo docker run -t -i "test:v1" /bin/bash@H_502_38@ bash-4.1#

这里我们看到直接执行的时bash,dockerapp1并未执行。

五、docker stop的行为

我们先来看看docker stop的manual:

$ sudo docker stop –help@H_502_38@ Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]@H_502_38@ Stop a running container by sending SIGTERM and then SIGKILL after a grace period@H_502_38@ -t,–time=10 Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.

可以看出当我们执行docker stop时,docker会首先向容器内的当前主程序发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出, 那么stop命令会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死,变为退出状态。

我们来验证一下docker stop的行为。启动刚才那个容器:

$ sudo docker start daf3ae88fec2@H_502_38@ daf3ae88fec2

attach到容器daf3ae88fec2@H_502_38@ $ sudo docker attach daf3ae88fec2

新打开一个窗口,执行docker stop命令:@H_502_38@ $ sudo docker stop daf3ae88fec2@H_502_38@ daf3ae88fec2

可以看到attach窗口输出:@H_502_38@ handle signal: terminated@H_502_38@ signal termiate received,app exit normally

通过docker ps查看,发现容器已经退出

也许通过上面的例子还不能直观的展示stop命令的两阶段行为,因为dockerapp1收到SIGTERM后直接就退出 了,stop命令无需等待容器慢慢退出,也无需发送SIGKILL。我们改造一下dockerapp1这个程序。

我们复制一下dockerapp1.go为dockerapp2.go,编辑dockerapp2.go,将handler中对SIGTERM的 处理注释掉,其他不变:

handler := func(s os.Signal,s)@H_502_38@ /*@H_502_38@ if s == syscall.SIGTERM {@H_502_38@ fmt.Printf("signal termiate received,app exit normally\n")@H_502_38@ os.Exit(0)@H_502_38@ }@H_502_38@ */@H_502_38@ }

我们使用dockerapp2来构建一个新image:test:v2,将Dockerfile中得dockerapp1换成 dockerapp2即可。

$ sudo docker build -t="test:v2" ./@H_502_38@ Sending build context to Docker daemon 9.369 MB@H_502_38@ Sending build context to Docker daemon@H_502_38@ Step 0 : FROM centos:centos6@H_502_38@ —> 68edf809afe7@H_502_38@ Step 1 : MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ —> Using cache@H_502_38@ —> c617b456934a@H_502_38@ Step 2 : COPY ./dockerapp2 /bin/@H_502_38@ —> 27cd613a9bd7@H_502_38@ Removing intermediate container 07c760b6223b@H_502_38@ Step 3 : CMD /bin/dockerapp2@H_502_38@ —> Running in 1aac086452a7@H_502_38@ —> 82eb876fefd2@H_502_38@ Removing intermediate container 1aac086452a7@H_502_38@ Successfully built 82eb876fefd2

利用image "test:v2"创建一个容器来测试stop。

$ sudo docker run -d "test:v2"@H_502_38@ 29f3ec1af3c355458cbbd802a5e8a53da28e9f51a56ce822c7bba2a772edceac

$ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ 29f3ec1af3c3 test:v2 "/bin/sh -c /bin/doc 7 seconds ago Up 6 seconds romantic_feynman

Attach到这个容器并观察,在另外一个窗口stop该container。我们在attach窗口只看到如下输出

handle signal: terminated

stop命令的执行没有立即返回,而是等待容器退出。等待10s后,容器退出,stop命令执行结束。从这个例子我们可以明显看出stop的两阶 段行为。

如果我们以sudo docker run -i -t "test:v1" /bin/bash形式启动容器,那stop命令会将SIGTERM发送给bash这个程序,即使你通过nsenter进入容 器,启动了dockerapp1,dockerapp1也不会收到SIGTERM,dockerapp1会随着容器的退出而被强行终止,就像被 kill -9了一样。

六、多进程容器服务程序

上面无论是dockerapp1还是dockerapp2,都是一个单进程服务程序。如果我们在容器内执行一个多进程程序,我们该如何优雅退出 呢?我们先来编写一个多进程的服务程序dockerapp3:

在dockerapp1.go的基础上对main和sysSignalHandleDemo进行修改形成dockerapp3.go,修改后这两 个函数代码如下:

//dockerapp3.go@H_502_38@ … …

func main() {@H_502_38@ go sysSignalHandleDemo()

pid,_,err := syscall.RawSyscall(syscall.SYS_FORK,0)@H_502_38@ if err != 0 {@H_502_38@ fmt.Printf("err fork process,err: %v\n",err)@H_502_38@ return@H_502_38@ }

if pid == 0 {@H_502_38@ fmt.Printf("i am in child process,pid = %v\n",syscall.Getpid())@H_502_38@ time.Sleep(time.Hour) // make the child process wait@H_502_38@ }@H_502_38@ fmt.Printf("i am parent process,syscall.Getpid())@H_502_38@ fmt.Printf("fork ok,childpid = %v\n",pid)@H_502_38@ time.Sleep(time.Hour) // make the main goroutine wait!@H_502_38@ }

func sysSignalHandleDemo() {@H_502_38@ ss := signalSetNew()@H_502_38@ handler := func(s os.Signal,arg interface{}) {@H_502_38@ fmt.Printf("%v: handle signal: %v\n",syscall.Getpid(),s)@H_502_38@ if s == syscall.SIGTERM {@H_502_38@ fmt.Printf("%v: signal termiate received,app exit normally\n",syscall.Getpid())@H_502_38@ os.Exit(0)@H_502_38@ }@H_502_38@ }

ss.register(syscall.SIGINT,nil)@H_502_38@ if err != nil {@H_502_38@ fmt.Printf("%v: unknown signal received: %v,sig)@H_502_38@ os.Exit(1)@H_502_38@ }@H_502_38@ }@H_502_38@ }

dockerapp3利用fork创建了一个子进程,这样dockerapp3实际上是两个进程在运行,各自有自己的signal监听 goroutine,goroutine的处理逻辑是相同的。注意:由于Windows和Mac OS X不具备fork语义,因此在这两个平台上运行dockerapp3不会得到预期结果。

利用dockerapp3,我们创建image "test:v3":

$ sudo docker build -t="test:v3" ./@H_502_38@ [sudo] password for tonybai:@H_502_38@ Sending build context to Docker daemon 11.24 MB@H_502_38@ Sending build context to Docker daemon@H_502_38@ Step 0 : FROM centos:centos6@H_502_38@ —> 68edf809afe7@H_502_38@ Step 1 : MAINTAINER Tony Bai<bigwhite.cn@gmail.com>@H_502_38@ —> Using cache@H_502_38@ —> c617b456934a@H_502_38@ Step 2 : COPY ./dockerapp3 /bin/@H_502_38@ —> 6ccf97065853@H_502_38@ Removing intermediate container 6d85fe241939@H_502_38@ Step 3 : CMD /bin/dockerapp3@H_502_38@ —> Running in 75d76380992a@H_502_38@ —> c9e7bf361ed7@H_502_38@ Removing intermediate container 75d76380992a@H_502_38@ Successfully built c9e7bf361ed7

启动基于test:v3 image的容器:

$ sudo docker run -d "test:v3"@H_502_38@ 781cecb4b3628cb33e1b104ea57e506ad5cb4a44243256ebd1192af86834bae6@H_502_38@ $ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ 781cecb4b362 test:v3 "/bin/sh -c /bin/doc 5 seconds ago Up 4 seconds insane_bohr

通过docker logs查看dockerapp3的输出

$ sudo docker logs 781cecb4b362@H_502_38@ i am parent process,pid = 1@H_502_38@ fork ok,childpid = 13@H_502_38@ i am in child process,pid = 13

可以看出主进程pid为1,子进程pid为13。我们通过stop停止该容器:

$ sudo docker stop 781cecb4b362@H_502_38@ 781cecb4b362

再次通过docker logs查看:

$ sudo docker logs 781cecb4b362@H_502_38@ i am parent process,pid = 13@H_502_38@ 1: handle signal: terminated@H_502_38@ 1: signal termiate received,app exit normally

我们可以看到主进程收到了stop发来的SIGTERM并退出,主进程的退出导致容器退出,导致子进程13也无法生存,并且没有优雅退出。而在非 容器状态下,子进程是可以被init进程接管的。

因此对于docker容器内运行的多进程程序,stop命令只会将SIGTERM发送给容器主进程,要想让其他进程也能优雅退出,需要在主进程与 其他进程间建立一种通信机制。在主进程退出前,等待其他子进程退出。待所有其他进程退出后,主进程再退出,容器停止。这样才能保证服务程序的优雅 退出

七、容器内启动多个服务程序

虽说dockerbest practice建议一个container内只放置一个服务程序,但对已有的一些遗留系统,在架构没有做出重构之前,很可能会有在一个 container中部署两个以上服务程序的情况和需求。而docker Dockerfile只允许执行一个CMD,这种情况下,我们就需要借助类似supervisor这样的进程监控管理程序来启动和管理container 内的多个程序了。

下面我们来自制作一个基于centos:centos6的安装了supervisord以及两个服务程序的image。我们将dockerapp1拷贝一份,并将拷贝命名为dockerapp1-brother。下面是我们的Dockerfile:

FROM centos:centos6@H_502_38@ MAINTAINER Tony Bai <bigwhite.cn@gmail.com>@H_502_38@ RUN yum install python-setuptools -y@H_502_38@ RUN easy_install supervisor@H_502_38@ RUN mkdir -p /var/log/supervisor@H_502_38@ COPY ./supervisord.conf /etc/supervisord.conf@H_502_38@ COPY ./dockerapp1 /bin/@H_502_38@ COPY ./dockerapp1-brother /bin/@H_502_38@ CMD ["/usr/bin/supervisord"]

supervisord的配置文件supervisord.conf内容如下:

; supervisor config file

[unix_http_server]@H_502_38@ file=/var/run/supervisor.sock ; (the path to the socket file)@H_502_38@ chmod=0700 ; sockef file mode (default 0700)

[supervisord]@H_502_38@ logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)@H_502_38@ pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)@H_502_38@ childlogdir=/var/log/supervisor ; ('AUTO' child log dir,default $TEMP)

[rpcinterface:supervisor]@H_502_38@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]@H_502_38@ serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket

[supervisord]@H_502_38@ nodaemon=false

[program:dockerapp1]@H_502_38@ command=/bin/dockerapp1@H_502_38@ stdout_logfile=/tmp/dockerapp1.log@H_502_38@ stopsignal=TERM@H_502_38@ stopwaitsecs=10

[program:dockerapp1-brother]@H_502_38@ command=/bin/dockerapp1-brother@H_502_38@ stdout_logfile=/tmp/dockerapp1-brother.log@H_502_38@ stopsignal=QUIT@H_502_38@ stopwaitsecs=10

开始build镜像:@H_502_38@ $> sudo docker build -t="test:supervisor-v1" ./@H_502_38@ … …@H_502_38@ Successfully built d006b9ad10eb

基于该镜像,启动一个容器:@H_502_38@ $> sudo docker run -d "test:supervisor-v1"@H_502_38@ 05ded2b898c90059d4c9b5c6ccc8603b6848ae767360c42bd9b36ff87fb4b9df

执行ps命令查看镜像id:@H_502_38@ $ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

怎么回事?Container没有启动起来?

$ sudo docker ps -a@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ 05ded2b898c9 test:supervisor-v1 "/usr/bin/supervisor 22 seconds ago Exited (0) 21 seconds ago hungry_engelbart

通过ps -a查看,container启动是成功了,但是成功退出了。于是尝试查看一下log:

sudo docker logs 05ded2b898c9@H_502_38@ /usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.@H_502_38@ 'Supervisord is running as root and it is searching '

似乎是supervisord转为daemon程序,容器主进程退出了,容器随之终止了。

看来容器内的supervisord不能以daemon形式运行,应该以前台形式run。修改一下supervisord.conf中得配置:

将@H_502_38@ [supervisord]@H_502_38@ nodaemon=false

改为

[supervisord]@H_502_38@ nodaemon=true

重新制作镜像:

$ sudo docker build -t="test:supervisor-v2" ./@H_502_38@ Sending build context to Docker daemon 13.12 MB@H_502_38@ Sending build context to Docker daemon@H_502_38@ Step 0 : FROM centos:centos6@H_502_38@ —> 68edf809afe7@H_502_38@ Step 1 : MAINTAINER Tony Bai <bigwhite.cn@gmail.com>@H_502_38@ —> Using cache@H_502_38@ —> c617b456934a@H_502_38@ Step 2 : RUN yum install python-setuptools -y@H_502_38@ —> Using cache@H_502_38@ —> e09c66a1ea8c@H_502_38@ Step 3 : RUN easy_install supervisor@H_502_38@ —> Using cache@H_502_38@ —> 9c8797e8c27e@H_502_38@ Step 4 : RUN mkdir -p /var/log/supervisor@H_502_38@ —> Using cache@H_502_38@ —> 9bfc67f8517d@H_502_38@ Step 5 : COPY ./supervisord.conf /etc/supervisord.conf@H_502_38@ —> 8c514f998363@H_502_38@ Removing intermediate container 4a185856e6ed@H_502_38@ Step 6 : COPY ./dockerapp1 /bin/@H_502_38@ —> 0317bd4914d3@H_502_38@ Removing intermediate container ac5738380854@H_502_38@ Step 7 : COPY ./dockerapp1-brother /bin/@H_502_38@ —> d89711888bdf@H_502_38@ Removing intermediate container eadc9444e716@H_502_38@ Step 8 : CMD ["/usr/bin/supervisord"]@H_502_38@ —> Running in aaa042ac3914@H_502_38@ —> 9655256bbfed@H_502_38@ Removing intermediate container aaa042ac3914@H_502_38@ Successfully built 9655256bbfed

有了前面的铺垫,这次build image瞬间完成。启动容器,查看容器启动状态,查看容器内supervisord的运行日志如下:

$ sudo docker run -d "test:supervisor-v2"@H_502_38@ 61916f1c82338b28ced101b6bde119e4afb7c7fa349b4332ed51a43a4586b1b9

$ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ 61916f1c8233 test:supervisor-v2 "/usr/bin/supervisor 16 seconds ago Up 16 seconds prickly_einstein

$ sudo docker logs 8eb3e9892e66

/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.@H_502_38@ 'Supervisord is running as root and it is searching '@H_502_38@ 2014-10-09 14:36:02,334 CRIT Supervisor running as root (no user in config file)@H_502_38@ 2014-10-09 14:36:02,349 INFO RPC interface 'supervisor' initialized@H_502_38@ 2014-10-09 14:36:02,349 CRIT Server 'unix_http_server' running without any HTTP authentication checking@H_502_38@ 2014-10-09 14:36:02,349 INFO supervisord started with pid 1@H_502_38@ 2014-10-09 14:36:03,354 INFO spawned: 'dockerapp1' with pid 14@H_502_38@ 2014-10-09 14:36:03,363 INFO spawned: 'dockerapp1-brother' with pid 15@H_502_38@ 2014-10-09 14:36:04,368 INFO success: dockerapp1 entered RUNNING state,process has stayed up for > than 1 seconds (startsecs)@H_502_38@ 2014-10-09 14:36:04,369 INFO success: dockerapp1-brother entered RUNNING state,process has stayed up for > than 1 seconds (startsecs)

可以看到supervisord已经将dockerapp1和dockerapp1-brother启动起来了。

现在我们尝试停止容器,我们预期是supervisord在退出通知dockerapp1和dockerapp1-brother先退出,我们可以通过 查看容器内的/tmp/dockerapp1.log和/tmp/dockerapp1-brother.log来确认supervisord是否做了通 知。

$ sudo docker stop 61916f1c8233@H_502_38@ 61916f1c8233

$ sudo docker logs 61916f1c8233@H_502_38@ … …@H_502_38@ 2014-10-09 14:37:52,253 WARN received SIGTERM indicating exit request@H_502_38@ 2014-10-09 14:37:52,254 INFO waiting for dockerapp1,dockerapp1-brother to die@H_502_38@ 2014-10-09 14:37:52,254 INFO stopped: dockerapp1-brother (exit status 0)@H_502_38@ 2014-10-09 14:37:52,256 INFO stopped: dockerapp1 (exit status 0)

通过容器的log,我们看出supervisord是等待两个程序退出后才退出的,不过我们还是要看看两个程序的输出日志以最终确认。重新启动容器,通过nsenter进入到容器中。

-bash-4.1# vi /tmp/dockerapp1.log

handle signal: terminated@H_502_38@ signal termiate received,app exit normally

-bash-4.1# vi /tmp/dockerapp1-brother.log

handle signal: terminated@H_502_38@ signal termiate received,app exit normally

两个程序的标准输出日志证实了我们的预期。

BTW,在物理机上测试supervisord以daemon形式运行,当kill掉supervisord时,supervisord是不会通知其监控 和管理的程序退出的。只有在以non-daemon形式运行时,supervisord才会在退出前先通知下面的程序退出。如果在一段时间内下面程序没有 退出,supervisord在退出前会kill -9强制杀死这些程序的进程。

最后要说的时,在验证一些想法时,没有必要build image,我们可以直接将本地文件copy到容器中,下面是一个例子,我们将dockerapp1和dockerapp1-brother拷贝到镜像中:@H_502_38@ $ sudo docker ps@H_502_38@ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES@H_502_38@ 4d8982bfccc7 centos:centos6 "/bin/bash" 26 minutes ago Up 26 minutes sharp_thompson@H_502_38@ $ sudo docker inspect -f '{{.Id}}' 4d8982bfccc7@H_502_38@ 4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4@H_502_38@ $ sudo cp dockerapp1 /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1@H_502_38@ $ sudo cp dockerapp1-brother /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1-brother

Golang Channel用法简编

技术志暂无评论

在进入正式内容前,我这里先顺便转发一则消息,那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面,国内go程序员与爱好者们可以到"Golang中 国",即golangtc.com去下载go 1.3.2版本。

Go这门语言也许你还不甚了解,甚至是完全不知道,这也有情可原,毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下,如 果没有涉及到“docker”字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作:《Docker – 从入门到实践》。

据我了解,目前国内试水Go语言开发后台系统的大公司与初创公司日益增多,比如七牛、京东、小米,盛大,金山,东软,搜狗等,在这里我们可以看到一些公司的Go语言应用列表,并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中,不过目前来看多以Go入 门与基础为主题,Go idioms、tips或Best Practice的Share并不多见,想必国内的先行者、布道师们还在韬光养晦,积攒经验,等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台,比如Golang技术大会之类的的平台。

在国外,虽然Go也刚刚起步,但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多,除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外,由Gopher Academy发起的GopherCon会议也于今年第一次举行,并放出诸多高质量资料,在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕,估计后续这两个大会将撑起Golang技术分享 的旗帜。

言归正传,这里要写的东西并非原创,自己的Go仅仅算是入门级别,工程经验、Best Practice等还谈不上有多少,因此这里主要是针对GopherCon2014上的“舶来品”的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验,这里针对其分享内容,记录一些学习体会和理解,并结合一些外延知识,也可以算是一种学习笔记吧,仅供参考。

一、Golang并发基础理论

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并不多,毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。

维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别:

Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言,又有一些根本上的不同之处:@H_502_38@ – CSP模型处理过程是匿名的,而Actor模型中的Actor则具有身份标识。@H_502_38@ – CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。相反,actor模型中的消息传递是异步 的,即消息的发送和接收无需在同一时间进行,发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下,基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。@H_502_38@ – CSP使用显式的Channel用于消息传递,而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下,进程可 以从一个实际上拥有身份标识的channel接收消息,而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。

二、Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel
@H_502_38@ c <- x //向一个Channel发送一个值@H_502_38@ <- c //从一个Channel中接收一个值@H_502_38@ x = <- c //从Channel c接收一个值并将其存储到x中@H_502_38@ x,ok = <- c //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

不带缓冲的Channel兼具通信和同步两种特性,颇受青睐。

三、Channel用作信号(Signal)的场景

1、等待一个事件(Event)

等待一个事件,有时候通过close一个Channel就足够了。例如:

//testwaitevent1.go@H_502_38@ package main

import "fmt"

func main() {@H_502_38@ fmt.Println("Begin doing something!")@H_502_38@ c := make(chan bool)@H_502_38@ go func() {@H_502_38@ fmt.Println("Doing something…")@H_502_38@ close(c)@H_502_38@ }()@H_502_38@ <-c@H_502_38@ fmt.Println("Done!")@H_502_38@ }

这里main goroutine通过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。

关于输出结果:

根据《Go memory model》中关于close channel与recv from channel的order的定义:The closing of a channel happens before a receive that returns a zero value because the channel is closed.

我们可以很容易判断出上面程序的输出结果:

Begin doing something!@H_502_38@ Doing something…@H_502_38@ Done!

如果将close(c)换成c<-true,则根据《Go memory model》中的定义:A receive from an unbuffered channel happens before the send on that channel completes.@H_502_38@ "<-c"要先于"c<-true"完成,但也不影响日志的输出顺序,输出结果仍为上面三行。

2、协同多个Goroutines

同上,close channel还可以用于协同多个Goroutines,比如下面这个例子,我们创建了100个Worker Goroutine,这些Goroutine在被创建出来后都阻塞在"<-start"上,直到我们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。

//testwaitevent2.go@H_502_38@ package main

import "fmt"

func worker(start chan bool,index int) {@H_502_38@ <-start@H_502_38@ fmt.Println("This is Worker:",index)@H_502_38@ }

func main() {@H_502_38@ start := make(chan bool)@H_502_38@ for i := 1; i <= 100; i++ {@H_502_38@ go worker(start,i)@H_502_38@ }@H_502_38@ close(start)@H_502_38@ select {} //deadlock we expected@H_502_38@ }

3、Select

【select的基本操作】@H_502_38@ select是Go语言特有的操作,使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。

select {@H_502_38@ case x := <- somechan:@H_502_38@ // … 使用x进行一些操作

case y,ok := <- someOtherchan:@H_502_38@ // … 使用y进行一些操作,@H_502_38@ //检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:@H_502_38@ // … z值被成功发送到Channel上时

default:@H_502_38@ // … 上面case均无法通信时,执行此分支@H_502_38@ }

【惯用法:for/select】

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出

for {@H_502_38@ select {@H_502_38@ case x := <- somechan:@H_502_38@ // … 使用x进行一些操作

case y,ok := <- someOtherchan:@H_502_38@ // … 使用y进行一些操作,@H_502_38@ // 检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:@H_502_38@ // … z值被成功发送到Channel上时

default:@H_502_38@ // … 上面case均无法通信时,执行此分支@H_502_38@ }@H_502_38@ }

【终结workers】

下面是一个常见的终结sub worker goroutines的方法,每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知

//testterminateworker1.go@H_502_38@ package main

import (@H_502_38@ "fmt"@H_502_38@ "time"@H_502_38@ )

func worker(die chan bool,index int) {@H_502_38@ fmt.Println("Begin: This is Worker:",index)@H_502_38@ for {@H_502_38@ select {@H_502_38@ //case xx:@H_502_38@ //做事的分支@H_502_38@ case <-die:@H_502_38@ fmt.Println("Done: This is Worker:",index)@H_502_38@ return@H_502_38@ }@H_502_38@ }@H_502_38@ }

func main() {@H_502_38@ die := make(chan bool)

for i := 1; i <= 100; i++ {@H_502_38@ go worker(die,i)@H_502_38@ }

time.Sleep(time.Second * 5)@H_502_38@ close(die)@H_502_38@ select {}//deadlock we expected@H_502_38@ }

【终结验证】

有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法

//testterminateworker2.go@H_502_38@ package main

import (@H_502_38@ "fmt"@H_502_38@ //"time"@H_502_38@ )

func worker(die chan bool) {@H_502_38@ fmt.Println("Begin: This is Worker")@H_502_38@ for {@H_502_38@ select {@H_502_38@ //case xx:@H_502_38@ //做事的分支@H_502_38@ case <-die:@H_502_38@ fmt.Println("Done: This is Worker")@H_502_38@ die <- true@H_502_38@ return@H_502_38@ }@H_502_38@ }@H_502_38@ }

func main() {@H_502_38@ die := make(chan bool)

go worker(die)

die <- true@H_502_38@ <-die@H_502_38@ fmt.Println("Worker goroutine has been terminated")@H_502_38@ }

关闭的Channel永远不会阻塞】

下面演示在一个已经关闭了的channel上读写的结果:

//testoperateonclosedchannel.go@H_502_38@ package main

import "fmt"

func main() {@H_502_38@ cb := make(chan bool)@H_502_38@ close(cb)@H_502_38@ x := <-cb@H_502_38@ fmt.Printf("%#v\n",x)

x,ok := <-cb@H_502_38@ fmt.Printf("%#v %#v\n",x,ok)

ci := make(chan int)@H_502_38@ close(ci)@H_502_38@ y := <-ci@H_502_38@ fmt.Printf("%#v\n",y)

cb <- true@H_502_38@ }

$go runtestoperateonclosedchannel.go@H_502_38@ false@H_502_38@ false false@H_502_38@ 0@H_502_38@ panic: runtime error: send on closed channel

可以看到在一个已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。

关闭带缓存的channel】

将unbuffered channel换成buffered channel会怎样?我们看下面例子:

//testclosedbufferedchannel.go@H_502_38@ package main

import "fmt"

func main() {@H_502_38@ c := make(chan int,3)@H_502_38@ c <- 15@H_502_38@ c <- 34@H_502_38@ c <- 65@H_502_38@ close(c)@H_502_38@ fmt.Printf("%d\n",<-c)@H_502_38@ fmt.Printf("%d\n",<-c)

c <- 1@H_502_38@ }

$go run testclosedbufferedchannel.go@H_502_38@ 15@H_502_38@ 34@H_502_38@ 65@H_502_38@ 0@H_502_38@ panic: runtime error: send on closed channel

可以看出带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

【range】

Golang中的range常常和channel并肩作战,它被用来从channel中读取所有值。下面是一个简单的实例:

//testrange.go@H_502_38@ package main

import "fmt"

func generator(strings chan string) {@H_502_38@ strings <- "Five hour's New York jet lag"@H_502_38@ strings <- "and Cayce Pollard wakes in Camden Town"@H_502_38@ strings <- "to the dire and ever-decreasing circles"@H_502_38@ strings <- "of disrupted circadian rhythm."@H_502_38@ close(strings)@H_502_38@ }

func main() {@H_502_38@ strings := make(chan string)@H_502_38@ go generator(strings)@H_502_38@ for s := range strings {@H_502_38@ fmt.Printf("%s\n",s)@H_502_38@ }@H_502_38@ fmt.Printf("\n")@H_502_38@ }

四、隐藏状态

下面通过一个例子来演示一下channel如何用来隐藏状态:

1、例子:唯一的ID服务

//testuniqueid.go@H_502_38@ package main

import "fmt"

func newUniqueIDService() <-chan string {@H_502_38@ id := make(chan string)@H_502_38@ go func() {@H_502_38@ var counter int64 = 0@H_502_38@ for {@H_502_38@ id <- fmt.Sprintf("%x",counter)@H_502_38@ counter += 1@H_502_38@ }@H_502_38@ }()@H_502_38@ return id@H_502_38@ }@H_502_38@ func main() {@H_502_38@ id := newUniqueIDService()@H_502_38@ for i := 0; i < 10; i++ {@H_502_38@ fmt.Println(<-id)@H_502_38@ }@H_502_38@ }

$ go run testuniqueid.go@H_502_38@ 0@H_502_38@ 1@H_502_38@ 2@H_502_38@ 3@H_502_38@ 4@H_502_38@ 5@H_502_38@ 6@H_502_38@ 7@H_502_38@ 8@H_502_38@ 9

newUniqueIDService通过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需通过channel获得最新id即可。

五、默认情况

我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法

1、select for non-blocking receive

idle:= make(chan []byte,5) //用一个带缓冲的channel构造一个简单的队列

select {@H_502_38@ case b = <-idle:
 //尝试从idle队列中读取@H_502_38@ …@H_502_38@ default: //队列空,分配一个新的buffer@H_502_38@ makes += 1@H_502_38@ b = make([]byte,size)@H_502_38@ }

2、select for non-blocking send

idle:= make(chan []byte,5)//用一个带缓冲的channel构造一个简单的队列

select {@H_502_38@ case idle <- b: //尝试向队列中插入一个buffer@H_502_38@ //…@H_502_38@ default: //队列满?

}

六、Nil Channels

1、nil channels阻塞

对一个没有初始化的channel进行读写操作都将发生阻塞,例子如下:

package main

func main() {@H_502_38@ var c chan int@H_502_38@ <-c@H_502_38@ }

$go run testnilchannel.go@H_502_38@ fatal error: all goroutines are asleep – deadlock!

package main

func main() {@H_502_38@ var c chan int@H_502_38@ c <- 1@H_502_38@ }

$go run testnilchannel.go@H_502_38@ fatal error: all goroutines are asleep – deadlock!

2、nil channel在select中很有用

看下面这个例子:

//testnilchannel_bad.go@H_502_38@ package main

import "fmt"@H_502_38@ import "time"

func main() {@H_502_38@ var c1,c2 chan int = make(chan int),make(chan int)@H_502_38@ go func() {@H_502_38@ time.Sleep(time.Second * 5)@H_502_38@ c1 <- 5@H_502_38@ close(c1)@H_502_38@ }()

go func() {@H_502_38@ time.Sleep(time.Second * 7)@H_502_38@ c2 <- 7@H_502_38@ close(c2)@H_502_38@ }()

for {@H_502_38@ select {@H_502_38@ case x := <-c1:@H_502_38@ fmt.Println(x)@H_502_38@ case x := <-c2:@H_502_38@ fmt.Println(x)@H_502_38@ }@H_502_38@ }@H_502_38@ fmt.Println("over")@H_502_38@ }

我们原本期望程序交替输出5和7两个数字,但实际的输出结果却是:

5@H_502_38@ 0@H_502_38@ 0@H_502_38@ 0@H_502_38@ … … 0死循环

再仔细分析代码,原来select每次按case顺序evaluate:@H_502_38@ – 前5s,select一直阻塞;@H_502_38@ – 第5s,c1返回一个5后被close了,“case x := <-c1”这个分支返回,select输出5,并重新select@H_502_38@ – 下一轮select又从“case x := <-c1”这个分支开始evaluate,由于c1被close,按照前面的知识,close的channel不会阻塞,我们会读出这个 channel对应类型的零值,这里就是0;select再次输出0;这时即便c2有值返回,程序也不会走到c2这个分支@H_502_38@ – 依次类推,程序无限循环的输出0

我们利用nil channel来改进这个程序,以实现我们的意图,代码如下:

//testnilchannel.go@H_502_38@ package main

import "fmt"@H_502_38@ import "time"

func main() {@H_502_38@ var c1,make(chan int)@H_502_38@ go func() {@H_502_38@ time.Sleep(time.Second * 5)@H_502_38@ c1 <- 5@H_502_38@ close(c1)@H_502_38@ }()

go func() {@H_502_38@ time.Sleep(time.Second * 7)@H_502_38@ c2 <- 7@H_502_38@ close(c2)@H_502_38@ }()

for {@H_502_38@ select {@H_502_38@ case x,ok := <-c1:@H_502_38@ if !ok {@H_502_38@ c1 = nil@H_502_38@ } else {@H_502_38@ fmt.Println(x)@H_502_38@ }@H_502_38@ case x,ok := <-c2:@H_502_38@ if !ok {@H_502_38@ c2 = nil@H_502_38@ } else {@H_502_38@ fmt.Println(x)@H_502_38@ }@H_502_38@ }@H_502_38@ if c1 == nil && c2 == nil {@H_502_38@ break@H_502_38@ }@H_502_38@ }@H_502_38@ fmt.Println("over")@H_502_38@ }

$go run testnilchannel.go@H_502_38@ 5@H_502_38@ 7@H_502_38@ over

可以看出:通过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。

七、Timers

1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

func worker(start chan bool) {@H_502_38@ timeout := time.After(30 * time.Second)@H_502_38@ for {@H_502_38@ select {@H_502_38@ // … do some stuff@H_502_38@ case <- timeout:@H_502_38@ return@H_502_38@ }@H_502_38@ }@H_502_38@ }

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

func worker(start chan bool) {@H_502_38@ heartbeat := time.Tick(30 * time.Second)@H_502_38@ for {@H_502_38@ select {@H_502_38@ // … do some stuff@H_502_38@ case <- heartbeat:@H_502_38@ //… do heartbeat stuff@H_502_38@ }@H_502_38@ }@H_502_38@ }

Ubuntu Server 14.04安装docker

技术志暂无评论

近期在研究docker这一轻量级容器引擎,研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从Ubuntu搬到了Mac Air上,对Mac OS X的一切均不甚熟悉,给docker研究带来了不便,于是打算在VirtualBox中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程,主要为了备忘,如果能给其他人带来帮助,我会甚感欣慰。

docker官方对ubuntu支持是蛮好的。docker对Linux内核版本有要求,要>=3.8,Ubuntu Server目前最新版本14.04.1恰符合这一要求,其kernel version = 3.13.0-32。

一、VirtualBox安装Ubuntu Server 14.04.1

VirtualBox安装Ubuntu OS做过了不止一遍,即便是换成最新的14.04.1 Server版,差别也没有太多,无非是按照安装提示,逐步Next。这里给Ubuntu Server 14.04分配了1G Memory,32G动态硬盘空间。

【配置源】

默认情况下,/etc/apt/sources.list中只有一组源:cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求,于是我把我常用的sohu源加入sources.list中,并且放在前面:

deb http://mirrors.sohu.com/ubuntu/ trusty main restricted@H_502_38@ deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted@H_502_38@ deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted@H_502_38@ deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted@H_502_38@ deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

deb-src http://mirros.sohu.com/ubuntu/ trusty main restricted@H_502_38@ deb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted@H_502_38@ deb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted@H_502_38@ deb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted@H_502_38@ deb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

公司采用代理访问外网,于是还得在/etc/apt/apt.conf中加上代理的设置,否则无法更新源,也就无法安装第三方软件:

Acquire::http::Proxy "http://username:passwd@proxyhost:proxyport";

【乱码处理】

由于安装时候选择了中国区域(locale zh_CN.UTF-8),因此在VirtualBox的窗口中直接执行命令的提示信息可能是乱码。对于Server,我们一般是不会直接通过其主机显示登录使用的,都是通过终端访问,但在未安装和开启ssh服务和未配置端口转发前,我们只能先凑合这个窗口了。可先将/etc/default /locale中的LANGUAGE由"zh_CN:zh"改为"en_US:en", logout后重新登录就可以看到非乱码的英文提示信息了。

【安装VirtualBox增强组件】

Ubuntu Server默认是不安装图形桌面的,只有一个命令行窗口,连鼠标都无法使用。因此增强组件安装的意义没有桌面系统那么强烈。我能想到的只有“共享目录”这一个功能有些用处。

安装方法也不难,按下面步骤逐步操作即可:

sudo apt-get install build-essential linux-headers-$(uname -r) dkms gcc g++@H_502_38@ sudo mnt /dev/cdrom /mnt@H_502_38@ cd /mnt@H_502_38@ sudo bash ./VBoxLinuxAdditions.run

如果结果都是"done",重启后就ok了。

【安装ssh服务】

ssh服务由openssh-server提供:@H_502_38@ sudo apt-get openssh-server@H_502_38@ @H_502_38@ 安装成功后,ssh server服务就会自动启动起来。

不过我们还是需要修改一些配置,比如允许Root登录:打开/etc/ssh/sshd_config,将PermitRootLogin后面的内容改为yes。@H_502_38@ @H_502_38@ 【设置端口转发】

前面说过,对于Server,我们更多是在其他主机上通过ssh或telnet远程访问该Server并执行各种操作。由于这里是VirtualBox安 装的虚拟机,其他主机无法看到这台Server,我们需要设置端口转发将外部访问的数据转发给这个内部虚拟Server。

我们通过VirtualBox软件提供的图形界面即可完成这个操作:@H_502_38@ 1、“设置”这个虚拟机@H_502_38@ 2、在“网络”标签中,点击“端口转发”按钮,进入端口转发规则添加窗口。@H_502_38@ 3、添加一条规则:@H_502_38@ 名称:ssh-rules@H_502_38@ 协议:TCP@H_502_38@ 主机IP、子系统IP可以为空。@H_502_38@ 主机端口:2222@H_502_38@ 子系统端口:22@H_502_38@ 4、配置结束

配置结束后,我们在宿主机上netstat -an|grep 2222,可以看到VirtualBox增加了该端口2222的监听。

现在我们就可以在其他机器上通过ssh -l tonybai 宿主机ip -p 2222的方式登录到我们新安装的这台虚拟Server了。

@H_502_38@ 二、安装docker

docker目前的最新版本号是1.2.0,但14.04源中的docker还是正式稳定版1.0之前的版本,显然这是无法满足我的要求的。我们只能另外添加docker源来安装最新版docker。

【安装docker】

我们在/etc/apt/sources.list中加入下面这个源:@H_502_38@ debhttp://mirror.yandex.ru/mirrors/docker/docker main@H_502_38@ @H_502_38@ 执行apt-get update。

sudo apt-get install lxc-docker

正在读取软件包列表… 完成@H_502_38@ 正在分析软件包的依赖关系树@H_502_38@ 正在读取状态信息… 完成@H_502_38@ 将会安装下列额外的软件包:@H_502_38@ aufs-tools cgroup-lite git git-man liberror-perl lxc-docker-1.2.0@H_502_38@ 建议安装的软件包:@H_502_38@ git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk@H_502_38@ gitweb git-arch git-bzr git-cvs git-mediawiki git-svn@H_502_38@ 下列新软件包将被安装:@H_502_38@ aufs-tools cgroup-lite git git-man liberror-perl lxc-docker lxc-docker-1.2.0@H_502_38@ 升级了 0 个软件包,新安装了 7 个软件包,要卸载 0 个软件包,有 59 个软件包未被升级。@H_502_38@ 需要下载 7,477 kB 的软件包。@H_502_38@ 解压缩后会消耗掉 35.4 MB 的额外空间。@H_502_38@ 您希望继续执行吗? [Y/n] y

这个源里的docker居然是最新版。于是安装之。安装后,我们执行docker version来确认一下安装是否成功。

tonybai@ubuntu-Server-14:~$ docker version@H_502_38@ Client version: 1.2.0@H_502_38@ Client API version: 1.14@H_502_38@ Go version (client): go1.3.1@H_502_38@ Git commit (client): fa7b24f@H_502_38@ OS/Arch (client): linux/amd64@H_502_38@ 2014/09/26 13:56:53 Get http:///var/run/docker.sock/v1.14/version: dial unix /var/run/docker.sock: permission denied

【为docker设置http代理】

在公司内使用代理才能访问到外网,于是我们也需要为docker命令设置代理以使其顺利执行命令。

我们安装的docker实际上分为两部分,docker命令行和docker daemon。两者是C/S结构,docker命令行将用户的请求转发给docker daemon,后者会真正与外部通信完成各种操作。

于是我们可以这样为docker daemon设置http_proxy:@H_502_38@ sudo service docker stop@H_502_38@ sudo http_proxy='http://user:passwd@proxyhost:port' docker -d &

这样设置启动后,我们可以通过下面命令测试设置是否ok:

sudo docker search ubuntu

如果你看到下面信息,说明设置成功了:

tonybai@ubuntu-Server-14:~$ sudo docker search ubuntu [info] GET /v1.14/images/search?term=ubuntu [b36518a9] +job search(ubuntu) [b36518a9] -job search(ubuntu) = OK (0) NAME DESCRIPTION STARS OFFICIAL AUTOMATED ubuntu Official Ubuntu base image 709 [OK] dockerfile/ubuntu Trusted automated Ubuntu (http://www.ubunt… 24 [OK] crashsystems/gitlab-docker A trusted,regularly updated build of GitL… 20 [OK] ubuntu-upstart Upstart is an event-based replacement for … 13 [OK] … ….

猜你在找的Go相关文章