Go语言学习笔记


Go 学习笔记 ⮝⮝ⶆ₀犃⭉⭉☱‪ 前⾔ 暂时不知道写啥 …… 下载地址: 本书不定期更新,可以到 github.com/qyuhen 下载最新版。 联系⽅式: email: qyuhen@hotmail.com weibo: http://weibo.com/qyuhen QQ: 1620443 2 更新记录 2012-01-11 开始学习 GoLang。 2012-01-15 本笔记第⼀版  (基于 release.r60.3 9516)。 2012-01-17 更正 interface 和 method 上的⼀些错误。 2012-01-19 更新⽅法集、接⼝和反射的⼀些内容。重新校验⽂字错误。 2012-01-28 更新更多细节。 2012-01-30 增加指针内容。 2012-02-03 新增部分内容。 2012-02-29 增加指针转换内容。 (基于 weekly.2012-02-22) 2012-03-05 增加包⽂档。 (基于 weekly.2012-03-04) 2012-03-17 按照 RC1 修改部分语⾔变更。 (基于 weekly.2012-03-13) 2012-03-25 依照 RC2 和 《 The Way to Go》调整内容。 (基于 weekly.2012-03-22) 2012-03-29 Go1 Released! 2012-04-01 增加 Command 内容。 2012-04-22 添加标准库内容。 2012-04-25 新增附录。 2012-06-08 陆续增加标准库内容。 2012-06-15 升级到 1.0.2。 2012-07-11 增加扩展库内容,并修正⼀些错误。 2012-07-25 更新并修正第⼀部分内容,并校对⽂字描述。 2012-09-04 修正局部⽂字错误。 2012-11-22 增加反射⽅法调⽤,⽂字校对。 2012-11-27 增加 interface、 cgo 内容。 2012-11-28 修正 interface 的局部内容。 2012-12-02 增加 struct 匿名⽅法、 gdb 调试等内容。 2012-12-09 增加⺫录结构和导⼊的局部内容。 2012-12-10 完善 GDB 调试内容。 2012-12-15 调整了⼀下版⾯。 2012-12-20 细微调整。 2012-12-28 增加性能测试。 2012-12-29 增加 gccgo 编译说明。 2013-01-05 增加 go build gcflags。 2013-01-10 更新 slice、 method 部分内容。 (⼀年了 ) 2013-01-18 细微调整。 2013-01-31 修正 des 错误。 3 ⺫录 第⼀部分 Go 语⾔ ! 9 第 1 章 基础 ! 10 1.1 变量 10 1.2 基本类型 11 1.3 类型转换 13 1.4 常量 13 1.5 字符串 15 1.6 运算符 20 1.7 指针 21 1.8 保留字 23 1.9 控制结构 23 1.10 ⾃定义类型 27 1.11 初始化 29 1.12 内置函数 30 第 2 章 函数 ! 31 2.1 函数类型 31 2.2 多返回值、命名返回参数 32 2.3 变参 33 2.4 匿名函数、闭包 33 2.5 Defer 35 2.6 Panic、 Recover 37 2.7 Call Stack 38 第 3 章 Array、 Slices 和 Maps! 40 3.1 Array 40 4 3.2 Slices 42 3.3 Maps 46 第 4 章 Structs! 50 4.1 定义 50 4.2 初始化 51 4.3 匿名字段 52 4.4 ⽅法 56 4.5 内存布局 61 4.6 字段标签 62 第 5 章 接⼝ ! 64 5.1 接⼝定义 64 5.2 执⾏机制 65 5.3 匿名字段⽅法 69 5.4 空接⼝ 70 5.5 类型推断 71 5.6 接⼝转换 72 第 6 章 并发 ! 73 6.1 Goroutine 73 6.2 Channel 75 第 7 章 程序结构 ! 84 7.1 源⽂件 84 7.2 包 85 7.3 测试 88 第 8 章 进阶 ! 92 8.1 运⾏时 92 8.2 内存分配 92 5 8.3 内存布局 93 8.4 反射 96 8.5 cgo 99 第 9 章 ⼯具 ! 101 9.1 命令⾏⼯具 101 9.2 GDB 调试 104 9.3 条件编译 107 9.4 跨平台编译 108 9.5 性能测试 108 第⼆部分 标准库 ! 110 第 10 章 io! 111 10.1 Interface 111 10.2 Text File 111 10.3 Binary File 112 10.4 Pipe 113 10.5 Encoding 114 10.6 Bufer 115 10.7 Temp 116 10.8 Path 117 第 11 章 strings! 121 11.1 strconv 121 11.2 strings 122 11.3 template 122 11.4 regexp 127 第 12 章 compress! 132 12.1 zlib 132 6 12.2 zip 133 第 13 章 flag! 135 第 14 章 crypto! 138 14.1 md5 138 14.2 des 138 14.3 rsa 139 第 15 章 encoding! 144 15.1 json 144 第 16 章 net! 147 16.1 tcp 147 16.2 http 148 16.3 rpc 152 第 17 章 syscall! 155 17.1 fork 155 17.2 daemon 156 第 18 章 time! 158 18.1 Time 158 18.2 Duration 159 18.3 Timer 160 第 19 章 sync! 162 19.1 Locker 162 19.2 Cond 164 19.3 Once 166 19.4 WaitGroup 167 19.5 atomic 168 第 20 章 os! 169 7 20.1 System 169 20.2 Environ 170 20.3 Process 171 20.4 Signal 173 20.5 User 175 第三部分 扩展库 ! 176 mgo - MongoDB Driver! 177 snappy - Compress/Decompress Library! 182 附录 ! 183 8 第⼀部分 Go 语⾔ 暂时不 知道写啥 …… 代码测试环境: • Go Version 1.0.3 • MacBook Pro, 4GB, Mac OS X Lion 10.8.2 9 第 1 章 基础 1.1 变量 使⽤ var 定义变量,⾃动初始化为零值  (Zero Value)。 • bool: false • integers: 0 • floats: 0.0 • string: "" • pointers, functions, interfaces, slices, channels, maps: nil 变量类型总是放在变量名后⾯,但可以省略  (根据初始化值进⾏类型推断 )。 Go 是强类型语⾔,类型 推断只是⼀种简便的代码语法糖,不同于 Javascript、 Python,我们不能修改变量的类型。 var a = 1234 var b string = "hello" var c bool func main() { println(a, b, c) } 在函数内部,甚⾄可以省略 var 关键字,⽤ ":=" 这种更短的表达式完成变量类型推断和初始化。 a := 1 可以⼀次声明多个变量,并对其赋予初始值。 var x, y int // 类型相同的多个变量 var ( // 类型不同的多个变量 a int b bool ) var c, d int = 1, 2 // 指定类型,多个变量类型相同。 var e, f = 123, "hello"// ⾃动推断,多个变量类型按初始值推断。 g, h := 123, "hello" // ⾃动推断,多个变量类型按初始值推断。 多变量赋值时,将先计算所有相关值,然后 left-to-right 进⾏变量赋值。 sa := []int{1, 2, 3} i := 0 i, sa[i] = 1, 2 // sets i = 1, sa[0] = 2 sb := []int{1, 2, 3} j := 0 10 sb[j], j = 2, 1 // sets sb[0] = 2, j = 1 println(sb[j], j) sc := []int{1, 2, 3} sc[0], sc[0] = 1, 2 // sets sc[0] = 1, then sc[0] = 2 (so sc[0] = 2 at end) println(sc[0]) 和 Python ⼀样, Go 也有个⽤来当垃圾桶的特殊变量 "_" (只能写,不能读 )。 func test() (int, string) { return 123, "abc" } func main() { a, _ := test() println(a) } 编译器会认为⼀个未被使⽤的变量和导⼊包是个错误。 package main import ( "fmt" // Error: imported and not used: fmt ) func main() { var a = 1 // Error: a declared and not used } 1.2 基本类型 Go 对整数进⾏了更明确的规划,清晰明了。另外⼀个不同于 C 的地⽅,就是引⽤类型这种更加合 理的⽅式代替指针。当然,指针还是有的。 类型 ⻓度 说明 bool 1 true, false。不能把⾮零值当作 true。 byte 1 uint8 别名 rune 4 int32 别名。代表⼀个 Unicode Code Point。 int/uint 依据所运⾏的平台,可能是 32bit 或 64bit。 int8/uint8 1 -128 ~ 127; 0 ~ 255 int16/uint16 2 -32768 ~ 32767; 0 ~ 65535 int32/uint32 4 -21亿 ~ 21亿 , 0 ~ 42亿 11 类型 ⻓度 说明 int64/uint64 8 float32 4 精确到 7 个⼩数位 float64 8 精确到 15 个⼩数位 complex64 8 complex128 16 uintptr ⾜够保存指针的 32 位或 64 位整数 array 值类型 如 :[2]int struct 值类型 string 值类型 slice 引⽤类型 如 : []int map 引⽤类型 channel 引⽤类型 interface 接⼝类型 function 函数类型 可以⽤ ⼋进制  (071)、⼗六进制  (0xFF) 或 科学计数法  (1e2) 对整数类型进⾏赋值。 math 包中包 含了 Min/Max 开头的各类型最⼩、最⼤值常量。 引⽤类型  (slice、 map、 channel) 的默认值为 nil,必须使⽤ make() 或初始化表达式分配内存,⾏ 为类似 C++/C# 的引⽤。 func main() { var m map[string]int m["a"] = 1 fmt.Println(m) } 输出 : panic: runtime error: assignment to entry in nil map 不能隐式将⾮零值当作 bool true,或零值、 nil 当作 bool false。 func main() { var a = 10 12 //if a { println("True") } // Error: non-bool a (type int) used as if condition if a > 0 { println("True") } } Go 总是按照值传递  (pass-by-value),就算是引⽤和指针本⾝也是值拷⻉。 1.3 类型转换 不⽀持隐式转换 ,必须进⾏显式类型转换 "(expression)"。 func main() { a := 0x1234 b := 1234.56 c := 256 fmt.Printf("%x\n", uint8(a)) // 截短⻓度 fmt.Printf("%d\n", int(b)) // 截断⼩数 fmt.Printf("%f\n", float64(c)) } 输出 : 34 1234 256.000000 注意下⾯容易造成误解的例⼦,转换优先级⽐这些运算符更⾼,因此该⽤括号的时候别嫌⿇烦。 *Point(p) // same as *(Point(p)) (*Point)(p) // p is converted to (*Point) <-chan int(c) // same as <-(chan int(c)) (<-chan int)(c) // c is converted to (<-chan int) 1.4 常量 常量必须是编译期能确定的 Number (char/integer/float/complex)、 String 和 bool 类型。可以 在函数内定义局部常量。 const x uint32 = 123 const y = "Hello" const a, b, c = "meat", 2, "veg" // 同样⽀持⼀次定义多个常量。 const ( z = false a = 123 ) 不⽀持 C/C++ 0x12L、 0x23LL 这样的类型后缀。 在定义常量组时,如不提供初始化值,则表⽰与上⾏常量的类型、值表达式完全相同 (是表达式相同 ⽽⾮结果相同 )。 13 func main() { const ( a = "abc" b ) println(a, b) } 输出 : abc abc 常量值还可以是 len()、 cap(), unsafe.Sizeof() 常量计算表达式的值。 const ( a = "abc" b = len(a) c = unsafe.Sizeof(a) ) 如果⺫标类型⾜以存储常量值,不会导致溢出  (overflow),可不做显式转换。 var million int = 1e6 // float syntax OK here var b byte = 'a'// int to byte 1.4.1 枚举 可以⽤ iota ⽣成从 0 开始的⾃动增⻓枚举值。按⾏递增,可以省略后续⾏的 iota 关键字。 const ( Sunday = iota// 0 Monday // 1 Tuesday // 2 Wednesday // 3 Thursday // 4 Friday // 5 Saturday // 6 ) 看⼀个常⽤的使⽤案例,后续⾏的类型和值表达式与 KB 相同,只有 iota 值发⽣变化。 type ByteSize int64 const ( _ = iota // 忽略 KB ByteSize = 1 << (10 * iota) MB GB TB PB ) 14 func main() { println(GB, GB / 1024 / 1024 / 1024) } 输出 : 1073741824 1 可以在同⼀⾏使⽤多个 iota,它们各⾃增⻓。 const ( A, B = iota, iota C, D E, F ) func main() { println(C, D, E, F) } 输出 : 1 1 2 2 如果某⾏不想递增,可单独提供初始值。不过想要恢复递增,必须再次使⽤ iota。 func main() { const ( a = iota// 0 b// 1 c// 2 d = "ha"// 独⽴值 e// 没有使⽤ iota 恢复,所以和 d 值相同 f = 100 g// 和 f 值相同 h = iota// 7, 恢复 iota couter,继续按⾏递增。 i// 8 ) println(a, b, c, d, e, f, g, h, i) } 输出 : 0 1 2 ha ha 100 100 7 8 1.5 字符串 string 是 不可变值类型 ,内部使⽤指针指向⼀个 UTF-8 byte 数组。变⻓编码⽅式,每个 Unicode 字符可能需要 1 ~ 4 个 byte 存储,不同于 Java/C#/Python 这类语⾔使⽤ UTF-16/UTF-32 固定 宽度编码。 • 值类型,默认值为空字符串 ""。 • 可以⽤索引号访问某个字节,如 s[i]。 • 不能通过序号获取字节元素指针, &s[i] 是⾮法的。 15 • 不可变类型,⽆法直接修改字节数组内容。 • 字节数组不包括 NULL (\0) 结尾。 内存结构 : src/pkg/runtime/runtime.h 146struct String 147{ 148 byte*str; 149 int32len; 150}; 单引号字符常量表⽰⼀个 Unicode 字符  (rune),⽀持 \uFFFF、 \UFFFFFFFF 和 \xFF 格式字符。 func main() { b := [10]byte{} b[1] = 'a' b[0] = '我 ' fmt.Println(b) } 输出 : main.go: constant 25105 overflows uint8 make: *** [_go_.6] Error 1 使⽤符号 ` 定义原始是字符串  (Raw String),不进⾏转义处理,⽀持跨⾏。 s := `ab\c` 使⽤ "+" 连接跨⾏字符串时需要注意,连接符号 "+" 必须在上⼀⾏尾部,否则会导致编译错 误  (Error: invalid operation: + ideal string)。 s := "abc" + ", 123" 标准库 strings 包提供了 Join() 等常⽤的各种字符串操作函数。 import ( "strings" ) func main() { a, b, c := "a", "b", "c" s := strings.Join([]string{ a, b, c }, "") println(s) } 还可以使⽤ bytes.Bufer 来完成 C# StringBuilder 的活。 func main() { sb := bytes.Buffer{} sb.WriteString("Hello ") 16 sb.WriteString("World") sb.WriteString("!") fmt.Println(sb.String()) } 输出 : Hello World! ⽀持切⽚操作,返回⼦串 (string),⽽⾮ slice。 func main() { s := "abcdefg" sub := s[1:3] fmt.Printf("%T = %v\n", sub, sub) } 输出 : string = bc 要修改字符串,需要显式转换成 []byte 或 []rune。 string(0x1234) // == "\u1234" string([]bytes)// bytes -> string string([]runes)// Unicode ints -> UTF-8 string []byte(string) // UTF-8 string -> bytes []rune(string) // UTF-8 string -> Unicode ints 转换成 []byte 后,可以修改字节内容,不过还需转换回来。 func main() { s := "abc" var c byte = s[1] // 按索引号访问 fmt.Printf("%c, %02x\n", c, c) bs := []byte(s) // 转换为 bytes,以便修改 bs[1] = 'B' println(string(bs)) } 输出 : b, 62 aBc 看看下⾯的转换演⽰。 func main() { // UTF-8 编码格式存储的字符串 s := "a:中国⼈ " println("utf-8 string: ", s, len(s)) 17 /* --- string to bytes --------------------------------------- */ // 转换成 bytes,以便于修改 bs := []byte(s) bs[0] = 'A' for i := 0; i < len(bs); i++ { fmt.Printf("%02x ", bs[i]) } // 将 bytes 转换为 string println(string(bs)) /* --- string to unicode ------------------------------------- */ // 转换成 Unicode u := []rune(s) println("unicode len =", len(u)) // 显⽰ Unicode CodePoint for i := 0; i < len(u); i++ { fmt.Printf("%04x ", u[i]) } // 按照 unicode 修改 u[4] = '⻰ ' // 将 unicode 转换为 string println(string(u)) } 输出 : utf-8 string: a:中国⼈ 11 41 3a e4 b8 ad e5 9b bd e4 ba ba A:中国⼈ unicode len = 5 0061 003a 4e2d 56fd 4eba a:中国⻰ 使⽤ for 遍历字符串时,需要注意 byte 和 rune 的区别。 func main() { s := "中国⼈ " for i := 0; i < len(s); i++ { fmt.Printf("%c\n", s[i]) } for i, c := range s { fmt.Printf("%d = %c\n", i, c) } } 输出 : ä 18 ¸ ­ å ½ ä º º 0 = 中 3 = 国 6 = ⼈ 字符串编码⽅案 字符串编码⽅案是个即简单⼜⿇烦的话题,这主要涉及到字节 (byte) 和字符 (character) 两个不同的概念。 字节是计算机软件系统的最⼩存储单位,我们可以⽤ n 个字节表达⼀个概念,诸如 4 字节的 32 位整数或着其 他什么对象之类的,任何对象最终都是通过字节组合来完成存储的。字符也是⼀种对象,它和具体的语⾔有 关,⽐如在中⽂⾥, "我 " 是⼀个字符,不能⽣⽣将其当作⼏个字节的拼接。 早期的计算机系统编码⽅案主要适应英⽂等字⺟语⾔体系, ASCII 编码⽅案的容量⾜以表达所有的字符,于是 每个字符占⽤ 1 个字节被固定下来,这也造成了⼀定程度上的误解,将字符和字节等同起来。要知道,这个世 界上并⾮只有英⽂⼀种字符,中⽂等东亚语⾔体系的字符数量就⽆法⽤ ASCII 容纳,⼈们只好⽤ 2 个或者更 多的字节来表达⼀个字符,于是就有了 gb2312、 big5 等等种类繁多且互不兼容的编码⽅案。 随着计算机技术在世界范围内的⼲泛使⽤,国际化需求越发重要,以往的字符编码⽅案已经不适应现代软件开 发。于是国际标谁化组织 (ISO) 在参考很多现有⽅案的基础上统⼀制定了⼀种可以容纳世界上所有⽂字和符号 的字符编码⽅案,这就是 Unicode 编码⽅案。 Unicode 使得我们在⼀个系统中可以同时处理和显⽰不同国家 的⽂字。 Unicode 本质上就是通过特定的算法将不同国家不同语⾔的字符都映射到数字上。⽐如中⽂字符 "我 " 对应的 数字是 25105 (\u6211)。 Unicode 字符集 (UCS, Unicode Character Set) 有 UCS-2、 UCS-4 两种标准。 其中 UCS-2 最多可以表达 U+FFFF 个字符。⽽ UCS-4 则使⽤ 4 字节编码,⾸位固定为 0,也就是说最多可 以有 2^31 个字符,⼏乎容纳了全世界所有的字符。 Unicode 只是将字符和数字建⽴了映射关系,但对于计算机⽽⾔,要存储和操作任何数据,都得⽤字节来表 ⽰,这其中还涉及到不同计算机架构的⼤⼩端问题 (Big Endian, Little Endian)。于是有了⼏种将 Unicode 字 符数字转换成字节的⽅法: Unicode 字符集转换格式 (UCS Transformation Format),缩写 UTF。 ⺫前常⽤的 UTF 格式有: 19 • UTF-8: 1 到 4 字节不等⻓⽅案,⻄⽂字符通常只⽤⼀个字节,⽽东亚字符 (CJK) 则需要三个字节。 • UTF-16: ⽤ 2 字节⽆符号整数存储 Unicode 字符,与 UCS-2 对应,适合处理中⽂。 • UTF-32: ⽤ 4 字节⽆符号整数存储 Unicode 字符。与 UCS-4 对应,多数时候有点浪费内存空间。 UTF-8 具有⾮常良好的平台适应性和交互能⼒,因此已经成为很多操作系统平台的默认存储编码⽅案。⽽ UTF-16 因为等⻓,浪费空间较少,拥有更好的处理性能,包括 .NET、 JVM 等都使⽤ 2 字节的 Unicode Char。 另外,按照⼤⼩端划分, UTF ⼜有 BE 和 LE 两种格式,⽐如 UTF-16BE、 UTF-16LE 等。为了让系统能⾃动 识别出 BE 和 LE,通常在字符串头部添加 BOM 信息, "FE FF" 表⽰ BE, "FF FE" 表⽰ LE。后⾯还会有⼀两个 字符⽤来表⽰ UTF-8 或 UTF-32。 摘⾃ : http://www.rainsts.net/article.asp?id=906  (⺴站已死,权作纪念 ) 1.6 运算符 Go运算符全部是从左到右结合  (总算从 C 泥沼⾥爬起来了 ) 优先级 运算符 说明 ⾼ * / % << >> & &^ . + - | ^ . == != < <= > >= . <- channel 运算符 . && 低 || 位运算符: 6:0110 11:1011 ----------------- &0010= 2 AND, 都为 1 |1111= 15 OR, ⾄少⼀个为 1 ^1101= 13 XOR, 只能⼀个为 1 &^0100= 4 AND NOT, bit clear。从 a 上清除所有 b 的标志位。 11 在 0、 1、 3 上设置了标志,检查 6 这三个⼆进制位,如果是 1 就改为 0 即可。 演⽰ : func main() { a := 0 20 a = a | (1 << 3)// 在 bit3 上设置标志位 (从 bit0 开始算 ) a = a | (1 << 6)// 在 bit6 上设置标志位 println(a) // a = 72 = 0100 1000 a = a &^ (1 << 6)// 清除 bit6 上的标志位, a = 8 = 0000 0100 println(a) } 不⽀持运算符重载  (某些内建类型,如 string 做了 "+" 运算符重载 )。 在 Go 中 ++、 -- 是 语句 (statement) ⽽⾮ 表达式 (expression)。另外, "*p++" 表⽰ "(*p)++" ⽽⾮ "*(p++)" 。 func main() { i := 0 p := &i //a := i++ // syntax error: unexpected ++, expecting semicolon or newline or } i++ a := i //b := (*p)++ // syntax error: unexpected ++, expecting semicolon or newline or } *p++ b := *p println(a, b) } Go 所有的操作符和分隔符都在这了。 +&+=&=&&==!=() -|-=|=||<<=[] *^*=^=<->>={} /<>%=>>=--!....: &^&^= 1.7 指针 Go 保留了指针, *T 表⽰ T 对应的指针类型。如果包含包名,则应该是 "*.T"。代表指 针类型的符号 "*" 总是和类型放在⼀起,⽽不是紧挨着变量名。同样⽀持指针的指针 (**T)。 可以确保下⾯两个变量都是指针类型,没有 C/C++ ⾥⾯那些弯弯绕的注意事项。 var a, b *int • 操作符 "&" 取变量地址,⽤ "*" 透过指针变量间接访问⺫标对象。 • 默认值是 nil,没有 NULL 常量。 21 • 不⽀持指针运算,不⽀持 "->" 运算符,直接⽤ "." 选择符操作指针⺫标对象成员。 • 可以在 unsafe.Pointer 和任意类型指针间进⾏转换。 • 可以将 unsafe.Pointer 转换为 uintptr,然后做变相指针运算。 uintptr 可以转换为整数。 type User struct { Id int Name string } func main() { i := 100 var p *int = &i // 取地址 //p++ // invalid operation: p += 1 (mismatched types *int and int) println(*p) // 取值 up := &User{ 1, "Jack" } up.Id = 100// 直接操作指针对象成员 fmt.Println(up) u2 := *up // 拷⻉对象 u2.Name = "Tom" fmt.Println(up, u2) } 输出 : 100 &{100 Jack} &{100 Jack} {100 Tom} 通过 unsafe.Pointer 在不同的指针类型间转换,做到类似 C void* 的⽤途。 const N int = int(unsafe.Sizeof(0)) func main() { x := 0x1234 p := unsafe.Pointer(&x) // *int -> Pointer p2 := (*[N]byte)(p) // Pointer -> *[4]int,注意 slice 的内存布局和 array 是不同的。 // 数组类型元素⻓度必须是常量。 for i, m := 0, len(p2); i < m; i++ { fmt.Printf("%02X ", p2[i]) } } 输出 : 34 12 00 00 将 unsafe.Pointer 转换成 uintptr,变相做指针运算。当然,还得⽤ Pointer 转换回来才能取值。 type User struct { Id int Name string 22 } func main() { p := &User{ 1, "User1" } var np uintptr = uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.Name) var name *string = (*string)(unsafe.Pointer(np)) println(*name) } 输出 : User1 如果基于安全需要,⽤ -ldfalgs "-u" 参数阻⽌编译 unsafe 代码。 1.8 保留字 Go 的保留字不多,整个语⾔设计相当简洁。 break default func interfaceselect case defer go map struct chan else goto package switch const fallthroughif range type continuefor import return var 1.9 控制结构 Go 代码控制结构⾮常简洁,只有 for、 switch、 if。 1.9.1 IF if 语句只需记住: • 条件表达式没有括号; • ⽀持⼀个初始化表达式 (可以是多变量初始化语句 ); • 左⼤括号必须和条件语句在同⼀⾏。 func main() { a := 10 if a > 0 { // 左⼤括号必须写在这,否则被解释为 "if a > 0;" 导致编译出错。 a += 100 } else if a == 0 { // 注意左⼤括号位置。 a = 0 } else { // 注意左⼤括号位置。 a -= 100 } 23 println(a) } ⽀持单⾏模式。 if a > 0 { a += 100 } else { a -= 100 } 初始化语句中定义的都是 block 级别的局部变量,不能在 block 外⾯使⽤ (else 分⽀内有效,但会 隐藏 block 外的同名变量 )。 if err := file.Chmod(0664); err != nil { log.Stderr(err) return err } 很遗憾, Go 不提供三元操作符 "?:",也没办法⽤ Python "and or",因为不是 bool。 Go 编译器有个⼩⼩的 Bug,下⾯的代码会引发编译错误 (或许是有意这么做的 )。 func test(b bool) string { if b { return "Y" } else { return "N" } } 输出 function ends without a return statement 只能改成下⾯这样。 func test(b bool) string { if b { return "Y" } return "N" } 1.9.2 For for ⽀持三种形式: for init; condition; post {} for condition {} for {} 初始化和步进表达式可以是多个值。 24 for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } 每次循环都会重新检查条件表达式,如果达表达式包含函数调⽤,则可能被执⾏多次。建议⽤初始 化表达式⼀次性计算。 func main() { l := func(s string) int { println("get length") return len(s) } ss := "abcd" for i := 0; i < l(ss); i++ { println(ss[i]) } println("---------") for i, m := 0, l(ss); i < m; i++ { println(ss[i]) } } 输出 : get length 97 get length 98 get length 99 get length 100 get length --------- get length 97 98 99 100 for 循环和保留字 range ⼀起使⽤,完成 迭代器 (iterator) 操作。除了 array, range 表达式是在循 环前预先计算好的,在此期间可以安全修改迭代器对象。 func main() { for i, c := range "abc" { fmt.Printf("s[%d] = %c\n", i, c) } } 输出 : 25 s[0] = a s[1] = b s[2] = c string、 array、 slice、 map、 channel 都可以⽤ range 进⾏迭代器操作。如果不想要序号,可以 ⽤ "_" 这个特殊变量。或者只要序号。 1.9.3 Switch switch case 表达式可以使⽤任意类型或表达式。也不必写 break,⾃动终⽌。如希望继续下⼀ case 处理,则须显式执⾏ fallthrough。 func main() { switch a := 5; a { case 0, 1: // 逗号指定多个分⽀ println("a") case 100: // 什么都不做。不是 fallthrough case 5: // 多⾏代码,⽆需使⽤ {}。 println("b") fallthrough// 进⼊后续 case 处理 default: println("c") } } 输出 : b c 不指定 switch 条件表达式,或直接为 true 时,可⽤于替代 if...else if...else...。 func main() { a := 5 switch { case a > 1: println("a") case a > 2: println("b") default: println("c") } } 可以使⽤初始化表达式进⼀步简化上⾯的代码。 func main() { switch a := 5; { ... } } 1.9.4 Goto, Break, Continue 26 标签名区分⼤⼩写,未经使⽤的标签也会引发编译错误。 ⽀持函数内部 goto 跳转, continue 进⼊⼀下次循环, break 终⽌循环。 break 和 continue 都可以配合标签,在多级嵌套循环间跳出。这和 goto 调整执⾏位置完全不同。 下⾯例⼦⽤ goto 代替 break 就成死循环了。通常建议往后 goto,避免死循环。 func main() { LABEL1: for { for i := 0; i < 10; i++ { if i > 2 { break LABEL1 } else { println("L1:", i) } } } LABEL2: for i := 0; i < 5; i++ { for { println("L2:", i) continue LABEL2 } } println("over") } 输出 : L1: 0 L1: 1 L1: 2 L2: 0 L2: 1 L2: 2 L2: 3 L2: 4 over 1.10 ⾃定义类型 依照类型标识符名称,可以将 Go 类型分为 "命名 (Named)" 和 "未命名 (UnNamed)" 两类。命名类 型包括 bool、 int、 string 等,⽽未命名类型有 array、 slice、 map、 channel、 funciton 等。 由于 Go 是强类型语⾔,因此就算底层类型相同的命名类型之间亦不能做隐式转换。但具有相同申 明的未命名类型则被视为同⼀种类型。 相等的未命名类型: • 具有相同元素类型和⻓度的 array。 • 具有相同元素类型的 slice。 27 • 拥有相同的字段序列 (字段名、类型、标签相同,顺序相同 ) 的匿名 struct。 • 相同基类型的 pointer。 • 拥有完全相同签名 (包括参数和返回值相同,但不包括参数名 ) 的 function。 • 拥有相同⽅法集签名 (函数名、参数签名相同,函数排列次序⽆关 ) 的 interface。 • 拥有相同 key、 value 类型的 map。 • 拥有相同数据类型和传送⽅向的 channel。 func main() { var a struct{x, y int} var b struct{x, y int} a.x = 1 a.y = 2 b = a fmt.Println(b) } 可以⽤ type 定义新的类型,以便为其添加⽅法  (⽅法定义只能⽤于同⼀个包的类型 )。 type MyInt int type IntPointer *int type Data struct { x, y int } type ITest interface { test(int)(string, int) } func main() { var x = 123 var p IntPointer = &x; println(*p) var d = Data{ 1, 2 } println(d.x, d.y) p2 := &Data{ y:200 } println(p2.x, p2.y) } type 定义的新类型 (基于其他类型 ) 并不是⼀个别名,⽽是⼀个全新的类型。除了和底层类型拥有相 同的数据结构外, 它不会 "继承 " 包括⽅法在内的任何信息 ,这与 struct 匿名字段嵌⼊完全不是⼀码 事。如果底层类型是命名类型,那么必须进⾏显式转换,才能将新类型对象转换为底层类型。 type MyInt int type MyIntSlice []int 28 func main() { var a MyInt = 10 // var b int = a // cannot use a (type MyInt) as type int in assignment var b int = int(a) // var c MyInt = b // cannot use b (type int) as type MyInt in assignment var c MyInt = MyInt(b) println(a, b, c) } ⽽当⺫标是未命名类型时,⽆需显式转换。 type MyIntSlice []int func main() { var a MyIntSlice = []int{1, 2, 3} var b []int = a println(a, b) } 1.11 初始化 初始化复合对象 (array、 slice、 map、 struct) 时,必须添加类型标识,否则编译器会把⼤括号当作 代码块处理,导致编译失败。 var d1 [2]int = [2]int{ 1 }// 太繁琐,但不能省略右边的类型。 var d2 = [2]int{ 2 } // 在函数外部需要 var。 d3 := [2]int{ 3 } // 函数内部这样最简洁。 成员初始化表达式可单⾏或多⾏。以 "," 分隔多个元素,但多⾏的最后⼀个初始化值必须以 "," 或 "}" 结尾,否则将导致语法错误。 s := []byte{ 1, 2, 4} m := map[string]int{ "a": 1, "b": 2, // 这个逗号是必须的,或以 } 结尾。 } u := struct { name string age int }{ "user1", 20, // 这个逗号是必须的。 29 } fmt.Println(s, m, u) 1.12 内置函数 内置函数不多,但都很实⽤。 • close: 关闭 channel。 • len: 获取 string、 array、 slice ⻓度, map key 的数量,以及 bufer channel 中当前可⽤数 据数量。 • cap: 获取 array ⻓度, slice 容量,以及 bufer channel 的最⼤缓冲容量。 • new: 通常⽤于值类型,为指定类型分配初始化过的内存空间,返回指针。 • make: 仅⽤于 slice、 map、 channel 引⽤类型,除了初始化内存,还负责设置相关属性。 • append: 向 slice 追加  (在其尾部添加 ) ⼀个或多个元素。 • copy: 在不同 slice 间复制数据。 • print/println: 不⽀持 format,要格式化输出,须使⽤ fmt 包。 • complex/real/imag: 复数处理。 • panic/recover: 错误处理。 30 第 2 章 函数 Go 函数不⽀持 嵌套 (nested)、重载  (overload) 和 默认参数 (default parameter),但⽀持: • ⽆需声明原型 ; • 不定⻓度变参 ; • 多返回值 ; • 命名返回值参数 ; • 匿名函数 ; • 闭包 ; 函数使⽤ func 开头,左⼤括号不能另起⼀⾏  (Go 约定所有的⼤括号规则都是这样滴, ^_^ )。 // 接收多个参数,⽆返回值。 func test(a, b int, c string) { println(a, b, c) } // 单个返回值 func add(a, b int) int { return a + b } func main() { test(1, 2, "abc") // 1 2 abc println(add(1, 2)) // 3 } 2.1 函数类型 可以定义函数类型,函数也可以作为值 (默认值 nil) 被传递。 type callback func(s string) // 定义函数类型 func test(a, b int, sum func(int, int) int){ // 接收函数类型参数,也可以直接⽤ callback 类型。 println(sum(a, b)) } func main() { var cb callback // 函数类型变量,默认值 nil。 cb = func(s string) { println(s) } cb("Hello, World!") test(1, 2, func(a, b int) int { return a + b }) // 这个写 Javascript 的⼈⽐较眼熟。 } 31 输出 : Hello, World! 3 2.2 多返回值、命名返回参数 函数可以象 Python 那样返回多个结果,只是没有 tuple 类型。对于不想要的返回值,可⽤特殊变 量 "_" 忽略。如使⽤命名返回值参数,则 return 语句可以为空。不为空时,则依旧按顺序返回多个 结果。 // 多个返回值 func swap(a, b int) (int, int) { return b, a } // 命名返回参数 func change(a, b int) (x, y int) { x = a + 100 y = b + 100 return // 也可以写成 return x, y } func main() { a, b := 1, 2 a, b = swap(a, b) // 2 1,可以⽤ _ 接收不想要的返回值 println(a, b) c, d := change(1, 2) println(c, d) // 101 102 } 命名返回参数可能会被代码块中的同名变量隐藏 (shadowed),此时就须显式 return 返回结果。 func test(a int) (x int) { if x := 10; a > 0 { return // block 变量 x 隐藏了命名返回值 x。 } return } func main() { println(test(0)) } 输出 : ./main.go: x is shadowed during return 改⽤显式 return 返回就没问题了。 32 func test(a int) (x int) { if x := 10; a > 0 { return x + 1 // 这样就没问题了。 } return// 这个不受 if block 影响。 } 函数的多返回值可以直接传递给其他函数做参数。 func args() (int, string) { return 1, "abc" } func test(i int, s string) { // 也可以⽤变参 fmt.Println(i, s) } func main() { test(args()) } 输出 : 1 abc 2.3 变参 变参本质上就是⼀个 slice,且必须是最后⼀个形参。将 slice 传递给变参函数时,注意⽤ "..." 展 开,否则就当作单个参数处理了。和 Python *args ⽅式相同。 func sum(s string, args ...int) { // 注意语法格式 var x int for _, n := range args { x += n } println(s, x) } func main() { sum("1 + 2 + 3 =", 1, 2, 3) x := []int{0, 1, 2, 3, 4} sum("0 + 1 + 2 =", x[:3]...) // 注意展开 } 2.4 匿名函数、闭包 Go 的匿名函数类似 Javascript,远⽐ Python lambda 好⽤。对闭包⽀持良好。 /* 闭包⽀持 */ 33 func closures(x int) (func(int) int) { // 返回匿名函数 return func (y int) int { return x + y } } func main() { f := closures(10) // 匿名函数,引⽤了 closures.x println(f(1)) // 11 println(f(2)) // 12 } 事实上,每次都会创建⼀个新的匿名函数对象。另外,从输出结果可以观察到闭包的 "延迟 " 现象。 func main() { var fs []func()(int) for i := 0; i < 3; i++ { fs = append(fs, func() int { return i }) } for _, f := range fs { fmt.Printf("%p = %v\n", f, f()) } } 输出: 0x42141000 = 3 0x42141040 = 3 0x42141080 = 3 闭包指向同⼀个变量,⽽不是复制。 func test(x int) (func()) { fmt.Printf("%p = %v\n", &x, x) return func() { fmt.Printf("%p = %v\n", &x, x) } } func main() { f := test(100) f() } 输出: 0x4212f100 = 100 0x4212f100 = 100 34 2.5 Defer defer 的作⽤就是向函数注册退出调⽤  (也就是说必须是函数或⽅法 ),可以看作是: func (...) { try { ...function body... } finally { defer2(...) defer1(...) return } } ⽀持匿名函数调⽤  (是调⽤,不仅仅是定义 )。多个 defer,按照 "先进后出  (FILO)" 的次序执⾏。 就算函数发⽣严重错误, defer 依然会被执⾏。 func test(a, b int) int { defer println("defer1:", a, "/", b) defer func() { println("defer2:", a, b) }() // 注意不仅仅是定义⼀个匿名函数,还要带上 "()" 进⾏调⽤。 return a / b } func main() { a := test(10, 2) b := test(10, 0) // 通过输出会发现,就算发⽣严重 runtime error, defer 依然被执⾏。 print(a, b) } 输出 : defer2: 10 2 defer1: 10 / 2 defer2: 10 0 defer1: 10 / 0 panic: runtime error: integer divide by zero [signal 0x8 code=0x7 addr=0x20c9 pc=0x20c9] 常⽤来做资源清理、关闭⽂件、解锁、记录执⾏时间等等操作。 func WriteData(file *File, mu *Mutex, data ...string) { mu.Lock() defer mu.Unlock() // write data ... } 35 既然是退出函数,⾃然可⽤于 "篡改 " 输出结果 (必须命名返回值,闭包作⽤ )。 func test(a, b int) (c int) { defer func() { c += 100 }() c = a + b return } func main() { x := test(10, 0) print(x) } 输出 : 110 注意 : defer 调⽤所需参数在 defer 语句执⾏时就被已计算好了 (拷⻉传递 )。 下⾯的例⼦中,匿名函数和 println() 在 defer 执⾏时就早早把参数 x 的复制品抢回家了。尽管这些 defer 在函数退出时执⾏,不过输出的确实是早先抢回家的数据。可以考虑⽤指针或者闭包代替参 数。 func main() { x := 10 defer func(a int) { println("a = ", a) }(x) defer println("print =", x) x += 100 println(x) } 输出 : 110 print = 10 a = 10 闭包引⽤对象,仅在执⾏的时候才获取对象。 func main() { var fs = [4]func(){} for i := 0; i < 4; i++ { defer println("defer i =", i) // defer: 直接获取当前 i 的值 defer func() { println("defer_closure", i)}() // defer_closure: 在 defer 函数中使⽤闭包 fs[i] = func(){ println("closure i = ", i) } // closure: 仅持有 i 的引⽤,在函数执⾏时再获取 36 } for _, f := range fs { f() } // 执⾏闭包函数 } 输出 : closure i = 4 closure i = 4 closure i = 4 closure i = 4 defer_closure 4 defer_closure 4 defer_closure 4 defer_closure 4 defer i = 3 defer i = 2 defer i = 1 defer i = 0 2.6 Panic、 Recover Go 没有 try ... catch ... finally 这种结构化异常处理,⽽是⽤ panic 代替 throw/raise 引发错误,然 后在 defer 中⽤ recover 函数捕获错误。 func panic(interface{}) func recover() interface{} 如果不使⽤ recover 捕获,则 panic 沿着 "调⽤堆栈 (call stack)" 向外层传递。 recover 仅在 defer 函数中使⽤才会终⽌错误,此时函数执⾏流程已经中断,⽆法像 catch 那样恢复到后续位置继续执 ⾏。 package main import ( "log" "runtime/debug" ) func Test(f func() int) { defer func() { if err := recover(); err != nil { debug.PrintStack() log.Println(err) } }() x := f() println(x) } func main() { 37 a := 0 Test(func() int { return 100 / a }) } 输出 : main.go:11 (0x2133) _func_001: debug.PrintStack() main.go:24 (0x21b6) _func_002: return 100 / a main.go:16 (0x2031) Test: x := f() main.go:25 (0x209c) main: }) 2012/07/25 00:41:26 runtime error: integer divide by zero 虽然 panic 可以抛出任意类型 (interface{}) 的错误对象,但规范中总是使⽤ error 接⼝类型。可使 ⽤ fmt.Errorf(format, ...)、 errors.New(text) 便捷创建错误对象。 除了使⽤ panic 引发错误外, go 还使⽤ error 返回错误信息。那么如何区别使⽤呢? The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values. 2.7 Call Stack runtime 包提供了 Caller、 Callers 两个函数⽤来获取调⽤堆栈信息。 func test() { fmt.Println(runtime.Caller(1)) } func main() { test() } 输出 : 8550 /Users/yuhen/.../main.go 13 true Callers 可以获取完整的调⽤堆栈列表。 func test() { ps := make([]uintptr, 10) count := runtime.Callers(0, ps) for i := 0; i < count; i++ { f := runtime.FuncForPC(ps[i]) fmt.Printf("%d, %s\n", i, f.Name()) 38 } } func main() { a := func() { test() } a() } 输出 : 0, runtime.Callers 1, main.test 2, main._func_001 3, main.main 4, runtime.main 5, runtime.goexit 输出调⽤堆栈信息,可以直接调⽤ runtime/debug PrintStack 函数。 利⽤ runtime.FuncForPC 可通过函数指针获取符号信息,⽐如名称、源⽂件、⾏号等。 (如果符号 表被删除,将导致执⾏出错 ) package main import ( "fmt" "reflect" "runtime" ) func test() {} func main() { p := reflect.ValueOf(test).Pointer() f := runtime.FuncForPC(p) fmt.Println(f) fmt.Printf("%p, %#x\n", test, f.Entry()) } 输出: &{main.test main.go [147 2] 8192 8193 9 8 0 0} 0x2000, 0x2000 39 第 3 章 Array、 Slices 和 Maps 3.1 Array 数组定义⽅式 "[n]" 有点古怪。⻓度下标 n 必须是编译期正整数常量  (或常量表达式 )。 ⻓ 度是类型的组成部分,也就是说 "[10]int" 和 "[20]int" 是完全不同的两种数组类型。 var a [4]int; // 所有元素⾃动被初始化为 0 a[1] = 100; for i := 0; i < len(a); i++ { println(a[i]) } 可以⽤复合语句直接初始化。 a := [10]int{ 1, 2, 3, 4 } // 未提供初始化值的元素为默认值 0 b := [...]int{ 1, 2 } // 由初始化列表决定数组⻓度,不能省略 "...",否则就成 slice 了。 c := [10]int{ 2:1, 5:100 }// 按序号初始化元素 数组指针类型 *[n]T,指针数组类型 [n]*T。 x, y := 1, 2 var p1 *[2]int = &[2]int{x, y} var p2 [2]*int = [2]*int{&x, &y} fmt.Printf("%#v\n", p1) fmt.Printf("%#v\n", p2) 输出 : &[2]int{1, 2} [2]*int{(*int)(0x4212f100), (*int)(0x4212f108)} ⽀持 "=="、 "!=" 相等操作符 (因为 Go 对象所使⽤的内存都被初始化为 0,按字节⽐较是可以的 ), 但不⽀持 ">"、 "<" 等⽐较操作符。 println([1]string{"a"} == [1]string{"a"}) 数组是值类型 ,也就是说会拷⻉整个数组内存进⾏值传递。可⽤ slice 或指针代替。 func test(x *[4]int) { for i := 0; i < len(x); i++ { println(x[i]) // ⽤指针访问数组语法并没有什么不同,⽐ C 更直观。 } x[3] = 300 } func main() { 40 x := &[4]int{ 2:100, 1:200 } test(x) println(x[3]) } 可以⽤ new() 创建数组,返回数组指针。 func test(a *[10]int) { a[2] = 100 // ⽤指针直接操作⽉有压⼒。 } func main() { var a = new([10]int) // 返回指针。 test(a) fmt.Println(a, len(a)) } 输出 : &[0 0 100 0 0 0 0 0 0 0] 10 多维数组和 C 类似,⼀种数组的数组。 func main() { var a = [3][2]int{ [...]int{1, 2}, [...]int{3, 4} } var b = [3][2]int{ {1, 2}, {3, 4} } c := [...][2]int{ {1, 2}, {3, 4}, {5, 6} } // 第⼆个维度不能⽤ "..." 。 c[1][1] = 100 fmt.Println(a, "\n", b, "\n", c, len(c), len(c[0])) } 输出: [[1 2] [3 4] [0 0]] [[1 2] [3 4] [0 0]] [[1 2] [3 100] [5 6]] 3 2 由于元素类型相同,因此可以使⽤复合字⾯值初始化数组成员。 type User struct { Id int Name string } func main() { a := [...]User { { 0, "User0" }, { 1, "User1" }, } b := [...]*User { { 0, "User0" }, 41 { 1, "User1" }, } fmt.Println(a) fmt.Println(b, b[1]) } 输出 : [{0 User0} {1 User1}] [0x42122340 0x42122320] &{1 User1} 3.2 Slices 作为变⻓数组的替代⽅案, slice 具备更灵活的特征。通过相关属性关联到底层数组的局部或全部。 src/pkg/runtime/runtime.h 172structSlice 173{// must not move anything 174 byte*array; // actual data 175 uint32len; // number of elements 176 uint32cap; // allocated number of elements 177}; http://blog.golang.org/2011/01/go-slices-usage-and-internals.html slice 是引⽤类型,默认值为 nil。可以⽤内置函数 len() 获取⻓度, cap() 获取容量。使⽤索引访问 时,不能超出 0 到 len - 1 范围。和数组之类型不同,引⽤类型 slice 赋值不会复制底层数组。 ⽤ slice ⻓时间引⽤ "超⼤ " 的底层数组,会导致严重的内存浪费。可考虑新建⼀个⼩的 slice 对象, 然后将所需的数据 copy 过去。 在 使⽤⽅法上和 Python 类似,可惜不⽀持负数定义逆向索引。 func main() { x := [...]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } var s1 []int = x[1:3] // 内容包括 x[1], x[2] fmt.Println(s1) s2 := x[4:]// x[4] ~ x[len - 1] fmt.Println(s2) s3 := x[:6]// x[0] ~ x[5] fmt.Println(s3) s4 := x[:] // x[0] ~ x[len - 1] fmt.Println(s4) } 输出 : [1 2] 42 [4 5 6 7 8 9] [0 1 2 3 4 5] [0 1 2 3 4 5 6 7 8 9] 对 slice 的修改就是对底层数组的修改。 func main() { x := [...]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } s := x[1:3] s[0] = 100 // 对 slice 的修改实际上是对原数组的修改,注意序号的差异。 fmt.Println(x) x[2] = 200 // 对数组修改会影响 slice。 fmt.Println(s) } 输出 : [0 100 2 3 4 5 6 7 8 9] [100 200] 可以直接创建⼀个 slice 对象  (内部⾃动创建底层数组 ),⽽不是从数组开始。 func test(s []int) { fmt.Println(s) s[1] = 100 // 内部总是指向同⼀个数组。 } func main() { s := []int{ 0, 1, 2 } test(s) // 引⽤类型,不⽤担⼼复制底层数组。 fmt.Println(s) } 输出 : [0 1 2] [0 100 2] 不能使⽤ new(),⽽应该是 make([]T, len, cap)。因为除了分配内存,还需要设置相关的属性。如 果忽略 cap 参数,则 cap = len。 func main() { s1 := make([]int, 10) // 相当于 [10]int{...}[:] s1[1] = 100 fmt.Println(s1, len(s1), cap(s1)) s2 := make([]int, 5, 10) s2[4] = 200 fmt.Println(s2, len(s2), cap(s2)) } 输出 : [0 100 0 0 0 0 0 0 0 0] 10 10 43 [0 0 0 0 200] 5 10 3.2.1 reslice cap 是 slice ⾮常重要的属性,它表⽰了 slice 可以容纳的最⼤元素数量。在初始创建 slice 对象时 cap = array_length - slice_start_index,也就是 slice 在数组上的开始位置到数组结尾的元素 数量。 在 cap 允许的范围内我们可以 reslice,以便操作后续的数组元素。超出 cap 限制不会导致底层数 组重新分配,只会引发 "slice bounds out of range" 错误。 func main() { a := [...]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } s1 := a[5:7] // [5 6], len = 2, cap = 5 s1 = s1[0:4] // [5, 6, 7, 8], len = 4, cap = 5 // 注意 reslice 不能超过 cap 的限制。 // 是在 slice 上重新切分,不是 Array,因此序号是以 slice 为准。 } 3.2.2 append 可以⽤ append() 向 slice 尾部添加新元素,这些元素保存到底层数组。 append 并不会影响原 slice 的属性,它返回变更后新的 slice 对象。如果超出 cap 限制,则会重新分配底层数组。 func main() { s1 := make([]int, 3, 6) // 添加数据,未超出底层数组容量限制。 s2 := append(s1, 1, 2, 3) // append 不会调整原 slice 属性。 // s1 == [0 0 0] len:3 cap:6 fmt.Println(s1, len(s1), cap(s1)) // 注意 append 是追加,也就是说在 s1 尾部添加。 // s2 == [0 0 0 1 2 3] len:6 cap:6 fmt.Println(s2, len(s2), cap(s2)) // 追加的数据未超出底层数组容量限制。 // 通过调整 s1,我们可以看到依然使⽤的是原数组。 // s1 == [0 0 0 1 2 3] len:6 cap:6 s1 = s1[:cap(s1)] fmt.Println(s1, len(s1), cap(s1)) // 再次追加数据  (使⽤了变参 )。 // 原底层数组已经⽆法容纳新的数据,将重新分配内存,并拷⻉原有数据。 // 我们通过修改数组第⼀个元素来判断是否指向原数组。 44 // s3 == [100 0 0 1 2 3 4 5 6] len:9 len:12 s3 := append(s2, []int{ 4, 5, 6 }...) s3[0] = 100 fmt.Println(s3, len(s3), cap(s3)) // ⽽原 slice 对象依然指向旧的底层数组对象,所以彻底和 s3 分道扬镳。 // s1 == [0 0 0 1 2 3] len:6 cap:6 // s2 == [0 0 0 1 2 3] len:6 cap:6 fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) } 输出 : [0 0 0] 3 6 [0 0 0 1 2 3] 6 6 [0 0 0 1 2 3] 6 6 [100 0 0 1 2 3 4 5 6] 9 12 [0 0 0 1 2 3] 6 6 [0 0 0 1 2 3] 6 6 因为 append 每次会创建新的 slice 对象,因此要优先考虑⽤序号操作。 func test1(n int) []int { datas := make([]int, 0, n) for i := 0; i < n; i++ { datas = append(datas, i) } return datas } func test2(n int) []int { datas := make([]int, n) for i := 0; i < n; i++ { datas[i] = i } return datas } func main() { // datas := test1(10000) datas := test2(10000) println(len(datas)) } 两种写法的性能差异很明显。虽然底层数组相同,但 test2 预先就 "填充 " 了全部元素,演变为普通 数组操作。 test1 append 每次循环都会创建新的 slice 对象,累加起来的消耗并不⼩。 45 3.2.3 copy 函数 copy ⽤于在 slice 间复制数据,可以是指向同⼀底层数组的两个 slice。复制元素数量受限于 src 和 dst 的 len 值  (两者的最⼩值 )。在同⼀底层数组的不同 slice 间拷⻉时,元素位置可以重叠。 func main() { s1 := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } s2 := make([]int, 3, 20) var n int n = copy(s2, s1) // n = 3。不同数组上拷⻉。 s2.len == 3,只能拷 3 个元素。 fmt.Println(n, s2, len(s2), cap(s2)) // [0 1 2], len:3, cap:20 s3 := s1[4:6] // s3 == [4 5]。 s3 和 s1 指向同⼀个底层数组。 n = copy(s3, s1[1:5]) // n = 2。同⼀数组上拷⻉,且存在重叠区域。 fmt.Println(n, s1, s3) // [0 1 2 3 1 2 6 7 8 9] [1 2] } 输出 : 3 [0 1 2] 3 20 2 [0 1 2 3 1 2 6 7 8 9] [1 2] 3.3 Maps 引⽤类型,类似 Python dict,不保证 Key/Value 存放顺序。 Key 必须是⽀持⽐较运算符 (==、 !=) 的类型。如 number、 string、 pointer、 array、 struct、 interface (接⼝实现类型必须⽀持⽐较运 算符 ),不能是 function、 map、 slice。 map 查找操作⽐线性搜索快很多,但⽐起⽤序号访问 array、 slice,⼤约慢 100x 左右。绝⼤多数 时候,其操作性能要略好于 Python Dict、 C++ Map。 func test(d map[string]int) { d["x"] = 100 } func main() { var d = map[string]int{ "a":1, "b":2 }; d2 := map[int]string{ 1:"a", 2:"b" }; test(d) fmt.Println(d, d2) d3 := make(map[string]string) d3["name"] = "Jack" fmt.Println(d3, len(d3)) } 输出 : map[a:1 b:2 x:100] map[1:a 2:b] map[name:Jack] 1 46 使⽤ array/struct key 的例⼦。 type User struct { Name string } func main() { a := [2]int{ 0, 1} b := [2]int{ 0, 1} d := map[[2]int]string { a: "ssss" } fmt.Println(d, d[b]) u := User{ "User1" } u2 := u d2 := map[User]string { u: "xxxx" } fmt.Println(d2, d2[u2]) } 输出 : map[[0 1]:ssss] ssss map[{User1}:xxxx] xxxx value 的类型就很⾃由了,完全可以⽤匿名结构或者空接⼝。 type User struct { Name string } func main() { i := 100 d := map[*int]struct{ x, y float64 } { &i: { 1.0, 2.0 } } fmt.Println(d, d[&i], d[&i].y) d2 := map[string]interface{} { "a": 1, "b": User{ "user1" } } fmt.Println(d2, d2["b"].(User).Name) } 输出 : map[0x42132018:{1 2}] {1 2} 2 map[a:1 b:{user1}] user1 使⽤ make() 创建 map 时,提供⼀个合理的初始容量有助于减少后续新增操作的内存分配次数。在 需要时, map 会⾃动扩张容量。 常⽤的判断和删除操作: func main() { var d = map[string]int{ "a":1, "b":2 }; v, ok := d["b"] // key b 存在, v = ["b"], ok = true fmt.Println(v, ok) v = d["c"] // key c 不存在, v = 0 (default) 47 fmt.Println(v) // 要判断 key 是否存在,⽤上⾯那种⽐较好。因为 key c 可能存在,⼜正好 value = 0。 // 可以⽤ _ 丢掉不想要的 value。 d["c"] = 3 // 添加或修改 fmt.Println(d) delete(d, "c") // 删除。删除不存在的 key,不会引发错误。 fmt.Println(d) } 输出 : 2 true 0 false map[a:1 c:3 b:2] map[a:1 b:2] 迭代器⽤法 (map 是⽆序的,每次迭代输出的次序都可能不同 ): func main() { d := map[string]int{ "a":1, "b":2 }; for k, v := range d { // 获取 key, value println(k, "=", v) } for k := range d { // 仅获取 key println(k, "=", d[k]) } } 只能对 Value 重新赋值,⽽不能直接修改其成员。 type User struct { Id int Name string } func main() { users := make(map[string]User) users["a"] = User{ 1, "user1" } fmt.Println(users, users["a"].Name) //users["a"].Name = "Jack" // Error: cannot assign to users["a"].Name u := users["a"] u.Name = "Jack" users["a"] = u fmt.Println(users) } 输出 : map[a:{1 user1}] user1 map[a:{1 Jack}] 48 要避免这种情形,可以使⽤指针类型。 type User struct { Id int Name string } func main() { u := User{ 100, "Tom" } m := map[int]*User{ 1:&u } fmt.Println(m, *m[1]) m[1].Name = "Jack" fmt.Println(m, *m[1]) } 输出 : map[1:0x42114b40] {100 Tom} map[1:0x42114b40] {100 Jack} 可以在 for range 迭代时安全删除和插⼊新的字典项。 func main() { d := map[string]int { "b":2, "c":3, "e":5 } for k, v := range d { println(k, v) if k == "b" { delete(d, k) } if k == "c" { d["a"] = 1 } } fmt.Println(d) } 输出: c 3 b 2 e 5 map[a:1 c:3 e:5] 注: go 6g 1.0.3,和 slice 值拷⻉传递不同, map 作为参数时,直接复制指针 。 49 第 4 章 Structs 看上去和 C struct ⾮常相似, Go 没有 class,⽤ struct 来实现⾯向对象编程模型。 4.1 定义 定义结构很简单,但只有⼤写字⺟开头的成员才能在包外被访问  (public, Go 以开头⼤写字⺟作为 导出符号的标志 )。 type User struct { Id int Name string } func main() { user1 := User{ 1, "Tom" } user2 := User{ Name:"Jack" } println(user1.Id, user1.Name) println(user2.Id, user2.Name) } 输出 : 1 Tom 0 Jack 可以⽤ "_" 定义 "补位 " 字段,⽀持指向⾃⾝的指针类型成员。 type Node struct { _ int Value string Next *Node } func main() { a := &Node{Value: "a"} b := &Node{Value: "b", Next: a} fmt.Printf("%#v\n", a) fmt.Printf("%#v\n", b) } 输出 : &main.Node{_:0, Value:"a", Next:(*main.Node)(nil)} &main.Node{_:0, Value:"b", Next:(*main.Node)(0x42120420)} ⽀持匿名结构,可⽤作成员或直接定义变量。 type Person struct { name string age int 50 contact struct { // 匿名结构成员 phone string address string postcode string } } func main() { var d = struct { // 直接定义匿名结构变量 name string age int title string }{"user1", 10, "cto"} d2 := Person{ name: "user1", age: 10, } d2.contact.phone = "13500000000" // 因为没有类型名称,只能如此初始化。或者重复定义那⼀⼤坨代码。 d2.contact.address = "beijing" d2.contact.postcode = "100000" fmt.Println(d, d2) } 匿名结构还可直接⽤于 map。 func test() { d := map[string]struct { x string y int }{ "a": {"a10", 10}, "b": {"b20", 20}, } fmt.Println(d) } 4.2 初始化 可以按顺序初始化全部字段,或者⽤字段名初始化部分字段,没有被初始化的字段为默认值 0。 u1 := User{ 1, "Tom" } u2 := User{ Id:2, Name:"Jack" } p1 := &User{ 1, "Tom" } // 习惯性⽤法 p2 := &User{} // 习惯性⽤法,相当于 new(User)。 允许直接⽤指针读写成员字段  (Go 没有 "->" 指针运算符 )。 51 type User struct { Id int Name string } func main() { u := &User{ 100, "Tom" } println(u.Id, u.Name) u.Id = 200 println(u.Id, u.Name) } 输出 : 100 Tom 200 Tom 相同类型的结构可以直接拷⻉赋值。 type User struct { Id int Name string } func main() { u := User{ 100, "Tom" } println(u.Id, u.Name) var u2 *User = new(User) *u2 = u println(u2.Id, u2.Name) } ⽀持 "=="、 "!=" 操作符,不⽀持 ">"、 "<" 等⽐较操作符。 println(User{"a"} == User{"a"}) 4.3 匿名字段 将结构体或指针嵌⼊到另⼀个结构体,但不提供字段名,这种⽅式称之为匿名字段。 这看上去很酷, 和⼤多数 OOP 语⾔实现继承⽅式有些类似,但其本质上就是隐式定义了⼀个 以类 型名为名称的字段 。由于是内容嵌⼊,所以在内存结构上它们是⼀个整体。 Go 对 "." 成员运算符做了特殊处理,使得我们可以直接访问和设置匿名字段成员。但在初始化成员 时,我们依然要把匿名字段当作⼀个正常的字段来赋值。 type User struct { Id int Name string } 52 type Manager struct { User Group string } func main() { m := Manager{ User{ 1, "Jack"}, "IT" } println(m.Id, m.Name, m.Group) m.Name = "Tom" println(m.Id, m.Name, m.Group) } 输出 : 1 Jack IT 1 Tom IT 就算被嵌套的匿名字段也包含匿名字段,依然可以⽤ "." 操作符直接访问任意层级的成员。编译器总 是从外向⾥进⾏查找,直到完成任务或出错。 type User struct { Id int Name string } type Manager struct { User Group string } type CXO struct { Manager Title string } func main() { ceo := CXO{ Manager{ User{ 100, "猪头三 " }, "Board of directors" }, "CEO" } fmt.Println(ceo, ceo.Id, ceo.Name, ceo.Group, ceo.Title) } 输出 : {{{100 猪头三 } Board of directors} CEO} 100 猪头三 Board of directors CEO 如果嵌⼊的结构来⾃其他包,则需要加上包名。可以匿名嵌⼊任意类型,包括 int、 string 等。 type User struct { Id int Name string } type Manager struct { User Group string 53 int string } func main() { m := Manager{ User:User{ 1, "Jack" }, Group:"IT", int:100, string:"Hello!" } println(m.Id, m.Name, m.Group, m.int, m.string) m.Name = "Tom" m.int = 200 m.string = "World!" println(m.Id, m.Name, m.Group, m.int, m.string) } 输出 : 1 Jack IT 100 Hello! 1 Tom IT 200 World! 匿名字段可能导致在不同层级上有多个同名字段,如此会导致隐藏⾏为发⽣: • 外部字段会隐藏匿名字段同名成员。 • 如果两个匿名字段存在同名成员,将可能导致字段访问错误。 这只是语法上的隐藏,依然可以通过类型名访问被隐藏的匿名成员。 type D1 struct { x int } type Data struct { D1 x int } func main() { d := Data { D1{ 10 }, 20 } println(d.x, d.D1.x) d.x = 200 d.D1.x = 100 println(d.x, d.D1.x) } 输出 : 20 10 200 100 如果多个匿名字段在同⼀层次同名,将导致编译器⽆法确定⺫标⽽出错。 type D1 struct { x int } type D2 struct { 54 x int } type Data struct { D1 D2 } func main() { d := Data { D1{ 10 }, D2{ 20 } } println(d.x) } 输出 : main.go:24: ambiguous DOT reference Data.x make: *** [_go_.6] Error 1 改⽤匿名字段类型前缀访问,就不会引发编译器错误了。 func main() { d := Data { D1{ 10 }, D2{ 20 } } println(d.D1.x, d.D2.x) } 输出 : 10 20 基于隐藏规则,如果外部类型 Data 也有⼀个 x int 字段,那么访问 d.x 肯定是没事的。 type Data struct { D1 D2 x int } func main() { d := Data { D1{ 10 }, D2{ 20 }, 30 } println(d.x, d.D1.x, d.D2.x) } 输出 : 30 10 20 需要注意的是: 匿名字段嵌⼊并不是继承, Go 语⾔根本没有继承概念 。 我们不能把下⾯例⼦中的 m 直接赋值给 u,⽽只能是 m.User 进⾏相同类型的值拷⻉。 (这种⽗类变 量引⽤⼦类对象的⾏为,称为多态,是 OOP 的重要特征之⼀ ) type User struct { Id int Name string } type Manager struct { 55 Group string User } func main() { m := Manager{ User:User{ 1, "Tom" }, Group:"IT" } //var u User = m // Error: cannot use m (type Manager) as type User in assignment var u User = m.User // value copy fmt.Println(m, u) } 试试直接嵌⼊匿名类型指针。 type User struct { Id int Name string } type Manager struct { Group string *User } func main() { m := Manager{ User:&User{ 1, "Tom" }, Group:"IT" } // 注意初始化字段名 fmt.Println(m, m.Id, m.Name) // 就算是指针,访问成员也⽉有压⼒。 } 输出 : {IT 0x42114b40} 1 Tom 不能同时嵌⼊ User 匿名类型和 *User 指针匿名类型,这将引发类似 "duplicate field User" 错误。 4.4 ⽅法 Go 虽然没有 class,但同样⽀持 method,使⽤⽅法和我们习惯的⾯向对象编程⽅式相似。更接近 Python method,包括在函数名前⾯这个被称为 receiver 的参数。 receiver 在多数语⾔中是隐式传递的 this,在 Python 中通常写作 self。 type User struct { Id int Name string } func (this *User)test() { println(this.Id, this.Name) } 56 func (this User)test2() { println(this.Id, this.Name) } func main() { u := &User{ 1, "Jack" } u.test() } ⽅法和函数类似,只是在函数名前⾯多了绑定类型参数 receiver。 • 只能为相同包中的类型定义⽅法。 • 如果⽅法代码从不使⽤ receiver 参数,那么可以省略变量名。 • receiver 类型可以是 T 或 *T,我们把 T 称为 base type。 • base type 不能是指针或接⼝。 • T 和 *T 的⽅法都被绑定  (bound) 到 base type。 • T 和 *T 的不能拥有同名⽅法,否则引发 "method redeclared" 错误。 • 可以⽤ T value 或 pointer 调⽤所有绑定的⽅法,编译器⾃动进⾏类型转换。 type User struct { Id int Name string } func (*User)test() { // 省略 receiver 的参数名 println("*User.test") } func (this User)test2() { // 名字不能是 test,否则导致如下编译错误! // method redeclared: User.test // method(*User)func() // method(this User)func() println("User.test2:", this.Name) } func main() { u := User{ 1, "Tom" } p := &u u.test() // == (&u).test() u.test2() // p.test() // p.test2() // == (*p).test2() } 输出 : *User.test User.test2: Tom *User.test User.test2: Tom 57 从某种意义上说,⽅法是普通函数的语法糖,我们完全⽤普通函数⽅式调⽤这些⽅法。 func main() { u := User{ 1, "Tom" } (*User).test(&u) # 注意正确的 receiver 类型。 User.test2(u) # 使⽤类型 User 或 *User,⽽不是变量 u。 var f func(*User) = (*User).test f(&u) var f2 func(User) = User.test2 f2(u) // ------------------------- // # 正因为是 "函数 ",所以下⾯写法是允许。 # 相当于 (*User).test(nil) var p *User = nil p.test() (*User)(nil).test() } 正如上⾯ "被还原 " 的普通函数样式,如果 receiver 不⽤指针类型,那么调⽤时势必发⽣值拷⻉⾏ 为,就成两个对象了,在⽅法内部对该对象的修改不会影响到 caller。 func (this User)test() { this.Id = 200; this.Name = "Tom" println(&this, this.Id, this.Name) } func main() { u := User{ 1, "Jack" } u.test() println(&u, u.Id, u.Name) } 输出 : 0x442085f78 200 Tom 0x442085f90 1 Jack 不⻅得使⽤指针就⼀定好过传值,因为按照 Go 的内存管理策略,涉及指针和引⽤的对象会被分配 到 GC Heap 上。如果对象很 "⼩ ",显然要⽐在栈上进⾏值拷⻉ "耗费 " 更多。 ⽅法不是 struct 的专利,我们给任何⾮指针和接⼝类型的本地类型  (同⼀个包 ) 添加⽅法。 type MyInt int // int 不再当前包中,因此⽤ MyInt "重新定义 "⼀个类型。 type MySlice []int func (i MyInt)test() { fmt.Printf("MyInt = %v\n", i) } 58 func (i MySlice)test() { fmt.Printf("MySlice = %v\n", i) } func main() { i := MyInt(10) i.test() s := MySlice{ 100, 200 } s.test() } 可直接访问匿名字段  (匿名类型或匿名指针类型 ) 的⽅法,这种⾏为类似 "继承 "。访问匿名字段⽅法 时,同样有隐藏规则。正是基于这种隐藏规则,我们很容易实现 override 效果。 因为编译器会⾃动将 receiver 转换为 value 或 pointer。正如下⾯例⼦中, Manager.Test receiver 类型可以不是指针。 type User struct { Id int Name string } type Manager struct { User Group string } func (this *User) Test() { println("User Test:", this.Id, this.Name) } func (this User) ToString() string { return fmt.Sprintf("User ToString: [%d] %s", this.Id, this.Name) } func (this Manager) Test() { println("Manager Test:", this.Id, this.Name) } func main() { m := &Manager{User{1, "Tom"}, "IT"} m.Test() println(m.ToString()) } 输出 : Manager Test: 1 Tom User ToString: [1] Tom ⽤匿名类型名称可以做到 "基类转型 " 的效果。 59 type User struct { Id int Name string } type Manager struct { Group string User } func main() { m := Manager{ User:User{ 1, "Tom" }, Group:"IT" } var p *User = &m.User// 并不是真的多态,只不过是访问其内部的字段⽽已。 println(p.Id, p.Name) } 通过下⾯这个例⼦,你会发现 Test 的 this 指向 Manager.User 成员指针。 type User struct { Id int Name string } type Manager struct { Group string User // 放在第⼆个位置, offset != 0,使其和外层 Manager 内存地址不同。 } func (this *User) Test() { fmt.Printf("Test this address = %p\n", this) } func main() { m := &Manager{ User:User{ 1, "Tom" }, Group:"IT" } fmt.Printf("m.address = %p\n", m) fmt.Printf("m.User.address = %p\n", &m.User) m.Test() } 输出 : m.address = 0x421016c0 m.User.address = 0x421016d0 Test this address = 0x421016d0 ⽅法总是从 Receiver 类型开始查找⺫标成员。下⾯例⼦中, A.test() 总是访问 A.x。如果将 B.test() 注释掉,那么⽆论⽤ o 还是 o.A 调⽤ test(),都将指向 A.x。 type A struct { x int 60 } type B struct { A x int } func (this *A) test() { println("A:", this.x)// A.x,因为 this *A 类型根本不包括 B 的成员。 } func (this *B) test() { println("B:", this.x)// B.x } func main() { o := B{x: 123} o.test() o.A.test() } 输出 : 123 0 4.5 内存布局 借助于 unsafe 包,我们可以探查⼀下 struct 的内存布局。 package main import ( "fmt" "unsafe" ) type User struct { Id int Name string } type Manager struct { Group string User } func main() { m := Manager{ User:User{ 1, "Tom" }, Group:"IT" } fmt.Printf("m alignof = %d\n", unsafe.Alignof(m)) fmt.Printf("m address = %p\n", &m) fmt.Printf("m size = %d\n", unsafe.Sizeof(m)) 61 fmt.Printf("m.Group address = %p\n", &m.Group) fmt.Printf("m.Group offset = %d\n", unsafe.Offsetof(m.Group)) fmt.Printf("m.Group size = %d\n", unsafe.Sizeof(m.Group)) fmt.Printf("m.User address = %p\n", &m.User) fmt.Printf("m.User offset = %d\n", unsafe.Offsetof(m.User)) fmt.Printf("m.User size = %d\n", unsafe.Sizeof(m.User)) fmt.Printf(" User.Id address = %p\n", &m.User.Id) fmt.Printf(" User.Id offset = %d\n", unsafe.Offsetof(m.User.Id)) fmt.Printf(" User.Id size = %d\n", unsafe.Sizeof(m.User.Id)) fmt.Printf(" User.Name address = %p\n", &m.User.Name) fmt.Printf(" User.Name offset = %d\n", unsafe.Offsetof(m.User.Name)) fmt.Printf(" User.Name size = %d\n", unsafe.Sizeof(m.User.Name)) } 输出 : m alignof = 8 m address = 0x421016c0 m size = 40 m.Group address = 0x421016c0 m.Group offset = 0 m.Group size = 16 m.User address = 0x421016d0 m.User offset = 16 m.User size = 24 // 内容⻓度 20,按 8 字节对⻬后为 24。 User.Id address = 0x421016d0 User.Id offset = 0 User.Id size = 4 User.Name address = 0x421016d8 User.Name offset = 8 User.Name size = 16 4.6 字段标签 可以为字段定义标签,⽤反射可以读取这些标签。做什么⽤呢?⽐如像下⾯这样: package main import ( "reflect" "fmt" ) type User struct { Name string "姓名 " Age int "年龄 " } func main() { 62 u := User{"Tom", 23} t := reflect.TypeOf(u) v := reflect.ValueOf(u) for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Printf("%s (%s = %v)\n", f.Tag, f.Name, v.Field(i).Interface()) } } 输出 : 姓名 (Name = Tom) 年龄 (Age = 23) 63 第 5 章 接⼝ 接⼝是⼀个或多个⽅法签名的集合,任何⾮接⼝类型只要拥有与之对应的全部⽅法实现  (包括相同的 名称、参数列表以及返回值。当然除了接⼝所规定的⽅法外,它还可以拥有其他的⽅法 ),就表⽰它 "实现 " 了该接⼝,⽆需显式在该类型上添加接⼝声明。此种⽅式,⼜被称作 Duck Type。 5.1 接⼝定义 接⼝定义看上去和 struct 类似,区别在于: • 只有⽅法签名,没有实现。 • 没有数据字段。 习惯性地将接⼝命名以 er 结尾,⽽不是以往习惯性地以⼤写字⺟ "I" 开头。   type Tester interface { Test() } 在接⼝中匿名嵌⼊另外的接⼝,这类似我们熟悉的接⼝继承。 type Reader interface { Read() } type Writer interface { Write() } type ReadWriter interface { Reader Writer } type ReadWriteTest struct { } func (*ReadWriteTest) Read() { println("Read") } func (*ReadWriteTest) Write() { println("Write") } func main() { t := ReadWriteTest{} var rw ReadWriter = &t 64 rw.Read() rw.Write() } 接⼝是可被实例化的类型,⽽不仅仅是语⾔上的约束规范。当我们创建接⼝变量时,将会为其分配 内存,并将赋值给它的对象拷⻉存储。 可以像下⾯这样,把接⼝类型匿名嵌⼊到 struct 中。 type Tester interface { Test() } type MyTest struct { } func (this *MyTest)Test() { fmt.Println("Test") } type User struct { Id int Name string Tester } func main() { u := User{ 100, "Jack", nil } u.Tester = new(MyTest) fmt.Println(u) u.Test() } 输出 : {100 Jack 0x420e51c0} Test 5.2 执⾏机制 接⼝对象内部由两部分组成: itab 指针 、 data 指针。 src/pkg/runtime/runtime.h 151struct Iface 152{ 153 Itab*tab; 154 void*data; 155}; src/pkg/runtime/iface.c 32struct Itab 65 33{ 34InterfaceType*inter; 35Type*type; 36Itab*link; 37int32bad; 38int32unused; 39void(*fun[])(void); 40}; http://research.swtch.com/interfaces http://groups.google.com/group/golang-nuts/browse_thread/thread/ f01465cb6206ad6/86eaf4610512d351?lnk=gst 将对象赋值给接⼝变量时,会发⽣值拷⻉⾏为。没错,接⼝内部存储的是指向这个复制品的指针。 ⽽且我们⽆法修改这个复制品的状态,也⽆法获取其指针。 type User struct { Id int Name string } type Tester interface { Test() } func (this User) Test() {} func main() { u := User{ 1, "Tom" } i := Tester(u) u.Id = 100 // 显式修改的原对象 u.Name = "Jack" fmt.Println(u, i) // 虽然接⼝内的复制品未受到影响 //i.(User).Name = "Jack" // ⽆法操作 : cannot assign to i.(User).Name // 要是指针就没问题了。 i.(*User).Name = ...,指针指向原始对象。 fmt.Println(&(i.(User))) // cannot take the address of i.(User) } 输出 : {100 Jack} {1 Tom} 就算等于 nil 的接⼝,也仅仅表⽰它存储的对象是 nil。 func main() { var a interface{} println(unsafe.Sizeof(a), a == nil) } 输出 : 16, true 66 但需要注意的是,只有当接⼝存储的类型和对象都是 nil 时,接⼝才等于 nil。 func main() { var a interface{} = nil // type: nil, value: nil println(a == nil) var p *int = nil // type: *int, value: nil var b interface{} = p println(b == nil) } 输出 : true false 再说说接⼝⽅法调⽤机制。 itab 依据 data 类型创建,存储了接⼝动态调⽤的元数据信息,其中包括 data 类型所有符合接⼝签 名的⽅法地址列表。⽤接⼝对象调⽤⽅法时,就从 itab 中查找所对应的⽅法,并将 *data (指针 ) 作 为 receiver 参数传递给该⽅法,完成实际⺫标⽅法调⽤。 编译器构建 itab 时,会区分 T 和 *T ⽅法集 (method sets),并从中获取接⼝实现⽅法的地址。接 ⼝调⽤不会做 receiver ⾃动转换,⺫标⽅法必须在接⼝实现⽅法集中。 • T 仅拥有属于 T 类型的⽅法集,⽽ *T 则同时拥有 T + *T ⽅法集。 • 基于 T 实现⽅法,表⽰同时实现了 interface(T) 和 interface(*T) 接⼝。 • 基于 *T 实现⽅法,那就只能是对 interface(*T) 实现接⼝。 Method sets A type may have a method set associated with it (Interface types, Method declarations). The method set of an interface type is its interface. The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type *T is the set of all methods with receiver *T or T (that is, it also contains the method set of T). Any other type has an empty method set. In a method set, each method must have a unique name. http://golang.org/doc/go_spec.html 为什么使⽤不同的⽅法集,究其原因是接⼝实现时的 value-copy。如果是 pointer-interface, T 和 *T method 确保都能正确执⾏。⽽如果是 value-interface,那么 *T ⽅法是⽆法保证合法操作 这个 readonly-data 的。官⽅的说法是⽆法获取其指针。在使⽤接⼝时,如果需要修改原对象,记 得⽤指针赋值。 Why do T and *T have diferent method sets? 67 From the Go Spec: The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type *T is the set of all methods with receiver *T or T (that is, it also contains the method set of T). If an interface value contains a pointer *T, a method call can obtain a value by dereferencing the pointer, but if an interface value contains a value T, there is no useful way for a method call to obtain a pointer. Even in cases where the compiler could take the address of a value to pass to the method, if the method modifies the value the changes will be lost in the caller. As a common example, this code: var buf bytes.Bufer io.Copy(buf, os.Stdin) would copy standard input into a copy of buf, not into buf itself. This is almost never the desired behavior. http://golang.org/doc/go_faq.html#diferent_method_sets type User struct { Id int Name string } type Tester interface { Test() } type Stringer interface{ String() } func (this User) Test() { fmt.Println("Test:", this) } func (this *User) String() { fmt.Printf("String: Id=%d, Name=%s\n", this.Id, this.Name) } func main() { u := User{ 1, "Tom" } p := &u // User.Test() 同时实现了 Tester(User) 和 Tester(*User) 接⼝ Tester(u).Test() Tester(p).Test() // *User.String() 仅实现了 Stringer(*User) 接⼝。下⾯这个错误就是证明。 68 // Error: cannot convert u (type User) to type Stringer: // User does not implement Stringer (String method requires pointer receiver) //Stringer(u).String() Stringer(p).String() } 输出 : Test: {1 Tom} Test: {1 Tom} String: Id=1, Name=Tom 5.3 匿名字段⽅法 接⼝同样⽀持由 struct 匿名字段实现的⽅法,因为外层结构 "继承 " 了匿名字段的⽅法集。 匿名字段⽅法集规则 : • 如果 S 嵌⼊匿名类型 T,则 S ⽅法集包含 T ⽅法集。 • 如果 S 嵌⼊匿名类型 *T,则 S ⽅法集包含 *T 的⽅法集 (T + *T)。 • 如果 S 嵌⼊匿名类型 T 或 *T,则 *S ⽅法集包含 *T 的⽅法集 (T + *T)。 下例中, *User 实现了 Tester 接⼝,⽽ Manager 嵌⼊了 User 匿名字段,是以同样认为 *Manager 实现了 Tester 接⼝。 type User struct { Id int Name string } type Manager struct { Group string User } type Tester interface { Test() } func (this *User)Test() { fmt.Println(this) } func main() { u := User{ 1, "Tom" } m := Manager{ User:User{ 2, "Jack" }, Group:"IT" } var i Tester i = &u // 我们为 *User 定义了 Test(),⾃然表⽰实现了该接⼝。 i.Test() 69 i = &m // 匿名字段,同样也拥有 Test() i.Test() } 借助于接⼝, Go 完全可以实现 OOP 所需的操作。 type User struct { Id int Name string } type Manager struct { Group string User } type Tester interface { Test() } func (this *User)Test() { // BaseClass method fmt.Println(this) } func (this *Manager)Test() { // Override BaseClass method fmt.Println(this) } func dosomething(o Tester) { // 正确调⽤ *User.Test 和 *Manager.Test,类似多态的功能。 o.Test() } func main() { m := Manager{ User:User{ 2, "Jack" }, Group:"IT" } dosomething(&m) dosomething(&m.User) } 输出 : &{IT {2 Jack}} &{2 Jack} 5.4 空接⼝ 任何类型默认都实现了空接⼝ interface{},这就是 C void*, Java/C# System.Object。 如果要接收任意类型的参数,可以使⽤ interface{}。 func test1(a int, b interface{}) {} func test2(a int, b ...interface{}) {} 70 5.5 类型推断 利⽤类型推断,可以判断接⼝对象是否是某个具体的接⼝或类型。 type User struct { Id int Name string } type Tester interface { Test() } func (this *User)Test() { fmt.Println("*User.Test, ", this) } func doSomething(i interface{}) { // 推断并检查是否实现了 Tester 接⼝,不会引发错误。 if o, ok := i.(Tester); ok { o.Test() } // 直接利⽤推断进⾏转型。 // 如果类型推断失败,或者⽅法不存在都会导致错误。 i.(*User).Test() } func main() { u := &User{ 1, "Tom" } doSomething(u) } 输出 : *User.Test, &{1 Tom} *User.Test, &{1 Tom} ⽽ type switch 则适合处理 interface{} 这种 "⽆类型 " 参数。 ".(type)" 也仅能⽤于 switch 语句。 type User struct { Id int Name string } func Test(i interface{}) { switch v := i.(type) { case *User: println("User: Name =", v.Name) case string: println("string = ", v) default: fmt.Printf("%T: %v\n", v, v) 71 } } func main() { Test(&User{ 1, "Tom" }) Test("Hello, World!") Test(123) } 输出 : User: Name = Tom string = Hello, World! int: 123 5.6 接⼝转换 拥有超集的接⼝可以被转换为⼦集的接⼝。⽐如下⾯例⼦中, IManager 拥有 IUser 的全部⽅法签 名,我们可以直接将 im 赋值给 iu。 (此例貌似看出 er 作为接⼝标志的弊端了,还是 I 开头好 ) type User struct { Id int Name string } type Manager struct { Group string User } type IUser interface { Test() } type IManager interface { Test() Test2() } func (this *User)Test() { fmt.Println(this) } func (this *User)Test2() { fmt.Println(this) } func main() { var im IManager = &Manager{"IT", User{1, "Tom"}} im.Test() im.Test2() var iu IUser = im iu.Test() } 72 第 6 章 并发 6.1 Goroutine goroutine 有点像 greenlet/coroutine,不过也仅仅是相似⽽已。因为节省了频繁创建和销毁线程 的开销,所以执⾏成本极低。执⾏ goroutine 只需极少的栈内存  (⼤概是 4~5KB),在必要的时候可 扩张或收缩。也正因为如此,可同时运⾏成千上万个并发任务。 goroutine ⽐ thread 更易⽤、更⾼ 效、更轻便。 • goroutine 基于并⾏设计,尽管它默认在⼀个线程上实现并发。⽽ coroutine 默认则是并发设 计,基于同⼀个线程进⾏切换调度。 • goroutine 通过 channel 相互通讯,⽽ coroutine 则通过 yield 和 resume。 func main() { go func() { time.Sleep(3 * time.Second) println("go...") }() println("hi!") time.Sleep(5 * time.Second) } 输出 : hi! go... 多个 goroutine 运⾏在同⼀进程的地址空间内,共享内存数据。不过设计上通常遵循规则: Do not communicate by sharing memory. Instead, share memory by communicating。 6g 编译器基于 multiplexed thread 实现 goroutine,多个任务会复⽤线程,相⽐ gccgo 为每个 goroutine 准备⼀个独⽴线程更加⾼效和轻便。只有在发⽣阻塞时,调度器才会使⽤空闲或新线程 去执⾏其他的排队任务。 默认情况下,调度器仅使⽤单线程,也就是说只实现了并发。 想要发挥多核处理器的并⾏威⼒,必 须调⽤ runtime.GOMAXPROCS(n) 告知调度器同时使⽤多个线程。也可以通过设置环境变量来达到 相同的⺫的,好处就是调整参数时⽆需重新编译程序。 注: "并发  (concurrency)" 和 "并⾏  ( parallelism)" 是不同的。在单个 CPU 核上,线程通过时间⽚或者让出控制权来实现 任务切换,达到 "同时 " 运⾏多个任务的⺫的,这就是所谓的并发。但实际上任何时刻都只有⼀个任务被执⾏,其他任务通 过某种算法来排队。多核 CPU 可以让同⼀进程内的 "多个线程 " 做到真正意义上的同时运⾏,它们之间不需要排队  (依然会 发⽣排队,因为线程数量可能超出 CPU 核数量,还有其他的进程等等。这⾥说的是⼀个理想状况 ),这才是并⾏。除了多 核,并⾏计算还可能是多台机器上部署运⾏。 73 下⾯这个例⼦,在我的 MacBook Pro 374 上,启⽤并⾏后性能增⻓⾮常明显。 package main import ( "runtime" ) func test(c chan bool, b bool) { x := 0 for i := 0; i < 100000000; i++ { x += i } println(x) if b { c <- true } } func main() { runtime.GOMAXPROCS(2) c := make(chan bool) for i := 0; i < 100; i++ { go test(c, i == 99) } <- c } 不使⽤ GOMAXPROCS 输出 : $ time ./test 887459712 ... ... real0m7.332s user0m7.243s sys0m0.012s 启⽤ GOMAXPROCS 输出 : $ time ./test 887459712 ... ... real0m3.979s user0m7.145s sys0m0.014s http://en.munknex.net/2011/12/golang-goroutines-performance.html 注意:进程退出时不会等待 goroutine 完成。 74 6.2 Channel channel 是类似 pipe 的单 /双向数据管道。从设计上确保,在同⼀时刻,只有⼀个 goroutine 能从 中接收数据。发送和接收都是原⼦操作,不会中断,只会失败。 6.2.1 阻塞同步 channel 是引⽤类型,使⽤ "make(chan )" 创建, 是要传递的数据类型。 channel 使⽤ "<-" 接收和发送数据。 "chan <- data" 发送数据, "data <- chan" 接收数据,可以 忽略掉接收结果,纯粹作为同步信号通知。默认情况下,发送和接收都会被阻塞  (block)。 • 发送操作被阻塞,直到接收端准备好接收。 • 接收操作被阻塞,直到发送端准备好发送。 func test(c chan int) { time.Sleep(3 * time.Second) println("go...") c <- 1 } func main() { c := make(chan int) go test(c) println("hi!") <- c // 阻塞等待退出信号,忽略掉返回的数据。 println("over!") } 输出 : hi! go... over! 常⽤的做法是将并发任务和创建 channel 放在⼀个⼯⼚函数中 (参考 time.Tick 实现 )。 func task(args ...int) chan int { x := 0 c := make(chan int) go func() { time.Sleep(2 * time.Second) for _, v := range args { x += v } c <- x 75 }() return c } func main() { c := task(1, 2, 3, 4) println("do something...") i := <- c println("task result =", i) } 输出 : do something... task result = 10 6.2.2 迭代器 可以⽤ range 迭代器从 channel 中接收数据,直到 channel close ⽅才终⽌循环。 func main() { c := make(chan int) go func() { for i := 0; i < 20; i++ { c <- i } close(c) // 如果不关闭,会引发接收端 throw: all goroutines are asleep - deadlock! }() for v := range c { println(v) } } 接收⽅还可以⽤另⼀种⽅式代替迭代器接收数据并判断 channel close 状态。 func main() { c := make(chan int) go func() { for i := 0; i < 20; i++ { c <- i } close(c) }() for { if v, ok := <- c; ok { 76 println(v) } else { break; } } } 只有发送端  (另⼀端正在等待接收 ) 才能 close,只有接收端才能获得关闭状态。 close 调⽤不是必 须的,但如果接收端使⽤迭代器或者循环接收数据,则必须调⽤,否则可能导致接收端 throw: all goroutines are asleep - deadlock!。 6.2.3 单向通道 可以将 channel 指定为单向通道。⽐如 "<-chan int" 仅能接收, "chan<- int" 仅能发送。 func recv(c <-chan int, over chan<- bool) { for v := range c { println(v) } over <- true } func send(c chan<- int) { for i := 0; i < 10; i++ { c <- i } close(c) // 省略 close,可能会导致 recv throw: all goroutines are asleep - deadlock! } func main() { c := make(chan int) // 双向 channel,可以转换为单向 channel。 o := make(chan bool) go recv(c, o) go send(c) <- o } 6.2.4 异步通道 异步 channel,就是给 channel 设定⼀个 bufer 值,在 bufer 未填满的情况下,不阻塞发送操 作。 bufer 未读完前,不阻塞接收操作。 bufer 是被缓冲数据对象的数量,不是内存⼤⼩。 通常情况下, bufered channel 拥有更⾼的性能。但 bufer 值不是越⼤越好,过多的 items 可能 造成性能瓶颈。可以考虑将多个 item 打包,以减少 bufer 内容的数量。 77 c := make(chan int, 10) bufer 仅仅是 channel 对象的⼀个内部属性,它的类型依然是 "chan "。 func main() { c := make(chan int, 10) o := make(chan bool) go func() { time.Sleep(2 * time.Second) println("recv:", <- c) o <- true }() c <- 100 // 未阻塞 println("send over...") <- o } 输出 : send over... recv: 100 使⽤异步 channel 实现 semaphore 同步信号量  (限制并发执⾏的数量 ) 的例⼦。 var over = make(chan bool) // 等待结束信号 var sem = make(chan int, 2) // 信号量,并发限制为 2 func worker(i int) { sem <- 1 // 向信号量丢个书包,抢个位⼦先。如果没有空位,就只能等待。 fmt.Println(time.LocalTime().Format("04:05"), i) // 输出时间,为了看看效果。 time.Sleep(1 * time.Second) // 模拟等待时间,否则很快结束,从时间上看不出效果。 <- sem // 完成⼯作后,拿回书包  (是不是⾃⼰的⽆所谓了 ),让出空位。 if i == 9 { over <- true } } func main() { // ⿊⼼⽼板⼀次丢出 10 个⼯作 for i := 0; i < 10; i++ { go worker(i) } // 等待结束信号 <- over } 输出 : 22:50 0 22:50 1 78 22:51 2 22:51 3 22:52 4 22:52 5 22:53 6 22:53 7 22:54 8 22:54 9 6.2.5 Select 如果有多个 channel 需要监听,可以考虑使⽤ select,随机处理⼀个可⽤的 channel。 还可以定义 case default,如果没有可⽤ channel,那么不阻塞,直接执⾏ default block 。如果 外⾯套了循环,就成洪⽔泛滥了。 func main() { c1, c2 := make(chan int), make(chan string) o := make(chan bool) go func() { for { select { case v, ok := <- c1: if !ok { o <- true; break } println("c1 =", v) case v, ok := <- c2: if !ok { o <- true; break } println("c2 =", v) } } }() c1 <- 1 c2 <- "a" c2 <- "b" c1 <- 2 close(c1) //close(c2) <- o } 输出 : c1 = 1 c2 = a c2 = b c1 = 2 select 也可⽤于发送端,由于其随机选择⼀个可⽤的 case,那么折腾同⼀个 channel 也是可以滴。 79 func main() { c := make(chan int) o := make(chan bool) go func() { for v := range c { print(v) } o <- true }() for i := 0; i < 10; i++ { select {// 随机发送 0 或 1 case c <- 0: // no statement, no fallthrough case c <- 1: } } close(c) <- o } 如果有其他 goroutine 正在运⾏,可以⽤空的 "select {}" 阻塞 main(),避免进程终⽌。 func main() { go func() { for { fmt.Println(time.Now()) time.Sleep(time.Second) } }() select {} } 6.2.6 Timeout time 包还提供了 After、 Tick 等函数返回计时器 channel。⽐如下⾯的例⼦中,我们⽤ After 来处 理⼀个需求:不管有没有数据操作,总之 5 秒后终⽌循环。 func main() { c := make(chan int) o := make(chan bool) tick := time.Tick(1 * time.Second) go func() { for { select { case v := <- c: println(v) case <- tick: println("tick") 80 case <- time.After(5 * time.Second): // 只⽤⼀次,没必要⽤外部变量。 println("timeout") o <- true break } } }() <- o } 6.2.7 Multiplexing channel 本⾝也可以作为数据发送给其他的 channel。 下⾯例⼦中, client 将要处理的数据连同与之通讯的 channel 对象⼀并打包发给 server。 server 使 ⽤⼀个独⽴的 Request Channel 循环 "监听 " 所有的 client 接⼊请求,并将接收到的数据⽴即提交 给另⼀个 goroutine 做并发处理,实现接⼊和处理的并发操作。⽽处理数据的 goroutine 直接从数 据包中获取与之通讯的 channel,返回结果给 client。这和我们熟悉的 socket server 编程模型类 似。 type Data struct { args []int ch chan string } func server() chan *Data { reqs := make(chan *Data) // 服务器 Request Channel go func() { // 使⽤独⽴的 goroutine 循环处理所有的客户端请求 for d := range reqs { // 循环从 Request Channel 获取客户端请求数据 go serverProcess(d) // 将请求的客户端数据交给另外的 goroutine 处理,并⽴即等待下⼀个请求。 } }() return reqs // 返回 Request Channel,⽤于 close server。 } func serverProcess(data *Data) { x := 0 for i := range data.args { // 统计⽤户请求数据 x += i } s := fmt.Sprintf("server: %d", x) data.ch <- s // 使⽤客户端请求数据包中的 Channel,返回结果给客户端。 } func main() { /* 启动服务器 */ 81 serverReqs := server() /* 客户端向服务器发送请求数据,并等待返回结果 */ data := &Data{ []int{1, 2, 3}, make(chan string) } serverReqs <- data // 发送请求到服务器 println(<- data.ch) // 获取服务器返回结果 /* 关闭服务器 */ close(serverReqs) } 6.2.8 runtime Goroutine runtime 包中提供了⼏个与 goroutinue 有关的函数。 Gosched() 类似 yield,让出当前 goroutine 的执⾏权限。调度器安排其他等待的任务运⾏,并在 下次某个时候从该位置恢复执⾏。 func main() { go func() { for i := 0; i < 5; i++ { println("go1:", i) if i == 2 { runtime.Gosched() } } }() go func() { println("go2") }() time.Sleep(3 * time.Second) } 输出 : go1: 0 go1: 1 go1: 2 go2 go1: 3 go1: 4 NumCPU() 返回 CPU 核数量, NumGoroutine() 返回正在执⾏和排队的任务总数。你会发现,总是 有⼏个 goroutinues 在偷偷运⾏ (main 和 GC Heap 管理 )。 func main() { println("start:", runtime.NumGoroutine()) for i := 0; i < 10; i++ { go func(n int) { println(n, runtime.NumGoroutine()) }(i) } 82 time.Sleep(3 * time.Second) println("over:", runtime.NumGoroutine()) } 输出 : start: 2 0 13 1 12 2 11 3 10 4 9 5 8 6 7 7 6 8 5 9 4 over: 3 Goexit() 将终⽌当前 goroutine (终⽌整个调⽤堆栈链,在内层退出 ),并确保 defer 函数被调⽤。 func main() { go func() { defer println("go1 defer...") for i := 0; i < 10; i++ { println("go1:", i) if i == 5 { runtime.Goexit() } } }() go func() { println("go2") }() time.Sleep(3 * time.Second) } 输出 : go1: 0 go1: 1 go1: 2 go1: 3 go1: 4 go1: 5 go1 defer... go2 83 第 7 章 程序结构 7.1 源⽂件 Go 编译器⼯具对项⺫⺫录结构特殊的约定。另外,建议使⽤ go fmt 格式化源码,以确保编码⻛格 统⼀。 7.1.1 格式 编码 : 源⽂件以 UTF-8 编码存储。 结束 : ⾏尾的 ";" 多数情况下可以省略。 注释 : ⽀持 /**/ 和 // 两种注释⽅式,不能嵌套。 命名 : camelCasing ⻛格,不建议⽤下划线连接多个单词。 7.1.2 ⺫录结构 Go 不再使⽤ Makefile,⽽是改⽤ "go " ⼯具集。想要正确运⾏这些⼯具,⾸先得设 置好相应的环境参数,⽽且项⺫⺫录结构也有硬性规定。 环境参数设置 : • GOROOT: Go 安装⺫录,搜索标准库时需要⽤到。 • GOPATH: 项⺫存放⺫录, import 通过该环境变量搜索库静态⽂件。 • 通常将⾸个路径作为第三⽅库的安装⺫录。 • 如不被其他项⺫引⽤,则⽆需将项⺫路径添加到该设置。但也会导致 go install ⽆法⼯作。 GOROOT=/usr/local/go GOPATH=$HOME/golib:$HOME/projects export GOROOT GOPATH ⺫录结构: • bin: go install 编译安装的可执⾏⽂件保存⺫录。 • pkg: go install 编译安装的⼆进制静态包⽂件 (.a) 保存⺫录。 • src: 项⺫源码⺫录 (每个项⺫⼀个⼦⺫录 )。 |_ bin |_ pkg |_ src |_ proj1 |_ proj2 84 ⼤的项⺫通常由多个包组成,我们可以创建下级⼦⺫录保存。 |_ bin |_ pkg |_ src |_ blog |_ main.go |_ engine |_ logic 7.2 包 Go 以 package 来组织代码,类似 Python package。 7.2.1 创建包 所有的代码都必须组织在 package 中。 • 在源码的第⼀⾏以 "package " 声明包名称 (相当于 namespace)。 • 同⼀个包可以由多个 .go 源⽂件组成。 • 包名和其所在⺫录名可以不同。 go build 默认以⺫录名作为静态包 (*.a) 主⽂件名。 • 在包内部  (包括多个源⽂件之间 ) 可以访问所有成员及其字段。 • 包内仅⾸字⺟⼤写的标识符可以被导出,相当于 public 权限。 • 通常包的全部⽂件  (源⽂件和测试⽂件 ) 都放在独⽴⺫录中,使⽤ "go install" 编译和安装。 • 可执⾏程序必须包含 package main,其内部有 main ⼊⼝函数。 有关包成员的访问权限设置, Go 采⽤了和 Python 类似的做法。 Python ⽤下划线标识 private, Go 则⽤⼤写⾸字⺟来确定 public。这个规则适⽤于全局变量、全局常量、类型、结构字段、函 数、⽅法等。 • 以⽆参数⽆返回值的 main.main() 函数作为程序⼊⼝函数; • 使⽤ os.Exit(0) 返回终⽌进程; • ⽤ os.Args 获取命令⾏启动参数。 7.2.2 导⼊包 使⽤包前,必须使⽤ import 导⼊包静态⽂件路径。 包路径 = 相对路径 /静态包主⽂件名 (不包含 .a 扩展名 ) 85 相对路径是指标准库路径和 GOPATH 环境参数所设置的路径列表。⽐如在我的电脑编译环境中, import "io/ioutil" 表⽰导⼊ "/usr/local/go/pkg/darwin_amd64/io/iotuil.a" 静态库。 包的库⽂件名和包名可能不同。导⼊⽤包静态库⽂件名 (filename.a),⽽调⽤成员则必须⽤包名 (package name, namespace)。这和 .NET 差不多, dll 库⽂件和 namespace 并没有⼀致关系。 和 Python import ⼀样,⽀持多种导⼊⽅式。可以设定别名,或者导⼊包的全部 public 成员。 import "lib/math" // 正常模式 : math.Sin import M "lib/math" // 别名模式 : M.Sin。是对包设定别名,⽽不是导⼊路径。 import . "lib/math" // 简便模式 : Sin。相当于 python 的 from lib.math import * import _ "lib/math" // 丢垃圾桶 : 这回不怕未使⽤包导致编译错误了,还可⽤来调⽤其初始化⽅法。 使⽤ "_" 导⼊的⺫的,主要是为了执⾏该包的 init 初始化函数。 对于多级⺫录结构的项⺫,我们还可以使⽤ local 导⼊⽅式。⽐如当前项⺫下有个 test 包。 package main import ( "./test" ) func main() { test.Hello() } 但你会发现该模式仅在 "go run" 时有效, build 编译会导致如下错误。 $ go build can't load package: main.go:4:2: local import "./test" in non-local package 7.2.3 包初始化 Go ⽀持初始化函数 : • 每个源⽂件都可以定义⼀个或多个初始化函数 func init() {}。 • 所有初始化函数都会在 main() 之前,在单⼀线程上被调⽤,仅执⾏⼀次。 • 编译器不能保证多个初始化函数的执⾏次序。 • 初始化函数在当前包所有全局变量初始化  (零或初始化表达式值 ) 完成后执⾏。 • 不能在程序代码中直接或间接调⽤初始化函数。 package main import ( "strings" "time" ) 86 func init() { println(a) // 在全局变量初始化完成后才会执⾏,否则初始化就有问题了 …… println("init over...") } var a string = strings.Join([]string{"a", "b"}, ",") func init() { time.Sleep(2 * time.Second) // main 必须等待初始化完成后才被执⾏,包括其他包中的初始化函数。 println("init2 over...") } func main() { println("Hello, World!") } 输出 : a,b init over... init2 over... Hello, World! 可以在初始化函数中执⾏ goroutine,如果期望 init 等待 goroutine 结束,可以⽤ channel。 package main import ( "time" ) func init() { go func() { println("goroutinue in init...") time.Sleep(1 * time.Second) }() println("init over...") } func main() { println("Hello, World!") time.Sleep(2 * time.Second) // 等待 goroutines 执⾏结束后才退出,否则看不到效果了。 } 输出 : init over... Hello, World! goroutinue in init... 初始化函数只能⽤来完成初始化该做的活,搞太多事情不符合设计初衷 …… 87 7.2.4 包⽂档 package 声明上⾯的注释将被当作包的说明⽂档,可以⽤ "go doc" 查看。 /* package main doc... */ package main import ( "fmt" ) func main() { fmt.Println("Hello, GoLang!") } 注释⽂档和 package 声明之间不能有空⾏。可以在包的多个⽂件中添加⽂档说明。 $ go doc PACKAGE package main import "src/main" package main doc... package main doc2... 同样,紧邻类型和函数的⽂档也视作 go doc。 7.3 测试 Go ⾃带了测试框架和⼯具,很容易完成单元测试和性能测试。 testing 中提供了 T 类型⽤于单元测试, B 类型⽤于性能测试。测试代码被放在 *_test.go ⽂件中, 通常和被测试代码属于同⼀个包,以便访问私有成员。 7.2.1 Test 在测试源⽂件中,测试函数名称必须是 "Test[^a-z]" 这样的格式。以 "Test" 开头,后⾯跟上⾮⼩写 字⺟开头的字符串。就是⽤⼤写字⺟、下划线或者数字什么的确定前⾯这个 Test 是个独⽴单词,好 让 go test 能知道这是个测试函数。像 "Testabc" 这样的,估计丫搞不清是⼀个测试函数,还是⼀ 个普通函数。 测试函数接收⼀个 *testing.T 类型参数,⽤于输出信息或中断测试。 88 main_test.go package main import ( "testing" ) func TestSum(t *testing.T) { if sum(1, 2, 3) != 16 { t.Log("我就是有意搞错滴,你想怎么地 !") t.FailNow() } } 运⾏ "go test" 开始测试,它会⾃动搜集所有的测试源⽂件 (_test.go),提取全部测试函数。 $ go test --- FAIL: TestSum (0.00 seconds) mypkg_test.go:9: 我就是有意搞错滴,你想怎么地 ! FAIL exit status 1 FAILmypkg0.025s 参数 : • -v: 显⽰所有测试函数运⾏细节。 • -run=regex: 指定要执⾏的的测试函数。 输出结果给出了出错测试函数名称、执⾏时间以及错误信息。 7.2.2 Benchmak 性能  (Benchmark) 也是及重要的测试项⺫。函数以 Bechmark 开头,参数类型是 *testing.B 。 参数 b.N 是测试循环次数。为什么不是 go test 循环调⽤ N 次 BenchmarkSum 呢?可能是为了避 免循环执⾏测试函数本⾝造成过多的性能消耗。 func BenchmarkSum(b *testing.B) { for i := 0; i < b.N; i++ { sum(1, 2, 3) } } 默认情况下, go test 不执⾏ Benchmark 测试,必须⽤ "-bench=" 指定性能测试的函 数。 Test 和 Benchmark 函数可以放在同⼀源⽂件中。 main_test.go package main 89 import ( "testing" ) func TestSum(t *testing.T) { if sum(1, 2, 3) != 16 { println("jaha") //t.Log("我就是有意搞错滴,你想怎么地 !") //t.FailNow() } } func BenchmarkSum(b *testing.B) { for i := 0; i < b.N; i++ { sum(1, 2, 3) } } $ go test -v -bench="." === RUN main.TestSum jaha --- PASS: main.TestSum (0.00 seconds) PASS main.BenchmarkSum20000000 134 ns/op 7.2.3 Example Example Test 类似 Python doctest,通过匹配 stdout 输出结果来判断测试成功或失败。在 Example 函数尾部⽤ "// Output: " 来标注正确输出结果。 注意:必须⽤ fmt.Print 系列函数。 add_test.go package main import ( "fmt" "testing" ) func TestX(t *testing.T) { t.Fail() } func ExampleAdd() { a := Add(1, 10) fmt.Println(a) // Output:11 } 90 输出 : $ go test -v === RUN TestX --- FAIL: TestX (0.00 seconds) === RUN: ExampleAdd --- PASS: ExampleAdd (0.00 seconds) FAIL exit status 1 FAILlearn0.026s $ go test -v -run "Example" === RUN: ExampleAdd --- PASS: ExampleAdd (0.00 seconds) PASS ok learn0.027s 91 第 8 章 进阶 8.1 运⾏时 Go 编译的可执⾏⽂件都包含了⼀个运⾏时 (runtime),和我们习惯的 Java/.NET VM 有些类似。 运⾏时负责内存分配 (Stack Handing、 GC Heap)、垃圾回收 (Garbage Collection)、 Goroutine 调度 (Schedule)、引⽤类型 (slice、 map、 channel) 管理,以及反射 (Reflection) 等⼯作。 Go 程 序进程启动后就⾃动创建两个 goroutine,分别⽤于执⾏ main ⼊⼝函数和 GC Heap 管理。 也正是因为编译⽂件中嵌⼊了运⾏时,使得其可执⾏⽂件相较其他语⾔更⼤⼀些。但 Go 的⼆进制 可执⾏⽂件都是静态编译的,⽆需其他任何链接库等⽂件,更利于分发。 (src/pkg/runtime/mgc0.c, mheap.c, proc.c) 8.2 内存分配 Go 在 Stack 和 GC Heap 上分配内存,⽆需⼿动释放内存。通常情况下,我们⽆需关⼼,或者说⽆ 法明确知道内存是分配在 Stack 还是 GC Heap 上。 • 简单规则:如果对象 "很⼤ ",或者有获取对象地址的操作,那么就分配到 GC Heap 上。 • 官⽅⽂档:正在努⼒将尽可能多的对象放在 stack frame 上,以提⾼性能。 How do I know whether a variable is allocated on the heap or the stack? From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language. The storage location does have an efect on writing efcient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack. In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack. 92 除了直接定义变量外,还可⽤ new 和 make 来分配内存。 • new: ⽤于值类型,返回⼀个被初始化的内存块指针。 • make: ⽤于引⽤类型 slice、 map、 channel,分配内存并设置属性,返回对象⽽⾮指针。 引⽤类型对象⾄少由 2 块内存组成:引⽤和内容。 引⽤内存块存储得并不⼀定就是指针,也可能是⼀个结构化元数据对象,由指向内容对象的内存指 针、数据项⻓度、容量等描述信息组成。⽐如 slice 的元数据就包含了其在底层数组的开始指针、⻓ 度和容量。不过, map 和 channel 的引⽤就只有⼀个指针。 另外,你可能会发现哪怕是简单的 "Hello, World!",其虚拟内存占⽤也很恐怖。这仅仅是 runtime 内存分配器的预留⾏为,并不会真的占⽤物理内存。 http://tip.golang.org/doc/faq#Why_does_my_Go_process_use_so_much_virtual_memory 8.3 内存布局 了解对象的内存布局,对于理解值传递、引⽤类型等概念有极⼤帮助。 参考资料 : • http://research.swtch.com/2009/11/go-data-structures.html • http://www.softwareresearch.net/fileadmin/src/docs/teaching/SS10/Sem/ Presentation_Aigner_Baumgartner.pdf • src/pkg/runtime/runtime.h。 8.3.1 基本数据类型 和 C 对应类型的内存结构基本相同。 i := 1234 1 3.14 h e l l o 1234 2 3 5 7 j := int32(1) f := float32(3.14) bytes := [5]byte{‘h’, ‘e’, ‘l’, ‘l’, ‘o’} primes := [4]int{2, 3, 5, 7} 注意:在不同平台, int ⻓度会有所区别,另外还有内存对⻬⾏为。 93 8.3.2 字符串 字符串对象存储的是指向⼀个 UTF-8 字节数组的指针,⾃带⻓度信息。 字节数组没有 NULL 结 尾,切⽚操作返回的依然是 string 对象。 h e l l o ptr len = 1 s := “hello”ptr len = 5 t := s[2:3] [5]byte 8.3.3 结构 和 C struct 内存布局基本⼀致。并没有 Python/C# Object 对象头的那些引⽤计数、类型指针等附 加字段。 type Point struct { X, Y int } 10 20 p := Point{10, 20}10 20 Point ptr pp := &Point{10, 20} 匿名嵌⼊的结构和原结构是⼀个整体。如果是嵌⼊匿名指针,那么存储的就是指针。 type Rect1 struct { Min, Max Point } type Rect2 struct { Min, Max *Point } 50 60 10 20 r1 := Rect1{Point{10, 20}, Point{50, 60}} 10 20 Rect2ptr Point r2 := Rect2{&Point{10, 20}, &Point{50, 60}} ptr 50 60 Rect1 94 8.3.4 Slices slice 内部指针指向某个底层数组的某个元素。下图⽰例中, x 和 y 指向同⼀个底层数组。 cap = 5 2 3 x := []int{2, 3, 5, 7, 11} ptr len = 5 [5]int5 6 []int 11 cap = 4ptr len = 2 y := x[1:3] []int 8.3.5 new new 返回⼀段已经被初始化的 GC Heap 内存块指针。 type Rect1 struct { Min, Max Point } type Rect2 struct { Min, Max *Point } new(Point) ptr 0 0 Point ptr 00 0 0 Rect1 00 0ptr [3]int ptr nil nil Rect2 new(Rect1) new(Rect2) new([3]int) 8.3.6 make make 通常要分配引⽤和内容两块内存,还得设置⼀些附加属性。返回⺫标对象,⽽⾮指针。 95 cap = 0ptr len = 0 []int [0]int make([]int, 0) 00 0 0 [5]int0 cap = 5ptr len = 2 []int make([]int, 2, 5) 8.4 反射 反射  (reflection) 可⼤⼤提⾼程序开发的灵活性,借助于反射让静态语⾔具备更加多样的运⾏期动 态特征。反射让程序具备了⾃省能⼒,这使得 interface{} 有更⼤的发挥余地。 反射使⽤ TypeOf 和 ValueOf 函数从 interface{} 对象获取实际⺫标对象的类型和值信息。 func TypeOf(i interface{}) Type func ValueOf(i interface{}) Value 8.4.1 从接⼝反射获取 Type、 Value 试着利⽤反射显⽰⼀个 "未知 " 结构类型的字段、⽅法等信息。 type User struct { Id int Name string Age int Title string } func (this User)Test(x int) { println("User.Test:", this.Name, x) } func test(i interface{}) { t := reflect.TypeOf(i) v := reflect.ValueOf(i) // Type Kind if k := t.Kind(); k != reflect.Struct { fmt.Println(k) return 96 } // Type Name println("Type:", t.Name()) // Fields println("Fields:") for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i).Interface() fmt.Printf(" %6s: %v = %v\n", field.Name, field.Type, value) } // Methods println("Methods:") for i := 0; i < t.NumMethod(); i++ { method := t.Method(i) fmt.Printf(" %s: %v\n", method.Name, method.Type) } } func main() { u := User{ 1, "Tom", 30, "Programmer" } test(u) } 输出 : Type: User Fields: Id: int = 1 Name: string = Tom Age: int = 30 Title: string = Programmer Methods: Test: func(main.User, int) 8.4.2 利⽤ Value 修改原对象 想要利⽤反射修改对象状态,前提是 interface.data 是 settable。很显然,这通常指 pointer- interface。 注意 ValueOf(pointer-interface) 返回的是⼀个 Pointer,也就是接⼝对象保存的 *data 内容。要 想操作⺫标对象,需要⽤ Elem() 进⼀步获取指针指向的实际⺫标。 func main() { x := 123 v := reflect.ValueOf(&x) v.Elem().SetInt(1000) println(x) 97 } 输出 : 1000 正式点说,我们应该⽤ Kind() 来判断是否是 Pointer,还需判断 CanSet() 为真。如果不是 Ptr,调 ⽤ Elem() 会出错。 type User struct { Id int Name string Age int Title string } func test(i interface{}) { v := reflect.ValueOf(i) // ⾸先判断是 Ptr 类型 // ⽤ Elem() 获取 ptr 指向的实际对象,并判断是 settable。 if v.Kind() != reflect.Ptr || !v.Elem().CanSet() { println("Cannot Set!") return } else { v = v.Elem() // 我们要操作的是⺫标对象,⽽不是 ptr。 } field := v.FieldByName("Name") if field.Kind() == reflect.String { field.SetString("Jack") } } func main() { u := User{ 1, "Tom", 30, "Programmer" } test(&u) fmt.Println(u) } 输出 : {1 Jack 30 Programmer} 8.4.3 调⽤⽅法 ⽤反射可以 "动态 " 调⽤⽅法,就是处理参数稍微⿇烦了⼀些。⽽且⽆法获取的⽅法的更多细节信 息,聊胜于⽆。 type User struct { Id int Name string Age int Title string 98 } func (this User) Test(x int) string { return fmt.Sprintf("user.name: %s; x: %d", this.Name, x) } func main() { var o interface{} = &User{1, "Tom", 30, "Programmer"} v := reflect.ValueOf(o) m := v.MethodByName("Test") rets := m.Call([]reflect.Value{reflect.ValueOf(1234)}) s := rets[0].Interface().(string) fmt.Printf("%T, %v\n", s, s) } 8.5 cgo cgo 让 Go 可以⾮常简单地融合 C 代码。 在 import "C" 上⾯添加特殊注释来包含 C 相关代码,注意不能有空⾏。还可以设置 CFLAGS、 LDFLAGS 等 gcc 编译参数。 /* #cgo CFLAGS: -O3 #cgo LDFLAGS: -lpthread #include #include #include #include "cfunc.h" void print_thread() { printf("thread: %lu\n", (unsigned long)pthread_self()); } */ import "C" // 中间不能有空⾏,否则出错。 import ( "fmt" "unsafe" "runtime" ) func PrintThreadId() { C.print_thread() } func Test(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) 99 C.test(cs) } cfunc.h #ifndef CFUNC_H #define CUFNC_H void test(char *s); #endif cfunc.c #include #include void test(char *s) { printf("%s\n", s); } 使⽤ CString 创建的 C 字符串必须调⽤ C.free() 主动释放。 我们可以直接使⽤ C malloc/calloc 分配的内存,不过记得释放,它们不归 Go runtime/gc 管。 package main /* #include */ import "C" import ( "fmt" "unsafe" ) func main() { var nobj, size C.size_t = 10, (C.size_t)(unsafe.Sizeof(0)) p := C.calloc(nobj, size) defer C.free(p) a := (*[10]int)(p) a[5] = 10 a[7] = 35 fmt.Println(*a) } 输出 : [0 0 0 0 0 10 0 35 0 0] 100 第 9 章 ⼯具 9.1 命令⾏⼯具 (1) go build: 编译包 在临时⺫录下创建包编译后的⼆进制⽂件。该命令不会将⼆进制⽂件安装到 bin、 pkg ⺫录,但会 将包含 main ⼊⼝函数的可执⾏⽂件拷⻉到 src ⺫录。 go build 会检查并编译所有依赖包。 参数 : • -gcflags: 传递给 6g 编译器的参数。 • -ldflags: 传递给 6l 链接器的参数。 • -work: 查看临时⺫录。通常在编译完成后被删除。 • -n: 查看但不执⾏编译命令。 • -x: 查看并执⾏编译命令。 • -a: 强制编译所有依赖包。 • -v: 查看被编译的包名 (包括依赖包 )。 gcflags: • -N -l: 禁⽤编译器内联优化,调试⽤。 • -m: 输出内联信息。 ldflags: • -s: 删除符号表。 • -w: 删除 DRAWF 调试信息 (不包括符号表 )。 • -u: 禁⽤ unsafe 包。 附 : 除⾃带的 gc 编译器外,还可以选择 gccgo。其汇编质量远远⾼出 gc,当然在 goroutine 的⽀ 持上还是 gc 的策略好⼀些。编译时必须使⽤静态链接,否则导致失败。 (在 Ubuntu 环境下,可以 直接⽤ apt-get 安装 gccgo) $ go build -compiler gccgo /usr/bin/ld: cannot find -lgcc_s collect2: error: ld returned 1 exit status $ go build -compiler gccgo -gccgoflags '-static-libgcc -O3'# O3 优化 (2) go install: 编译并安装包 和 go build 参数相同,唯⼀的区别在于将编译结果拷⻉到 bin、 pkg ⺫录中。 101 建议:不要设置 GOBIN 环境变量,否则会优先将编译结果安装到该变量所指定的⺫录。 (3) go clean: 垃圾清理 清除当前包⺫录下的编译临时⽂件。 参数 : • -n: 查看但不执⾏清理命令。 • -x: 查看并执⾏清理命令。 • -i: 同时删除 bin、 pkg ⺫录下 go install 安装的⼆进制⽂件。 • -r: 同时清理所有依赖包 src ⺫录下的临时⽂件。 (4) go get: 下载并安装扩展包 从⺴络下载并安装第三⽅扩展包。 参数 : • -d: 下载源码后,并不执⾏安装命令。 • -u: 检查并下载源码更新⽂件。 • -x: 查看并执⾏命令。 go get ⽀持 go install 的相关参数。 (5) go list: 查看包信息 查看包名、路径、依赖项等等信息。 参数 : • -json: 使⽤ json 格式输出包的相关信息,包括下⾯的依赖和导⼊。 • -f {{.Deps}}: 查看依赖包,包括直接或间接依赖。 • -f {{.Imports}}: 查看导⼊的包。 (6) gofmt: 调整源码格式 依照官⽅格式规范格式化源码⽂件。 参数 : • -w: 格式化结果写回原⽂件。 • -tabs=false: 不使⽤ TAB 缩进。 • -tabwidth=4: 缩进⻓度。 102 • -comments=false: 去掉注释。慎⽤,会连 doc ⼀同去掉。 ⽰例 : $ gofmt -tabs=false -tabwidth=4 -w *.go (7) go env: 查看 Go 环境变量 $ go env GOROOT="/usr/local/go" GOBIN="" GOARCH="amd64" GOCHAR="6" GOOS="darwin" GOEXE="" GOHOSTARCH="amd64" GOHOSTOS="darwin" GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64" GOGCCFLAGS="-g -O2 -fPIC -m64 -pthread -fno-common" CGO_ENABLED="1" (8) godoc: 查看包或成员帮助信息 查看某个具体包成员帮助信息。 $ godoc fmt Printf package fmt import "fmt" FUNCTIONS func Printf(format string, a ...interface{}) (n int, err error) Printf formats according to a format specifier and writes to standard output. It returns the number of bytes written and any write error encountered. 查看源码 $ godoc -src fmt Printf // Printf formats according to a format specifier and writes to standard output. // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } 启动⼀个 WebServer,查看官⽅⽂档。 $ godoc -http=:6060 103 (9) go fix: 修复源码 当新版本的 Go 发布后,可以⽤该命令修改因为语⾔规范变更造成的语法错误。 (10) go vet: 源码检查 ⽐如检查源⽂码中 Printf 等语句的 format 和 args 匹配情况。 package main import ( "fmt" ) func main() { fmt.Printf("%d, %s\n", "abc") } 输出 : $ go vet main.go main.go:8:2: wrong number of args in Printf call: 2 needed but 1 args go tool vet: exit status 1 9.2 GDB 调试 默认情况下,编译过的⼆进制⽂件已经包含了 DWARFv3 调试信息,只要 GDB 7.1 以上版本都可以 进⾏调试。 在 OSX 下,如⽆法执⾏调试指令,可尝试⽤ sudo ⽅式执⾏ gdb。 删除调试符号: go build -ldflags "-s -w" • -s: 去掉符号信息。 • -w: 去掉 DWARF调试信息。 关闭内联优化: go build -gcflags "-N -l" 调试相关函数: • runtime.Breakpoint():触发调试器断点。 • runtime/debug.PrintStack():显⽰调⽤堆栈。 • log:适合替代 print 显⽰调试信息。 GDB 调试⽀持: • 参数载⼊: gdb <filename> -d $GCROOT。 • ⼿⼯载⼊: source pkg/runtime/runtime-gdb.py。 104 更多细节,请参考 : http://golang.org/doc/gdb 调试演⽰: (OSX 10.8.2, Go 1.0.3, GDB 7.5.1) package main import ( "fmt" "runtime" ) func test(s string, x int) (r string) { r = fmt.Sprintf("test: %s %d", s, x) runtime.Breakpoint() return r } func main() { s := "haha" i := 1234 println(test(s, i)) } $ go build -gcflags "-N -l" // 编译,关闭内联优化。 $ sudo gdb demo // 启动 gdb 调试器,⼿⼯载⼊ Go Runtime。 GNU gdb (GDB) 7.5.1 Reading symbols from demo...done. (gdb) source /usr/local/go/src/pkg/runtime/runtime-gdb.py Loading Go Runtime support. (gdb) l main.main // 以 . ⽅式查看源码。 14func main() { 15 s := "haha" 16 i := 1234 17 println(test(s, i)) 18} (gdb) l main.go:8 // 以 : ⽅式查看源码。 8func test(s string, x int) (r string) { 9 r = fmt.Sprintf("test: %s %d", s, x) 10 runtime.Breakpoint() 11 return r 12} (gdb) b main.main // 以 . ⽅式设置断点。 Breakpoint 1 at 0x2131: file main.go, line 14. (gdb) b main.go:17 // 以 : ⽅式设置断点。 Breakpoint 2 at 0x2167: file main.go, line 17. (gdb) info breakpoints // 查看所有断点。 105 Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000002131 in main.main at main.go:14 2 breakpoint keep y 0x0000000000002167 in main.main at main.go:17 (gdb) r / / 启动进程,触发第⼀个断点。 Starting program: demo [New Thread 0x1c03 of process 4088] [Switching to Thread 0x1c03 of process 4088] Breakpoint 1, main.main () at main.go:14 14func main() { (gdb) info goroutines // 查看 goroutines 信息。 * 1 running runtime.gosched * 2 syscall runtime.entersyscall (gdb) goroutine 1 bt // 查看指定序号的 goroutine 调⽤堆栈。 #0 0x000000000000f6c0 in runtime.gosched () at pkg/runtime/proc.c:927 #1 0x000000000000e44c in runtime.main () at pkg/runtime/proc.c:244 #2 0x000000000000e4ef in schedunlock () at pkg/runtime/proc.c:267 #3 0x0000000000000000 in ?? () (gdb) goroutine 2 bt // 这个 goroutine 貌似跟 GC 有关。 #0 runtime.entersyscall () at pkg/runtime/proc.c:989 #1 0x000000000000d01d in runtime.MHeap_Scavenger () at pkg/runtime/mheap.c:363 #2 0x000000000000e4ef in schedunlock () at pkg/runtime/proc.c:267 #3 0x0000000000000000 in ?? () (gdb) c / / 继续执⾏,触发下⼀个断点。 Continuing. Breakpoint 2, main.main () at main.go:17 17 println(test(s, i)) (gdb) info goroutines // 当前 goroutine 序号为 1。 * 1 running runtime.gosched 2 runnable runtime.gosched (gdb) goroutine 1 bt // 当前 goroutine 调⽤堆栈。 #0 0x000000000000f6c0 in runtime.gosched () at pkg/runtime/proc.c:927 #1 0x000000000000e44c in runtime.main () at pkg/runtime/proc.c:244 #2 0x000000000000e4ef in schedunlock () at pkg/runtime/proc.c:267 #3 0x0000000000000000 in ?? () (gdb) bt // 查看当前调⽤堆栈,可以与当前 goroutine 调⽤堆栈对⽐。 #0 main.main () at main.go:17 #1 0x000000000000e44c in runtime.main () at pkg/runtime/proc.c:244 #2 0x000000000000e4ef in schedunlock () at pkg/runtime/proc.c:267 #3 0x0000000000000000 in ?? () (gdb) info frame // 堆栈帧信息。 Stack level 0, frame at 0x442139f88: rip = 0x2167 in main.main (main.go:17); saved rip 0xe44c called by frame at 0x442139fb8 source language go. 106 Arglist at 0x442139f28, args: Locals at 0x442139f28, Previous frame's sp is 0x442139f88 Saved registers: rip at 0x442139f80 (gdb) info locals // 查看局部变量。 i = 1234 s = "haha" (gdb) p s // 以 Pretty-Print ⽅式查看变量。 $1 = "haha" (gdb) p $len(s) // 获取对象⻓度 ($cap) $2 = 4 (gdb) whatis i / / 查看对象类型。 type = int (gdb) c / / 继续执⾏,触发 breakpoint() 断点。 Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. runtime.breakpoint () at pkg/runtime/asm_amd64.s:81 81 RET (gdb) n / / 从 breakpoint() 中出来,执⾏源码下⼀⾏代码。 main.test (s="haha", x=1234, r="test: haha 1234") at main.go:11 11 return r (gdb) info args // 从参数信息中,我们可以看到命名返回参数的值。 s = "haha" x = 1234 r = "test: haha 1234" (gdb) x/3xw &r / / 查看 r 内存数据。 (指针 8 + ⻓度 4) 0x442139f48:0x421212400x000000000x0000000f (gdb) x/15xb 0x42121240 // 查看字符串字节数组 0x42121240:0x740x650x730x740x3a0x200x680x61 0x42121248:0x680x610x200x310x320x330x34 (gdb) c / / 继续执⾏,进程结束。 Continuing. test: haha 1234 [Inferior 1 (process 4088) exited normally] (gdb) q!// 退出 GDB。 9.3 条件编译 Go 未提供 #if...#else,可通过 runtime 包的环境常量 GOOS、 GOARCH 等常量代替。 • GOOS: windows, darwin, linux, freebsd 107 • GOARCH: 386, amd64, arm 9.4 跨平台编译 对苦逼的码农⽽⾔,在同⼀个开发环境编译出不同平台所需的⼆进制可执⾏⽂件是⾮常必要的。 (1) ⾸先进⼊ go/src 源码所在⺫录,执⾏如下命令创建⺫标平台所需的包和⼯具⽂件。 $ cd /usr/local/go/src $ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./make.bash 如果是 Windows 则修改 GOOS 即可。 $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 ./make.bash (2) 现在可以编译 Linux 和 Windows 平台所需的执⾏⽂件了。 $ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build 不过该⽅式暂时不⽀持 CGO。 http://solovyov.net/en/2012/03/09/cross-compiling-go/ 9.5 性能测试 go tools 集成了 pprof,⽅便我们对程序进⾏性能测试,找出瓶颈所在。 import ( "os" "runtime/pprof" ) func main() { // CPU w, _ := os.Create("cpu.prof") defer w.Close() pprof.StartCPUProfile(w) defer pprof.StopCPUProfile() // Memory w2, _ := os.Create("mem.prof") defer w2.Close() defer pprof.WriteHeapProfile(w2) // code ... } 108 程序编译执⾏后,会⽣成 cpu.prof、 mem.prof 两个结果⽂件。 pprof 命令⾏参数: • text: 输出⽂本统计结果。 • web: ⽣成 svg 图形⽂件,并⽤浏览器打开。 (可能需要安装 graphviz) $ go tool pprof -text test cpu.prof Total: 443 samples 440 99.3% 99.3% 443 100.0% net.sotypeToNet 1 0.2% 99.5% 1 0.2% net.(*UDPConn).WriteToUDP 1 0.2% 99.8% 1 0.2% runtime.FixAlloc_Free 1 0.2% 100.0% 1 0.2% strconv.ParseInt 0 0.0% 100.0% 443 100.0% net.unixSocket 0 0.0% 100.0% 53 12.0% runtime.InitSizes 0 0.0% 100.0% 321 72.5% runtime.cmalloc 0 0.0% 100.0% 1 0.2% runtime.convT2E 0 0.0% 100.0% 1 0.2% runtime.gc 0 0.0% 100.0% 1 0.2% runtime.printeface 0 0.0% 100.0% 136 30.7% syscall.Kevent 0 0.0% 100.0% 97 21.9% syscall.Read 0 0.0% 100.0% 251 56.7% syscall.Syscall 0 0.0% 100.0% 136 30.7% syscall.Syscall6 0 0.0% 100.0% 154 34.8% syscall.Write 0 0.0% 100.0% 136 30.7% syscall.kevent pprof 通过统计采样数来衡量性能,⼤概每秒 100 次的频率。上⾯输出结果是 443,也就是说程序 ⼤约执⾏了 5 秒。 结果列表每⾏给出了⼀个函数的详细性能指标,按列分别对应: • 函数本地采样数量 (不包括其调⽤的其他函数 )。 • 函数本地采样数量所占百分⽐。 • 前⼏个函数 (包括当前函数 ) 本地采样总和所占百分⽐。 • 函数 (包括它调⽤的函数 ) 采样总数量。 • 函数采样总数量所占百分⽐。 采样数越⼤,意味着该函数执⾏时间越⻓,也可能就是问题所在。⾄于 svg 图⽚,可以观察各函数 之间的调⽤关系,更加直观,还⽀持图⽚缩放。 $ go tool pprof -web test cpu.prof 不带参数时,直接进⼊交互模式,可以⽤ top、 top10、 web 等指令。 另,测试 HTTP Server 应该选⽤ net/http/pprof。 109 第⼆部分 标准库 不得不说 Go 1.0.3 的标准库还有很多不完善的⼩细节⋯⋯ 这部分的⽂字和内容尚未校对,仅供参考。如发现错漏,敬请指正。 110 第 10 章 io 10.1 Interface 标准 io 包定义了 IO 操作所需的常⽤接⼝。 type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Seeker interface { Seek(offset int64, whence int) (ret int64, err error)// 0: Begin; 1: Current; 2: End } type Closer interface { Close() error } io 包除了相关接⼝,还提供了⼏个实⽤的类型。 • LimitedReader: 限制最多可读取数据的数量。 • MulitReader: 串联多个 Reader 供连续读取数据。 • TeeReader: 将读取的数据同步写⼊另外⼀个 Writer 中。 • SectionReader: 同时指定开始和结束位置的⽚段读取器。 • MultiWriter: 同时将数据写⼊多个 Writer 中。 io 和 io/ioutil 中另有很多便捷函数,可简化代码。 注意:所有的 Read 函数都可能同时返回数据和错误,⽐如⻓度不⾜的尾数据和 EOF。因此在处理 错误前,应该总是先检查返回值。另外, ReadAt/WriteAt 操作不会影响底层对象的 ofset 值。 10.2 Text File 通常使⽤ os.Open / Create 打开或创建⽂件,然后⽤ bufio 进⾏缓冲 IO 操作,以提⾼性能。下⾯ 是⼀个按⾏读取 UTF-8 格式⽂本⽂件的例⼦。 • Open: 只读 • OpenFile: 通过 flag 和 perm 控制打开模式。如: OpenFile("test.txt", os.O_RDWR, 600) • Create: 以读写⽅式创建⽂件,已有⽂件会被 Truncate(0)。 111 func main() { f, _ := os.Open("test.txt") defer f.Close() r := bufio.NewReaderSize(f, 4096) for { line, isPrefix, err := r.ReadLine() if isPrefix { print(string(line)) // 如果是⼀⾏的前部分,则不换⾏。 (⾏数据超出缓冲区⼤⼩ ) } else if len(line) > 0 { println(string(line)) } if err == io.EOF { break } } } 利⽤ fmt 包中的函数可以⾮常⽅便地读写格式化字符串。 10.3 Binary File 在 *nix 系统下,并不区分⽂本⽂件和⼆进制⽂件。如果我们希望读写⼆进制⽂件,则必须将相关类 型转换成 []byte。 下⾯例⼦中,使⽤ encoding/binary 在 Number 和 []byte 间进⾏转换。当然,还可以使⽤序列化 等⼿段⼀次性转换⼀个结构体。完成数据写操作,在关闭⽂件前,切记调⽤ bufio.Writer.Flush() 或 os.File.Sync() 函数将缓冲数据写回硬盘。 import ( "encoding/binary" "fmt" "log" "os" ) func checkError(err interface{}) { if err != nil { log.Fatal(err) } } func write() { f, err := os.Create("test.dat") checkError(err) defer func() { 112 f.Sync()// 必须显式调⽤ , Close 不会刷新。 f.Close() }() var i int32 = 0x1234 // 必须是具体⻓度的类型 checkError(binary.Write(f, binary.LittleEndian, i)) var d float32 = 0.1234 checkError(binary.Write(f, binary.LittleEndian, d)) var s string = "Hello, 中国 !" checkError(binary.Write(f, binary.LittleEndian, int32(len(s)))) // 先写⼊字符串⻓度 _, err = f.WriteString(s) // 写⼊字符串内容 checkError(err) } func read() { f, err := os.Open("test.dat") checkError(err) defer f.Close() var i int32 checkError(binary.Read(f, binary.LittleEndian, &i)) var d float32 checkError(binary.Read(f, binary.LittleEndian, &d)) var l int32 checkError(binary.Read(f, binary.LittleEndian, &l)) s := make([]byte, l) _, err = f.Read(s) fmt.Printf("%#x; %f; %s;\n", i, d, string(s)) } func main() { write() read() } 字符串除使⽤⻓度标记外,也可⽤结束字节标记,⽐如 '\n',然后⽤ bufio.Reader.ReadString 读 取。 10.4 Pipe 某些时候,需要在多个任务间进⾏数据流通讯。 io.Pipe 提供读写管道。 func main() { e := make(chan bool) r, w := io.Pipe() 113 go func() { for i := 0; i < 10; i++ { fmt.Fprintf(w, "data%d\n", i) } w.Close() }() go func() { for { var s string _, err := fmt.Fscanln(r, &s) if err == io.EOF { break } println(s) } e <- true }() <-e } 10.5 Encoding 除了 UTF-8 编码,我们通常还需要读写 GB、 UTF-16 等其他编码格式⽂件。标准库并没有内置字 符集转换包,须借助第三⽅扩展。 $ go get code.google.com/p/mahonia/ 先⽤ Python 准备⼀个 gb18030 的⽂件样本。 In [0]: import sys In [1]: reload(sys) In [2]: sys.setdefaultencoding("utf-8") In [3]: import codecs In [4]: f = codecs.open("test.txt", "w", "gb18030") In [5]: f.write("中国⼈ ") In [6]: f.close() 114 In [7]: !xxd test.txt 0000000: d6d0 b9fa c8cb ...... 导⼊ mahonia 包,创建 gb18030 Decoder 进⾏编码转换。 import ( "bufio" "code.google.com/p/mahonia" "log" "os" ) func checkError(err interface{}) { if err != nil { log.Fatal(err) } } func main() { f, err := os.Open("test.txt") checkError(err) defer f.Close() decoder := mahonia.NewDecoder("gb18030") r := bufio.NewReader(decoder.NewReader(f)) line, _, err := r.ReadLine() checkError(err) println(string(line)) } 除了 mahonia,还可以使⽤ go-charset,只是没这个⽅便罢了。 10.6 Bufer 除了⽂件,缓冲区是另⼀个最常⽤的 IO 操作了 —— bytes.Bufer。 import ( "bytes" "encoding/binary" "fmt" ) func main() { buffer := bytes.NewBuffer(nil) var i int32 = 0x1234 binary.Write(buffer, binary.LittleEndian, i) 115 buffer.WriteString("abc") fmt.Printf("% x\n", buffer.Bytes()) } 输出 : 34 12 00 00 61 62 63 默认情况下 (没有提供初始值 ), Bufer 使⽤⾃带的⼀个 [64]byte 数组作为存储空间。当超出限制 时,另创建⼀个 2x 的空间,并复制当前值。 UnreadByte 和 UnreadRune ⽅法的作⽤是忽略刚才的读操作,将内部 position 恢复到读操作以 前。 import ( "bytes" "io" ) func main() { buffer := bytes.NewBuffer([]byte{0, 1, 2, 3, 4}) c, _ := buffer.ReadByte() buffer.UnreadByte() println(c) for { c, err := buffer.ReadByte() if err == io.EOF { break } println(c) } } 输出 : 0 0 1 2 3 4 10.7 Temp 使⽤临时⽂件是常⽤的编程需求。 • os.TempDir: 返回系统临时⺫录。 • ioutil.TempDir: 在指定⺫录或系统临时⺫录下创建指定前缀名称的临时⺫录。 • ioutil.TempFile: 在指定⺫录或系统临时⺫录下创建指定前缀名称的可读写⽂件对象。 116 ioutil.TempDir 和 TempFile 可以确保不同的进程或者每次调⽤都唯⼀临时名称,但创建者必须负 责删除这些临时⺫录和⽂件。当不指定⺫标⺫录时,表⽰使⽤系统临时⺫录。 package main import ( "fmt" "io/ioutil" "os" ) func main() { f, _ := ioutil.TempFile("", "abc") defer func() { f.Close() os.Remove(f.Name()) }() fmt.Println(f.Name()) } 输出 : /var/folders/zb/xvwdb1dj4rv450lk847s_z200000gn/T/abc471645390 10.8 Path filepath.Abs 的作⽤是获取完整的绝对路径, 可以⽤ IsAbs 判断是否绝对路径。 ⾄于 "~/Documents" 这样的地址,则需要⽤到 os.ExpandEnv 展开。 package main import ( "path/filepath" "os" ) func main() { p1, _ := filepath.Abs("./demo.txt") p2, _ := filepath.Abs("~/demo.txt") p3 := os.ExpandEnv("$HOME/demo.txt") println(p1) println(p2) println(p3) println(filepath.IsAbs(p1), filepath.IsAbs("./demo.txt")) } 输出 : /Users/yuhen/Documents/go/src/main/demo.txt /Users/yuhen/Documents/go/src/main/~/demo.txt /Users/yuhen/demo.txt 117 true false Dir ⽤于返回⺫录名, Base 则返回⽂件全名, Ext 获取扩展名。 p, _ := filepath.Abs("./demo.txt") println(p) println(filepath.Dir(p)) println(filepath.Base(p)) println(filepath.Ext(p)) 输出 : /Users/yuhen/Documents/go/src/main/demo.txt /Users/yuhen/Documents/go/src/main demo.txt .txt Clean 则⽤于将路径名清理干净,这在拼接多级路径时很有⽤。 p := "..//test/../test/./demo.txt" println(filepath.Clean(p)) 输出 : ../test/demo.txt Rel 和 Abs 相反,⽤于获取相对路径。 p, _ := filepath.Abs("./a/b/c/demo.txt") d, _ := filepath.Abs("./a") r, _ := filepath.Rel(d, p) println(p) println(r) 输出 : /Users/yuhen/Documents/go/src/main/a/b/c/demo.txt b/c/demo.txt Split 将路径分割成 "路径 " 和 "⽂件名 " 两部分,⽽ Join 则其合并成为⼀个完整路径。 p1, _ := filepath.Abs("./demo.txt") dir, name := filepath.Split(p1) println(dir, ",", name) p2 := filepath.Join(dir, name) println(p2) 输出 : /Users/yuhen/Documents/go/src/main/ , demo.txt /Users/yuhen/Documents/go/src/main/demo.txt 不要误解 SplitList,它的实际⽤途是分解由特定符号分隔的多个路径。⽐如在 *nix 系统中⽤ ":" 分 隔多个 PATH 路径。分隔符由 os.PathListSeparator 常量定义。 118 path := os.Getenv("PATH") println(path) fmt.Printf("%#v\n", filepath.SplitList(path)) 输出 : /opt/local/bin:/opt/local/sbin:/usr/local:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin []string{"/opt/local/bin", "/opt/local/sbin", "/usr/local", "/usr/local/bin", "/usr/local/sbin", "/ usr/bin", "/bin", "/usr/sbin"} Match 和 Glob 都是通配符判断,语法规则简单: • *: 任意 n 个字符。 • ?: 0 或 1 个字符。 • []: 匹配括号内的任意字符。 • [^] 或 [!]: 不匹配括号内的任意字符。 • {,}: 匹配多组通配字符中的⼀个。 Glob ⽤于匹配当前⺫录下的⽂件,⽽ Match 则匹配字符串。如果需要使⽤特定语法字符,使⽤ "\" 转义。 注意匹配的⽂件名不能包含路径 ,可以⽤ Base 函数获取⽂件名。 m, _ := filepath.Glob("m*.go") fmt.Println(m) m2, _ := filepath.Match("*.go", "main.go") fmt.Println(m2) 输出 : [main.go] true Walk ⽤于遍历多级路径下的所有⽂件。包括 "." 在内的所有⺫录和⽂件都会触发 walkFunc,当该 函数返回有效错误 (err != nil) 时,遍历⽴即终⽌, Walk 会返回该错误。 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if !info.IsDir() { fmt.Printf("%s, %d\n", path, info.Size()) } return nil }) fmt.Println(err) 输出 : code/evaltest.go, 1234 code/mgotest.go, 1244 main, 1327120 main.go, 261 119 但如果是为了忽略某些⼦⺫录时,可以在 walkFunc 内直接返回 SkipDir。 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if info.IsDir() && strings.Contains(path, "code") { return filepath.SkipDir } fmt.Printf("%s, %d\n", path, info.Size()) return nil }) fmt.Println(err) 输出 : ., 170 main, 1327120 main.go, 331 EvalSymlinks 获取符号链接的真实⺫标⽂件路径。 p, _ := filepath.EvalSymlinks("/usr/local/s.sh") println(p) 输出 : /users/yuhen/Documents/OSX/config/s.sh FromSlash 和 ToSlash 的作⽤是在不同平台上转换路径表达⽅式,⽐如将 Windows "\a\b.txt" 转 换为 *nix 路径 "/a/b.txt",或反之。 120 第 11 章 strings 11.1 strconv Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。 import ( "strconv" "fmt" ) func main() { bs := make([]byte, 0, 100) bs = strconv.AppendInt(bs, 4567, 10) bs = strconv.AppendBool(bs, false) bs = strconv.AppendQuote(bs, "abcxx") bs = strconv.AppendQuoteRune(bs, '我 ') fmt.Println(string(bs)) } 输出 : 4567false"abcxx"'我 ' Format 和 Parse 在字符串和整数类型间进⾏转换,需要提供进制参数。 fmt.Println(strconv.FormatInt(1234, 2)) fmt.Println(strconv.FormatInt(1234, 8)) fmt.Println(strconv.FormatInt(1234, 10)) fmt.Println(strconv.FormatInt(1234, 16)) fmt.Println(strconv.ParseInt("10011010010", 2, 0)) fmt.Println(strconv.ParseInt("2322", 8, 0)) fmt.Println(strconv.ParseInt("1234", 10, 0)) fmt.Println(strconv.ParseInt("4d2", 16, 0)) Parse 函数另需要提供 bitSize 参数,⽤于检查转换结果是否超出我们预期的限制。⽐如 bitSize = 8,那么转换结果就不应该超出 8 位整数限制,否则转换出错。 (0 int; 8 int8; 16 int16; 32 int32; 64 int64) fmt.Println(strconv.ParseInt("127", 10, 8)) fmt.Println(strconv.ParseInt("128", 10, 8)) fmt.Println(strconv.ParseUint("255", 10, 8)) fmt.Println(strconv.ParseUint("256", 10, 8)) 输出 : 127 127 strconv.ParseInt: parsing "128": value out of range 255 18446744073709551615 strconv.ParseUint: parsing "256": value out of range 121 Atoi 和 Itoa 是相关函数的简便封装版本。 11.2 strings 忽略⼤⼩写判断字符串是否相同可以⽤ EqualFold。 strings.EqualFold("Abc", "abc") Fields ⽤于分割字符串,忽略多余的空格,提取有效字段。 fmt.Printf("%#v\n", strings.Fields(" name\tsex age address")) 输出 : []string{"name", "sex", "age", "address"} 没有 Startswith、 Endswith,其对应函数是 HasPrefix 和 HasSufx。 strings.HasPrefix("Abcde", "Ab") strings.HasSuffix("Abcde", "cde") SplitAfter 和 Split 的区别在于,前者会保留分隔符。 fmt.Printf("%#v\n", strings.Split("a,b,c,d", ",")) fmt.Printf("%#v\n", strings.SplitAfter("a,b,c,d", ",")) 输出 : []string{"a", "b", "c", "d"} []string{"a,", "b,", "c,", "d"} 11.3 template Go 提供了 string 和 html 两套模板引擎,使⽤⽅法⼤同⼩异。 11.3.1 基本语法 基本语法构成很简单。 • 标记 : {{ ... }} • 变量 : $name • 成员 : "." 访问 Key、 Field、 Method 成员。 • 迭代 : {{range}}...{{end}} • 判断 : {{if}}...{{else}}...{{end}} • 上下⽂ : {{with}}...{{end}} 默认情况下, "$" 总是指向 Execute() 传⼊的上下⽂对象, "." 指向当前上下⽂对象。 122 import ( "log" "os" "text/template" ) func tmpl(text string, data interface{}) { t, _ := template.New("test").Parse(text) if err := t.Execute(os.Stdout, data); err != nil { log.Fatalln(err) } } func main() { t := ` Data: {{.}} {{$}} Var、 Key: {{/* 注意变量使⽤ := 赋值 */}} {{$name := .name}} Data.Name = {{$name}} Range: Map : {{range $key, $value := $}} {{$key}} = {{$value}}; {{end}} {{/* 此处的 . 代表当前上下⽂,也就是 item */}} Slice: {{range .ints}} {{.}}, {{end}} {{/* 索引序号 */}} Slice: {{range $index, $value := .ints}} [{{$index}}] = {{$value}}, {{end}} {{/* ⽤内置函数 index 访问指定序号的元素 */}} Index: ints[2] = {{index .ints 2}} With: {{/* with 定义块上下⽂,可以减少前缀输⼊ */}} {{with .meta}} {{.X}} {{end}} IF: {{/* 0、 false、 nil, len = 0 的 string、 array、 slice、 map,或者不存在的成员都是 FALSE */}} {{if .ints}} Y {{else}} N {{end}} {{with .SomeOne}} Y {{else}} N {{end}} ` d := map[string]interface{}{ "name": "Q.yuhen", "ints": []int{1, 2, 3, 4}, "meta": struct{ X int }{100}, } tmpl(t, d) } 123 输出 : Data: map[ints:[1 2 3 4] meta:{100} name:Q.yuhen] map[ints:[1 2 3 4] meta:{100} name:Q.yuhen] Var、 Key: Data.Name = Q.yuhen Range: Map : ints = [1 2 3 4]; meta = {100}; name = Q.yuhen; Slice: 1, 2, 3, 4, Slice: [0] = 1, [1] = 2, [2] = 3, [3] = 4, Index: ints[2] = 3 With: 100 IF: Y N 11.3.2 函数调⽤ 在模版中调⽤函数,不需要括号和逗号分隔参数。 func main() { t := ` {{ $x := len $ }} Length = {{$x}} {{/* ⽤变量存储函数返回值 */}} {{ len $ | printf "Length = %d"}} {{/* ⽤管道将返回值传递给下⼀个函数 */}} ` tmpl(t, []int{0, 1, 2, 3}) } 除了内置函数,还可以直接调⽤数据对象⽅法和额外注⼊的函数。外部函数须提前注⼊。 type User struct { Id int Name string } func (this User) ToString(s string) string { return s + " = " + this.Name } func FormatUser(f string, u *User) string { return fmt.Sprintf(f, u) } func main() { tmpl := template.New("test") tmpl.Funcs(template.FuncMap{"FormatUser": FormatUser}) 124 tmpl.Parse(` Method : {{ .ToString "Username = " }} Function: {{ FormatUser "%#v" $ }} {{/* 多个参数之间,⽆需逗号分隔 */}} `) tmpl.Execute(os.Stdout, &User{1, "User1"}) } 输出 : Method : Username = User1 Function: &main.User{Id:1, Name:"User1"} 11.3.3 嵌套模板 模板可以包含多个⼦模板,以便形成嵌套输出。⼦模板要访问数据对象,则必须通过 {{template}} 参数传递。 func main() { root := ` T1: { {{template "T1" .}} {{/* 输出⼦模板内容,注意传递参数给⼦模板 */}} } T2: { {{template "T2" .age}} {{/* 输出⼦模板内容,注意传递参数给⼦模板 */}} } ` t1 := ` Name: {{.name}}; Age: {{.age}} ` t2 := ` Age: {{.}} ` tmpl, _ := template.New("root").Parse(root) tmpl.New("T1").Parse(t1) tmpl.New("T2").Parse(t2) d := map[string]interface{}{"name": "User1", "age": 18} tmpl.Execute(os.Stdout, d) } 输出 : T1: { Name: User1; Age: 18 } T2: { Age: 18 } 125 也可直接⽗模版中定义⼦模板,以实现复⽤和细分。 func main() { s := ` {{define "T1"}} Name: {{.name}}; Age: {{.age}} {{end}} {{define "T2"}} Age: {{.}} {{end}} T1 { {{template "T1" .}} } T2 { {{template "T2" .age}} } ` tmpl, _ := template.New("root").Parse(s) d := map[string]interface{}{"name": "User1", "age": 18} tmpl.Execute(os.Stdout, d) // 查看所有模版,包括⽗模版。 for _, sub := range tmpl.Templates() { println(sub.Name()) } } 输出 : T1 { Name: User1; Age: 18 } T2 { Age: 18 } root T1 T2 另有 ParseFiles 和 ParseGlob 函数⽤于载⼊模板⽂件。默认使⽤⽂件名作为模板名,并将第⼀参数 作为根模板。 root.txt Root: T1 {{template "t1.txt" .}} T2 {{template "t2.txt" .age}} 126 t1.txt Name:{{.name}}; Age:{{.age}} t2.txt Age:{{.}} func main() { tmpl, _ := template.ParseFiles("root.txt", "t1.txt", "t2.txt") d := map[string]interface{}{"name": "User1", "age": 18} tmpl.Execute(os.Stdout, d) } 输出 : Root: T1 Name:User1; Age:18 T2 Age:18 还可以直接在模板⽂件中使⽤ {{define}} 定义模板名以替代 "丑陋 " 的⽂件名。 root.txt {{define "Root"}} Root: T1 {{template "T1" .}} T2 {{template "T2" .age}} {{end}} t1.txt {{define "T1"}} Name:{{.name}}; Age:{{.age}} {{end}} t2.txt {{define "T2"}} Age:{{.}} {{end}} 不过这样⼀样,只能换⽤ ExecuteTemplate 了。 // t.Execute(os.Stdout, d) t.ExecuteTemplate(os.Stdout, "Root", d) 11.4 regexp Go 的正则表达式语法规则和 Python 基本相同,只是功能要弱许多,感觉是个半成品。 这个库的设计真的很难接受。 127 11.4.1. 基本语法 转义 : . ^ $ * + ? { } [ ] \ | ( )转义 : . ^ $ * + ? { } [ ] \ | ( ) 定义 :定义 : \d 数字,相当于 [0-9]。 \D ⾮数字字符,相当于 [^0-9]。 \s 空⽩字符,相当于 [ \t\r\n\f\v]。 \S ⾮空⽩字符。 \w 字⺟或数字,相当于 [0-9a-zA-Z]。 \W ⾮字⺟或数字。 . 任意字符。 | 或。 ^ ⾮,或者开始位置标记。 $ 结束位置标记。 \b 单词边界。 \B ⾮单词边界。 重复 :重复 : * 0 或任意多个字符。添加 ? 后缀避免贪婪匹配。 ? 0 或⼀个字符。 + 1 或多个字符。 {n} n 个字符。 {n,} 最少 n 个字符。 {,m} 最多 m 个字符。 {n, m} n 到 m 个字符。 编译 : 可以直接在表达式前部 添加 "(?iLmsux)" 标志编译 : 可以直接在表达式前部 添加 "(?iLmsux)" 标志 s 单⾏。 i 忽略⼤⼩写。 128 L 让 \w 匹配本地字符,对中⽂⽀持不好。 m 多⾏。 x 忽略多余的空⽩字符。 u Unicode。 11.4.2 Match Regexp 对象提供了对 []byte 和 string 的不同操作,使⽤⽅法相同。 reg, _ := regexp.Compile(`\d{2,}`) s := "ab0c1234d567x" fmt.Println(reg.MatchString(s)) 输出 : true 也可以直接使⽤简便函数。 m, _ := regexp.MatchString(`\d{2,}`, "ab0c1234d567x") 11.4.3 Find Find 仅返回⾸个匹配, FindAll 可指定最⼤匹配数量 (-1 全部 )。 reg, _ := regexp.Compile(`\d{2,}`) s := "ab0c1234d567x" fmt.Printf("%#v\n", reg.FindString(s)) fmt.Printf("%#v\n", reg.FindAllString(s, -1)) 输出: "1234" []string{"1234", "567"} 匹配失败时返回空字符串或空 slice。 reg, _ := regexp.Compile(`\d{20,}`) s := "ab0c1234d567x" r1 := reg.FindString(s) r2 := reg.FindAllString(s, -1) fmt.Printf("%#v\n", r1) fmt.Printf("%#v, %d\n", r2, len(r2)) 输出 : "" 129 []string(nil), 0 FindIndex 返回匹配结果的索引位置信息 (在原数据的起始和结束位置 )。匹配失败返回空 slice。 reg, _ := regexp.Compile(`\d{2,}`) s := "ab0c1234d567x" fmt.Printf("%#v\n", reg.FindStringIndex(s)) fmt.Printf("%#v\n", reg.FindAllStringIndex(s, -1)) 输出 : []int{4, 8} [][]int{[]int{4, 8}, []int{9, 12}} 11.4.4 Group 组操作没有 Python re ⽅便。没⽤ map,⽽是按照解析顺序⽤ slice 保存。 ⽅法 SubexpNames ⽅法返回所有的返回组名,其中包含命名组。 reg, _ := regexp.Compile(`(\d{2,})(?P[a-z]+)`) s := "ab0c1234d567x" fmt.Printf("%#v\n", reg.SubexpNames()) r := reg.FindAllStringSubmatch(s, -1) fmt.Printf("%#v\n", r) 输出 : []string{"", "", "letter"} [][]string{[]string{"1234d", "1234", "d"}, []string{"567x", "567", "x"}} 从输出结果来看,返回值分别对应 : {$0, $1, $2/letter}。 FindSubmatchIndex 则返回所有组的起 始和结束位置。 reg, _ := regexp.Compile(`(\d{2,})(?P[a-z]+)`) s := "ab0c1234d567x" r := reg.FindAllStringSubmatchIndex(s, -1) fmt.Printf("%#v\n", r) 输出 : [][]int{[]int{4, 9, 4, 8, 8, 9}, []int{9, 13, 9, 12, 12, 13}} 除了 (?P) 命名组和 (?:) ⾮捕获组外,貌似不⽀持反向引⽤和位置声明等⽤法。 reg, _ := regexp.Compile(`(?:\d{2,})(?P[a-z]+)`) s := "ab0c1234d567x" fmt.Printf("%#v\n", reg.SubexpNames()) r := reg.FindAllStringSubmatch(s, -1) fmt.Printf("%#v\n", r) 130 输出 : []string{"", "letter"} [][]string{[]string{"1234d", "d"}, []string{"567x", "x"}} 11.4.5 Replace ⽀持包含组引⽤的通⽤替换,函数替换,以及不解析组引⽤的 Literal 替换。 reg, _ := regexp.Compile(`(\d{2,})(?P[a-z]+)`) s := "ab0c1234d567x" r := reg.ReplaceAllString(s, "<$letter;$1>") fmt.Println(r) r2 := reg.ReplaceAllLiteralString(s, "<$1>") fmt.Println(r2) r3 := reg.ReplaceAllStringFunc(s, func(x string) string { return "r:" + x }) fmt.Println(r3) 输出 : ab0c ab0c<$1><$1> ab0cr:1234dr:567x 11.4.6 Expand 所谓 Expand 就是将匹配的结果按组序号或组名替换字符串模板变量。 模板参数使⽤ $ 或 $,必要时使⽤ ${number} 进⾏分隔。 有两个参数需要说明⼀下: • dst: 通常⽤ nil,除⾮将模板替换结果追加到其尾部。 • match: FindSubmatchIndex 的结果, Expand ⽅法根据其起始结束位置提取值。 reg, _ := regexp.Compile(`(\d{2,})(?P[a-z]+)`) s := "ab0c1234d567x" r := reg.ExpandString(nil, "template: $1; $letter;", s, reg.FindStringSubmatchIndex(s)) fmt.Printf("%#v\n", string(r)) 输出 : "template: 1234; d;" 131 第 12 章 compress 12.1 zlib 先⽤ Python zlib 库压缩⼀段⽂本,保存到⽂件中。 In [1]: import zlib In [2]: s = zlib.compress("Hello, World!") In [3]: s Out[3]: 'x\x9c\xf3H\xcd\xc9\xc9\xd7Q\x08\xcf/\xcaIQ\x04\x00\x1f\x9e\x04j' In [4]: f = open("test.dat", "w") In [5]: f.write(s) In [6]: f.close() In [7]: !xxd test.dat 0000000: 789c f348 cdc9 c9d7 5108 cf2f ca49 5104 x..H....Q../.IQ. 0000010: 001f 9e04 6a ....j 使⽤标准库 compress/zlib 解压缩。 package main import ( "bytes" "compress/zlib" "io/ioutil" ) func main() { data, _ := ioutil.ReadFile("test.dat") r, _ := zlib.NewReader(bytes.NewBuffer(data)) bs, _ := ioutil.ReadAll(r) println(string(bs)) } 输出 : Hello, World! 试试压缩数据。 package main import ( "bytes" "compress/zlib" 132 "fmt" "io" "io/ioutil" ) func main() { buffer := bytes.NewBuffer(nil) w := zlib.NewWriter(buffer) io.WriteString(w, "Hello, World!") w.Close() // 必须调⽤,确保刷新缓存。 fmt.Printf("% x\n", buffer.Bytes()) ioutil.WriteFile("test2.dat", buffer.Bytes(), 0644) } 输出 : 78 9c f2 48 cd c9 c9 d7 51 08 cf 2f ca 49 51 04 04 00 00 ff ff 1f 9e 04 6a 再⽤ Python 还原。 In [8]: s = open("test2.dat", "r").read() In [9]: zlib.decompress(s) Out[9]: 'Hello, World!' 12.2 zip zip 格式应该是在不同平台交换压缩数据⽂件的最佳选择了。 package main import ( "fmt" "archive/zip" "os" "io/ioutil" ) func ZipFile() { file, _ := os.Create("test.zip") defer file.Close() writer := zip.NewWriter(file) defer writer.Close() datas := []struct{ name, body string }{ {"a.txt", "Hello, World!"}, {"b.txt", "12345..."}, } // 直接创建压缩项,直接写⼊内容数据。 for _, data := range datas { 133 w, _ := writer.Create(data.name) w.Write([]byte(data.body)) } } func UnZipFile() { zr, _ := zip.OpenReader("test.zip") defer zr.Close() // 迭代压缩包所有内容 for _, f := range zr.File { // 获取当前压缩项内容 br, _ := f.Open() bs, _ := ioutil.ReadAll(br) fmt.Printf("%s: %s\n", f.Name, string(bs)) } } func main() { ZipFile() UnZipFile() } 输出 : a.txt: Hello, World! b.txt: 123... 我们还可以考虑⽤ CreateHeader 来替代 Create,以便让压缩项有修改时间、权限等信息。 func ZipFile() { file, _ := os.Create("test.zip") defer file.Close() writer := zip.NewWriter(file) defer writer.Close() for _, name := range []string{"a.txt", "b.txt"} { info, _ := os.Stat(name) header, _ := zip.FileInfoHeader(info) w, _ := writer.CreateHeader(header) body, _ := ioutil.ReadFile(name) w.Write(body) } } 134 第 13 章 flag 这个包的名字取得够古怪的,不符合惯例。 package main import ( "flag" ) var file string var count int func init() { flag.StringVar(&file, "file", "default.txt", "input filename.") flag.IntVar(&count, "count", 0, "process count.") flag.Parse() } func main() { println(file, count) } 可以使⽤ --help 查看参数帮助信息。 $ ./main --help Usage of ./main: -count=0: process count. -file="default.txt": input filename. 输出参数看看效果。 $ ./main --file "test.txt" --count=12 test.txt 12 帮助信息不好看?修改 flag.Usage 这个默认函数就⾏了。 func init() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "MyProgram, version 0.0.0.1\n") fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) flag.PrintDefaults() } flag.StringVar(&file, "file", "default.txt", "input filename.") flag.IntVar(&count, "count", 0, "process count.") flag.Parse() } func main() { 135 println(file, count) } 输出 : MyProgram, version 0.0.0.1 Usage of ./main: -count=0: process count. -file="default.txt": input filename. Args 获取⾮ flag 参数列表。这类参数必须放在 flag 参数后⾯。 func main() { println(file, count) fmt.Println(flag.Args()) } 输出 : $ ./main --file="a.txt" a 123 a.txt 0 [a 123] NFlag 返回命令⾏ flag 参数数量, NArg 则返回⾮ flag 参数数量。 还可以⽤ set 修改 flag 参数值。 func main() { if count == 0 { flag.Set("count", "100") } flag.Set("file", "new_"+file) println(file, count) } 输出 : $ ./main --file="a.txt" new_a.txt 100 $ ./main --file="a.txt" --count 20 new_a.txt 20 Visit ⽤于遍历命令⾏显式提供的 flag 参数, VisitAll 则遍历所有已定义的 flag 参数。 func main() { flag.VisitAll(func(f *flag.Flag) { fmt.Printf("%s = %v (default: %v)\n", f.Name, f.Value, f.DefValue) }) } 输出 : $ ./main --file="a.txt" count = 0 (default: 0) file = a.txt (default: default.txt) 136 不想遍历的话,就⽤ Lookup 返回指定名称的 flag 参数信息。 func main() { f := flag.Lookup("count") fmt.Printf("%s = %v (default: %v)\n", f.Name, f.Value, f.DefValue) } 137 第 14 章 crypto 14.1 md5 最痛苦的莫过于不同语⾔的 md5 结果却不相同。 In [1]: import md5 In [2]: m = md5.md5("Hello, World!") In [3]: m.hexdigest() Out[3]: '65a8e27d8879283831b664bd8b7f0ad4' 看看标准库 crypto/md5 的结果。 package main import ( "bytes" "crypto/md5" "fmt" "io" ) func main() { h := md5.New() io.WriteString(h, "Hello, World!") buffer := bytes.NewBuffer(nil) fmt.Fprintf(buffer, "%x\n", h.Sum(nil)) println(buffer.String()) } 输出 : 65a8e27d8879283831b664bd8b7f0ad4 14.2 des 虽说 DES 算法的加密强度有点问题,但应付常规应⽤是没问题的。注意确保 DES key ⻓度是 8 字 节, TDES key 是 24 字节。原数据⻓度则必须是 8 的倍数。 package main import ( "crypto/cipher" "crypto/des" "fmt" ) 138 func main() { key := []byte("12345678") s := "1234567812345678" block, _ := des.NewCipher(key) enc := cipher.NewCBCEncrypter(block, key[:8]) dec := cipher.NewCBCDecrypter(block, key[:8]) eb := make([]byte, len(s)) db := make([]byte, len(eb)) enc.CryptBlocks(eb, []byte(s)) fmt.Println(eb) dec.CryptBlocks(db, eb) fmt.Println(string(db)) } 输出 : [61 117 149 169 139 255 128 157 93 107 113 221 4 20 37 108] 1234567812345678 这个加密结果和 pyDes 相同。 >>> d = pyDes.des("12345678", pyDes.CBC, "12345678") >>> eb = d.encrypt("1234567812345678") >>> map(lambda c: ord(c), eb) [61, 117, 149, 169, 139, 255, 128, 157, 93, 107, 113, 221, 4, 20, 37, 108] >>> d.decrypt(eb) '1234567812345678' 14.3 rsa 不对称加密通常⽤来加密对称密钥。 package main import ( "crypto/rand" "crypto/rsa" "fmt" ) func main() { prv, _ := rsa.GenerateKey(rand.Reader, 256) pub := &prv.PublicKey // fmt.Printf("%#v\n%#v\n", pub, prv) 139 out, _ := rsa.EncryptPKCS1v15(rand.Reader, pub, []byte("Hello")) fmt.Printf("%#v\n", out) out, _ = rsa.DecryptPKCS1v15(rand.Reader, prv, out) fmt.Println(string(out)) } 输出 : &rsa.PublicKey { N:54526781523556354064704820438955314860952161264953685861088439140355575837137, E:65537 } &rsa.PrivateKey { PublicKey:rsa.PublicKey{...}, D:63864317099473362192452233347486304968037855213553751715089555401660594120673, Primes:[]*big.Int { 277824243110515704091598830358305669691, 196263583455048398895951925960918462307 }, } []byte{0x50, 0x8a, 0xb, 0xd0, 0x4b, 0x68, 0xcb, 0x20, 0x4, 0x59, 0x80, 0x3a, 0xe8, 0x10, 0x4e, 0xe5, 0x91, 0xac, 0xd, 0xba, 0xff, 0x17, 0x65, 0x66, 0x9d, 0xb, 0xea, 0x19, 0x9b, 0x1c, 0x4a, 0x46} Hello ⽤ Python rsa 解密,必须确保 key 相同。 In [1]: import rsa In [2]: prv = rsa.PrivateKey(54526781523556354064704820438955314860952161264953685861088439140355575837137, 65537, 63864317099473362192452233347486304968037855213553751715089555401660594120673, 277824243110515704091598830358305669691, 196263583455048398895951925960918462307) In [3]: d = [0x50, 0x8a, 0xb, 0xd0, 0x4b, 0x68, 0xcb, 0x20, 0x4, 0x59, 0x80, 0x3a, 0xe8, 0x10, 0x4e, 0xe5, 0x91, 0xac, 0xd, 0xba, 0xff, 0x17, 0x65, 0x66, 0x9d, 0xb, 0xea, 0x19, 0x9b, 0x1c, 0x4a, 0x46] In [4]: es = "".join(map(chr, d)) In [5]: rsa.decrypt(es, prv) Out[5]: 'Hello' 当然,反过来也是可以的。这回由 Python ⽣成 key。 In [1]: import rsa In [2]: pub, prv = rsa.newkeys(256) 140 In [3]: pub Out[3]: PublicKey(77382517088185642540198827044269957133089599436536059442470395728196745078087, 65537) In [4]: prv Out[4]: PrivateKey(77382517088185642540198827044269957133089599436536059442470395728196745078087, 65537, 1127611941639337910278009060946912568522140122080678641891404921503575643113, 61637561604096858245321635305261239680573, 1255444165446062450326558454000766419) In [5]: es = rsa.encrypt("world", pub) In [6]: %pprint Pretty printing has been turned OFF In [7]: map(ord, es) Out[7]: [161, 72, 236, 77, 102, 176, 138, 150, 72, 182, 116, 80, 53, 5, 156, 84, 28, 7, 222, 86, 57, 250, 117, 11, 225, 137, 120, 126, 160, 147, 208, 67] 注意 key 各字段的对应关系。 package main import ( "crypto/rand" "crypto/rsa" "fmt" "math/big" ) func fromBase10(base10 string) *big.Int { i := new(big.Int) i.SetString(base10, 10) return i } func getKeys() (prv rsa.PrivateKey, pub rsa.PublicKey) { pub = rsa.PublicKey{ N: fromBase10("77382517088185642540198827044269957133089599436536059442470395728196745078087"), E: 65537, } prv = rsa.PrivateKey{ PublicKey: pub, D: fromBase10("1127611941639337910278009060946912568522140122080678641891404921503575643113"), Primes: []*big.Int{ fromBase10("61637561604096858245321635305261239680573"), fromBase10("1255444165446062450326558454000766419"), }, } return 141 } func main() { prv, _ := getKeys() out, _ := rsa.DecryptPKCS1v15(rand.Reader, &prv, []byte{161, 72, 236, 77, 102, 176, 138, 150, 72, 182, 116, 80, 53, 5, 156, 84, 28, 7, 222, 86, 57, 250, 117, 11, 225, 137, 120, 126, 160, 147, 208, 67}) fmt.Println(string(out)) } 输出 : world 还可以利⽤ rsa 算法做签名验证,以确保数据未被篡改。 import ( "crypto" "crypto/md5" "crypto/rand" "crypto/rsa" "fmt" "io" ) func main() { hash := md5.New() io.WriteString(hash, "hello") d := hash.Sum(nil) prv, _ := rsa.GenerateKey(rand.Reader, 1024) pub := &prv.PublicKey fmt.Printf("%#v\n", pub) sig, _ := rsa.SignPKCS1v15(rand.Reader, prv, crypto.MD5, d) fmt.Printf("%#v\n", sig) if rsa.VerifyPKCS1v15(pub, crypto.MD5, d, sig) == nil { println("OK") } else { println("Error") } } 输出 : []byte{0x5d, 0x41, 0x40, 0x2a, 0xbc, 0x4b, 0x2a, 0x76, 0xb9, 0x71, 0x9d, 0x91, 0x10, 0x17, 0xc5, 0x92} &rsa.PublicKey{N: 810956637676809500341176551706545618687540961670053279518026033197679710896409170405625577920588965 191426234316973163482938614604243136558523906706992319077024954677968540528252090976444227159950819 046443994056433010872227208008113286501217707209141174992039933960428102768577286322991548807176388 74603801071, E:65537} 142 []byte{0xd, 0x89, 0xe, 0x91, 0x2, 0x7a, 0x92, 0x66, 0xa9, 0xf, 0xf3, 0xac, 0xf2, 0x4a, 0xf7, 0xef, 0x5a, 0xea, 0x5d, 0x98, 0x8a, 0x44, 0xcd, 0xf7, 0x65, 0xd8, 0x6d, 0x90, 0x9a, 0x76, 0x89, 0xa6, 0x55, 0x68, 0xa1, 0x78, 0x52, 0x48, 0x16, 0x2e, 0x27, 0x9c, 0xd4, 0xa2, 0x40, 0x5b, 0xf3, 0x82, 0x8e, 0x6f, 0xd4, 0x21, 0xe4, 0xe5, 0xb1, 0x6, 0x9f, 0x94, 0x4a, 0x28, 0x15, 0xf9, 0x41, 0x60, 0x3b, 0x32, 0xf8, 0xe8, 0xc1, 0x2f, 0x47, 0xf7, 0x5, 0x8c, 0x80, 0x64, 0x4c, 0x18, 0xa, 0xf3, 0x67, 0xd0, 0xb5, 0x29, 0x18, 0xc6, 0x4d, 0xb4, 0xe5, 0x33, 0xc6, 0xb5, 0x52, 0xd1, 0x5, 0xeb, 0xe3, 0xa0, 0xbd, 0x7c, 0xb9, 0x23, 0x76, 0x8c, 0x1b, 0x2d, 0x77, 0x7a, 0xae, 0x80, 0x75, 0xff, 0xd4, 0x4, 0xa6, 0x57, 0xe3, 0x60, 0xf5, 0x43, 0xc6, 0xcf, 0x64, 0x33, 0x3a, 0xdf, 0x6d, 0xa0} OK 试着⽤ Python rsa 验证签名。 In [1]: pub = rsa.PublicKey(8109566376768095003411765517065456186875409616700532795180260331976797108964091704056 255779205889651914262343169731634829386146042431365585239067069923190770249546779685405282520909764 442271599508190464439940564330108722272080081132865012177072091411749920399339604281027685772863229 9154880717638874603801071, 65537) In [2]: sig = "".join(map(chr, [0xd, 0x89, 0xe, 0x91, 0x2, 0x7a, 0x92, 0x66, 0xa9, 0xf, 0xf3, 0xac, 0xf2, 0x4a, 0xf7, 0xef, 0x5a, 0xea, 0x5d, 0x98, 0x8a, 0x44, 0xcd, 0xf7, 0x65, 0xd8, 0x6d, 0x90, 0x9a, 0x76, 0x89, 0xa6, 0x55, 0x68, 0xa1, 0x78, 0x52, 0x48, 0x16, 0x2e, 0x27, 0x9c, 0xd4, 0xa2, 0x40, 0x5b, 0xf3, 0x82, 0x8e, 0x6f, 0xd4, 0x21, 0xe4, 0xe5, 0xb1, 0x6, 0x9f, 0x94, 0x4a, 0x28, 0x15, 0xf9, 0x41, 0x60, 0x3b, 0x32, 0xf8, 0xe8, 0xc1, 0x2f, 0x47, 0xf7, 0x5, 0x8c, 0x80, 0x64, 0x4c, 0x18, 0xa, 0xf3, 0x67, 0xd0, 0xb5, 0x29, 0x18, 0xc6, 0x4d, 0xb4, 0xe5, 0x33, 0xc6, 0xb5, 0x52, 0xd1, 0x5, 0xeb, 0xe3, 0xa0, 0xbd, 0x7c, 0xb9, 0x23, 0x76, 0x8c, 0x1b, 0x2d, 0x77, 0x7a, 0xae, 0x80, 0x75, 0xff, 0xd4, 0x4, 0xa6, 0x57, 0xe3, 0x60, 0xf5, 0x43, 0xc6, 0xcf, 0x64, 0x33, 0x3a, 0xdf, 0x6d, 0xa0])) In [3]: rsa.verify("hello", sig, pub) 143 第 15 章 encoding 15.1 json 数据交互必不可少的东⻄,远⽐ XML 招⼈喜欢。 import ( "bytes" "encoding/json" "fmt" ) func main() { buffer := bytes.NewBuffer(nil) encoder := json.NewEncoder(buffer) decoder := json.NewDecoder(buffer) data := map[string]interface{}{ "s": "Hello, World!", "i": 1234, "m": map[string]int{"x": 1, "y": 2}, } encoder.Encode(data) fmt.Printf("%v\n", buffer.String()) d := make(map[string]interface{}) decoder.Decode(&d) // 注意使⽤指针,因为转换成 interface{} 时发⽣ value-copy。 fmt.Printf("%#v\n", d) } 输出 : {"i":1234,"m":{"x":1,"y":2},"s":"Hello, World!"} map[string]interface {}{"i":1234, "m":map[string]interface {}{"y":2, "x":1}, "s":"Hello, World!"} 还有简便函数 Marshal、 Unmarshal, Indent 则⽤于格式化输出结果。 import ( "encoding/json" "fmt" ) func main() { data := map[string]interface{}{ "s": "Hello, World!", "i": 1234, "m": map[string]int{"x": 1, "y": 2}, } d, _ := json.MarshalIndent(data, "", " ") fmt.Printf("%s\n", string(d)) 144 d2 := make(map[string]interface{}) json.Unmarshal(d, &d2) fmt.Printf("%#v\n", d2) } 输出 : { "i": 1234, "m": { "x": 1, "y": 2 }, "s": "Hello, World!" } map[string]interface {}{"i":1234, "m":map[string]interface {}{"y":2, "x":1}, "s":"Hello, World!"} 除了使⽤ map 外,还可直接使⽤ struct。因为使⽤反射获取信息,所以必须是可导出的成员。⽀持 匿名结构成员,但不⽀持匿名嵌⼊。 import ( "encoding/json" "fmt" ) type P struct{ X, Y int } type Data struct { S string I int T struct{ A, B string } // ⽀持 E []int P // 该字段被忽略 f float32 // 该字段被忽略 } func main() { data := Data{ S: "Hello, World!", I: 1234, T: struct{ A, B string }{"aaa", "bbb"}, E: []int{1, 2, 3, 4}, P: P{100, 200}, f: 1.23, } fmt.Printf("%#v\n", data) j, _ := json.Marshal(&data) fmt.Printf("%s\n", string(j)) var d Data json.Unmarshal(j, &d) fmt.Printf("%#v\n", d) } 145 输出 : main.Data{ S:"Hello, World!", I:1234, T:struct { A string; B string }{A:"aaa", B:"bbb"}, E:[]int{1, 2, 3, 4}, P:main.P{X:100, Y:200}, f:1.23 } { "S":"Hello, World!", "I":1234, "T":{"A":"aaa","B":"bbb"}, "E":[1,2,3,4] } main.Data{ S:"Hello, World!", I:1234, T:struct { A string; B string }{A:"aaa", B:"bbb"}, E:[]int{1, 2, 3, 4}, P:main.P{X:0, Y:0}, f:0.0 } 通过 struct tag 可以设置更多的信息。 type Person struct { Username string `json:"name"` // 显式指定名称 Age int `json:"age,string"` // 显式指定 JSON 数据类型 Address string `json:"address,omitempty"` // 如果值为空,则忽略该字段。 Data []byte `json:"-"` // 显式忽略的字段 } func main() { p := &Person{"Tom", 15, "", []byte{1, 2}} b, _ := json.MarshalIndent(p, "", "\t") fmt.Println(string(b)) p2 := new(Person) json.Unmarshal(b, p2) fmt.Println(p2) } 输出 : { "name": "Tom", "age": "15" } &{Tom 15 []} 146 第 16 章 net 16.1 tcp 虽然 net 包提供了具体的 TCPListener、 UDPConn 等类型,但我们通常直接⽤ Listen 和 Dial ⼯⼚ 函数,通过相关参数它们会⾃动创建合适的⺫标类型对象。基于接⼝编程也是基本的设计原则。 import ( "bufio" "fmt" "io" "net" "runtime" ) func server() { listen, _ := net.Listen("tcp", ":8080") for { conn, _ := listen.Accept() go func() { defer conn.Close() rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) for { s, err := rw.ReadString('\n') if err == io.EOF { fmt.Println("[server] client close...") return } println("[server]", s) fmt.Fprintf(rw, "server: %s\n", s) rw.Flush() } }() } } func client() { conn, _ := net.Dial("tcp", "localhost:8080") defer conn.Close() rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) fmt.Fprintln(rw, "Hello, World!") rw.Flush() 147 s, _ := rw.ReadString('\n') fmt.Println("[client]", s) } func main() { runtime.GOMAXPROCS(runtime.NumCPU()) c := make(chan bool) go server() go client() <-c } 使⽤ goroutinue ⼤⼤简化了 server 编码模式,⽽且远⽐ threading 要⾼效。 16.2 http 16.2.1 Web Server 使⽤ net/http 包开发 WebServer,需要两个基本元素: • Server: 监听,并为每个客户端接⼊创建⼀个 goroutine。 • Handler: 该接⼝有个 ServeHTTP ⽅法,提供 response、 request 参数⽤于处理请求信息和返 回结果。 package main import ( "io" "net/http" "runtime" "time" ) type myHandler struct{} func (*myHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) { http.SetCookie(response, &http.Cookie{Name: "a", Value: "xxx"}) http.SetCookie(response, &http.Cookie{Name: "b", Value: "yyy"}) response.Header().Add("Server", "go/1.0") io.WriteString(response, "Hello, World!") } func main() { runtime.GOMAXPROCS(runtime.NumCPU()) 148 server := &http.Server{ Addr: ":8080", Handler: &myHandler{}, ReadTimeout: time.Second * 5, // 建议设置,避免端⼝意外未释放,⽆法接⼊新的连接。 } server.ListenAndServe() } 输出 : $ http get http://localhost:8080 HTTP/1.1 200 OK Date: Mon, 07 May 2012 12:44:45 GMT Server: go/1.0 Set-Cookie: a=xxx Set-Cookie: b=yyy Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 Hello, World! 利⽤包中提供的函数,可以简化相关代码。 func main() { http.ListenAndServe(":8080", &myHandler{}) } 可以⽤ NotFound、 Error、 Redirect 来处理 "特殊 " 的返回结果。 func (*myHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) { http.NotFound(response, request) } 输出 : $ http get http://localhost:8080 HTTP/1.1 404 Not Found Content-Type: text/plain; charset=utf-8 Date: Mon, 07 May 2012 12:52:01 GMT Transfer-Encoding: chunked 404 page not found 包中⾃带了⼀些常⽤的 Handler,不过最重要的就是组合其他 Handler,然后通过 url Route 进⾏ 匹配调⽤的 ServerMux。 采取路径前缀最⻓匹配策略,不⽀持正则。⽐如优先匹配 /a/b/,然后才是 /a/ 前缀。 import ( "io" "net/http" ) type myHandler struct{} 149 func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "server: "+r.URL.String()) } func main() { mux := http.NewServeMux() // ⽀持 /a/、 /a/b 路径 mux.Handle("/a/", &myHandler{}) // 静态⽂件。 /static/filename 经 StripPrefix 将前缀移除后, FileServer 即可访问指定的⽂件。 mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/go/src/pkg/net/http")))) // 使⽤函数做为 Handler。 mux.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "bbb") }) // 路径跳转 mux.Handle("/c", http.RedirectHandler("/b", http.StatusFound)) http.ListenAndServe(":8080", mux) } 输出 : $ http get http://localhost:8080/a/ HTTP/1.1 200 OK Date: Mon, 07 May 2012 13:24:39 GMT Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 server: /a/ $ http get http://localhost:8080/a/b HTTP/1.1 200 OK Date: Mon, 07 May 2012 13:24:41 GMT Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 server: /a/b $ http get http://localhost:8080/b HTTP/1.1 200 OK Date: Mon, 07 May 2012 13:24:49 GMT Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 bbb $ http get http://localhost:8080/c HTTP/1.1 302 Found Date: Tue, 08 May 2012 06:52:17 GMT Location: /b 150 Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 Found. $ http get http://localhost:8080/static/server.go HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 35840 Content-Type: text/plain; charset=utf-8 Date: Mon, 07 May 2012 13:24:59 GMT Last-Modified: Thu, 26 Apr 2012 20:37:39 GMT // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // HTTP server. See RFC 2616. 利⽤包中提供的默认 DefaultServeMux 变量,我们可以进⼀步简化上述代码。 import ( "io" . "net/http" ) type myHandler struct{} func (*myHandler) ServeHTTP(w ResponseWriter, r *Request) { io.WriteString(w, "server: "+r.URL.String()) } func main() { Handle("/a/", &myHandler{}) Handle("/static/", StripPrefix("/static/", FileServer(Dir("/usr/local/go/src/pkg/net/http")))) HandleFunc("/b", func(w ResponseWriter, r *Request) { io.WriteString(w, "bbb") }) ListenAndServe(":8080", nil) } 还可以⽤ Hijacker 将 ResponseWriter 转换为 net.Conn 和 bufio.ReadWriter,以便我们更 "精确 " 控制读写过程。具体使⽤⽅法请参考官⽅⽂档。 如需要启⽤ HTTPS 安全协议,可⽤ openssl 创建所需的 X.509 证书。 $ openssl req -new -x509 -key key.pem -out cert.pem -days 1095 http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil) 151 16.3 rpc 默认的 rpc 服务基于 encoding/gob 编码协议。 服务函数签名有严格限制,第⼀个函数参数⽤于传递调⽤参数,通常使⽤复合类型指针 (⽐如 *struct),第⼆参数是返回值指针。可以⽤ RegisterName 指定⾮默认服务名称。 import ( "fmt" "log" "net" "net/rpc" ) type MyService struct { } func (*MyService) Add(args []int, reply *int) error { *reply = args[0] * args[1] return nil } func Server() { l, err := net.Listen("tcp", ":8181") if err != nil { log.Fatalln(err) } server := rpc.NewServer() server.RegisterName("my", new(MyService)) go server.Accept(l) } func Client() { client, _ := rpc.Dial("tcp", "127.0.0.1:8181") x := 0 if err := client.Call("my.Add", []int{2, 3}, &x); err != nil { log.Fatalln(err) } fmt.Println(x) } func main() { Server() Client() } rpc.Server.Accept() 为每个接⼊的连接创建默认 gobServerCodec 类型进⾏消息编码。当然,我们 可以⽤其他的编码替换,⽐如 net/rpc/jsonprc。 152 func Server() { server := rpc.NewServer() server.RegisterName("my", new(MyService)) go func() { l, _ := net.Listen("tcp", ":8181") for { conn, _ := l.Accept() go server.ServeCodec(jsonrpc.NewServerCodec(conn)) } }() } func Client() { client, _ := jsonrpc.Dial("tcp", "127.0.0.1:8181") defer client.Close() x := 0 if err := client.Call("my.Add", []int{2, 3}, &x); err != nil { log.Fatalln(err) } fmt.Println(x) } Client 另提供了⼀个 Go(),⽤于异步返回结果。 func Client() { client, _ := jsonrpc.Dial("tcp", "127.0.0.1:8181") defer client.Close() x := 0 rep := client.Go("my.Add", []int{2, 3}, &x, nil) rcall := <-rep.Done if rep.Error != nil { log.Fatalln(rep.Error) } fmt.Println(x) fmt.Println(" Call :", rcall, rcall == rep) fmt.Println(" Reply:", *(rcall.Reply.(*int))) } 输出 : 6 Call : &{my.Add [2 3] 0xf840088668 0xf8400d5000} true Reply: 6 Go() 返回⼀个 struct Call,其中的 channel 可以显式通过参数传递,或者由函数⾃动创建。只需通 过 Call.Done 即可知道函数异步调⽤是否完成。 channel 返回的和 Go() 返回的是同⼀对象。 153 显式传递 Bufer Channel 的好处是,多次调⽤服务,然后⼀次性处理结果。通常情况下,异步架构 具备更好的性能。 make(chan *Call, 10) 下⾯是使⽤ http 的例⼦。 func Server() { server := rpc.NewServer() server.RegisterName("my", new(MyService)) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { server.ServeHTTP(w, r) }) go http.ListenAndServe(":8181", mux) } func Client() { client, _ := rpc.DialHTTP("tcp", "127.0.0.1:8181") x := 0 if err := client.Call("my.Add", []int{2, 3}, &x); err != nil { log.Fatalln(err) } fmt.Println(x) } 和 http 包⼀样, rpc 也提供了操作 DefaultServer 的简便函数。但我个⼈不喜欢这么做,有污染的 嫌疑。⽼实说,习惯了 .NET、 JAVA 标准库的⼈,对 Go 标准库的设计可能有些不习惯,很多⽅法 设计有些莫名其妙。⽐如 rpc.Server 的⼏个⽅法之间,很难搞清楚依赖关系。起码那个 codec 完全 可以⽤ "属性 " 或插⼊设计。 154 第 17 章 syscall 17.1 fork 在 *nix 下编程,没有 fork ⾃然是不⾏滴。 import ( "os" "runtime" "syscall" "time" ) func fork() (int, syscall.Errno) { r1, r2, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) if err != 0 { return 0, err } if runtime.GOOS == "darwin" && r2 == 1 { r1 = 0 } return int(r1), 0 } func main() { println(os.Getpid()) pid, _ := fork() if pid == 0 { println("child:", os.Getpid(), os.Getppid()) time.Sleep(time.Second * 5) os.Exit(-1) } else { println("parent:", os.Getpid(), pid) p, _ := os.FindProcess(pid) status, _ := p.Wait() println("parent: child exit", status.Success()) } } 输出 : 1496 parent: 1496 1498 child: 1498 1496 parent: child exit false 155 os.FindProcess Wait ⾃然没有 waitpid(-1,,,) 等待所有⼦进程⽅便,可问题是 syscall ⾥⾯只有⼀ 个在 BSD/Darwin 下正常⼯作的 Wait4 函数。也不知道 Go 开发⼈员是怎么想的。 17.2 daemon 除了使⽤命令⾏⼯具 nohup、 daemon 外,⾃带 Deamon 功能貌似更好⼀些。 import ( "log" "os" "runtime" "syscall" "time" ) func fork() (int, syscall.Errno) { r1, r2, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) if err != 0 { return 0, err } if runtime.GOOS == "darwin" && r2 == 1 { r1 = 0 } return int(r1), 0 } func daemon() { pid, err := fork() if err != 0 { log.Fatalln("Daemon Error!") } if pid != 0 { os.Exit(0) } syscall.Umask(0) syscall.Setsid() os.Chdir("/") f, _ := os.Open("/dev/null") devnull := f.Fd() syscall.Dup2(int(devnull), int(os.Stdin.Fd())) syscall.Dup2(int(devnull), int(os.Stdout.Fd())) syscall.Dup2(int(devnull), int(os.Stderr.Fd())) } func main() { 156 println(os.Getpid()) daemon() for { time.Sleep(time.Second * 1) } } 157 第 18 章 time 对于 Go time format 格式定义,实在有点不习惯。 18.1 Time 可以⽤ RFC3339 格式解析常⻅时间字符串,或者直接以数字创建。 import ( "fmt" "time" ) func main() { t1, err := time.Parse(`2006-01-02 15:04:05 Z0700`, "2012-06-09 00:08:32 +0800") fmt.Println(t1, err) loc, _ := time.LoadLocation("Local") t2 := time.Date(2012, 6, 9, 0, 8, 32, 0, loc) fmt.Println(t2) } 输出 : 2012-06-09 00:08:32 +0800 CST 2012-06-09 00:08:32 +0800 CST 注: RFC3339 格式⾥的 magic 数字含义,可以参考 pkg/time/format.go ⾥头部 const 定义。 注意 "Local" ⼤⼩写,否则导致获取本地时区错误。 Time.Zone ⽅法可获取时区及时间差。另提供 了相关⽅法,可以简单在 UTC 和本地时区间转换,还可以⽤ In ⽅法转换为任意时区时间。 loc, _ := time.LoadLocation("Local") lt := time.Date(2012, 6, 9, 0, 8, 32, 0, loc) ut := lt.UTC() lt2 := ut.Local() fmt.Println(lt) fmt.Println(ut) fmt.Println(lt2) 输出 : 2012-06-09 00:08:32 +0800 CST 2012-06-08 16:08:32 +0000 UTC 2012-06-09 00:08:32 +0800 CST ⾝在北京,要早 8 ⼩时迎接 2012-06-09 这天。 除了使⽤ Year、 Month、 Day、 Hour、 Minute、 Second、 Date、 Clock 这些⽅法获取相关字段 外,我们常⽤的做法是将其转换为 Unix Timestamp。 158 loc, _ := time.LoadLocation("Local") t := time.Date(2012, 6, 9, 0, 8, 32, 0, loc) utSencond := t.Unix() utNanoSecond := t.UnixNano() t2 := time.Unix(utSencond, 0) fmt.Println(utSencond) fmt.Println(utNanoSecond) fmt.Println(t2) 输出 : 1339171712 1339171712000000000 2012-06-09 00:08:32 +0800 CST Unix 返回 UTC 时间,在 Python 中,可以⽤ datetime.fromtimestamp 或 time.localtime 转换为 本地时间。 Sub ⽤于计算时间差, Add 和 AddDate 则是调整时间 (⽀持负数 )。⾄于 Before、 After、 Equal ⾃ 然是⽐较两个时间的先后或者相等了。 loc, _ := time.LoadLocation("Local") t1 := time.Date(2012, 6, 9, 0, 8, 32, 0, loc) t2 := time.Date(2012, 6, 9, 0, 9, 32, 0, loc) d := t2.Sub(t1) fmt.Println(d) fmt.Println(t1.Add(d)) fmt.Println(t2.AddDate(-1, 0, 0)) fmt.Println(t1.Before(t2)) fmt.Println(t2.After(t1)) fmt.Println(t1.Add(d).Equal(t2)) 输出 : 1m0s 2012-06-09 00:09:32 +0800 CST 2011-06-09 00:09:32 +0800 CST true true true 18.2 Duration Duration 是⼀个 64 位整数,⽤以记录⼀段时⻓,单位是纳秒。 t := time.Now() fmt.Println(t) 159 var d time.Duration = time.Hour * 1 + time.Second * 10 t2 := t.Add(d) fmt.Println(t2) 输出 : 2012-06-09 00:54:23.989535 +0800 CST 2012-06-09 01:54:33.989535 +0800 CST 另提供了相关⽅法,⽤不同的计量单位来表⽰ Duration。 var d time.Duration = time.Hour * 1 + time.Second * 10 fmt.Println(d.Hours()) fmt.Println(d.Seconds()) 输出 : 1.0027777777777778 3610 可以⽤更⾃然⼀点的⽅法来表达 : 时 h、分 m、秒 s、毫秒 ms、微秒 us、纳秒 ns。 d, _ := time.ParseDuration("1h2m3s4ms5us6ns") fmt.Println(d) fmt.Println(d.Nanoseconds()) 输出 : 1h2m3.004005006s 3723004005006 要知道从某个时候到现在共过去多少时间,不妨试试 Since。 t1 := time.Now() time.Sleep(time.Second * 2) fmt.Println(time.Since(t1)) 输出 : 2.001021s 18.3 Timer Timer 和 Ticker 使⽤⼀个 channel 来触发计时器事件。 t := time.NewTimer(time.Second * 1) fmt.Println(<-t.C) Timer 事件仅被触发⼀次,在触发前可以调⽤ Stop ⽅法取消事件。或者⽤ AfterFunc 替代 NewTimer 创建计时器,在事件触发时执⾏特定函数。 Ticker 则⼀直运作,直到 Stop 被调⽤。 其实最常⽤是 After 和 Tick 这两个⼯⼚函数,它们创建计时器,并直接返回 Channel。 a := time.After(time.Second * 10) 160 t := time.Tick(time.Second * 1) for { select { case <-a: fmt.Println("timeout") os.Exit(1) case v, _ := <-t: fmt.Println(v) } } 输出 : 2012-06-09 12:40:39.306083 +0800 CST 2012-06-09 12:40:40.306652 +0800 CST 2012-06-09 12:40:41.306612 +0800 CST 2012-06-09 12:40:42.30664 +0800 CST 2012-06-09 12:40:43.306576 +0800 CST 2012-06-09 12:40:44.306556 +0800 CST 2012-06-09 12:40:45.307022 +0800 CST 2012-06-09 12:40:46.30666 +0800 CST 2012-06-09 12:40:47.306417 +0800 CST timeout Tick 才是我们习惯的计时器类型。注意每次调⽤ After 和 Tick 都会创建新的对象和 Channel,如 果时间参数为 0,则直接返回 nil。 161 第 19 章 sync 虽说并发和同步是⼀对搭档,但强烈建议⽤ Channel,尽可能避免使⽤ Lock。 19.1 Locker 下⾯的例⼦中,不同的 goroutinue 交错运⾏。 import ( "fmt" "time" "strconv" ) func main() { f := func(name string) { for i := 0; i < 3; i++ { fmt.Printf("[%s] %d\n", name, i) } } for i := 0; i < 2; i++ { go f("go" + strconv.Itoa(i)) } time.Sleep(time.Second * 3) } 输出 : [go0] 0 [go1] 0 [go0] 1 [go1] 1 [go0] 2 [go1] 2 借助于 sync.Mutex 这个 Locker,我们可以简单地实现排队。 import ( "fmt" "time" "strconv" "sync" ) func main() { m := new(sync.Mutex) f := func(name string) { m.Lock() 162 defer m.Unlock() for i := 0; i < 3; i++ { fmt.Printf("[%s] %d\n", name, i) } } for i := 0; i < 2; i++ { go f("go" + strconv.Itoa(i)) } time.Sleep(time.Second * 3) } 输出 : [go0] 0 [go0] 1 [go0] 2 [go1] 0 [go1] 1 [go1] 2 注意:不⽀持递归锁。因为多个 goroutinue 可能运⾏在同⼀线程,因此基于线程实现的递归锁是 ⽆法使⽤的。 另⼀个 Locker 实现是 RWMutex,通常称为读写锁。当写操作获得锁时,所有读操作锁被阻塞。但 所有读操作锁之间不会阻塞互斥,完全是并发⾏为,也就是说读锁不过是检查写锁状态⽽已。同 样,想获得写锁的前提是所有读锁被释放。 import ( "fmt" "time" "sync" ) func main() { m := new(sync.RWMutex) r := func(x int) { fmt.Printf("[%d] RLock...\n", x) // 等待写锁释放。 m.RLock() defer m.RUnlock() for n := 0; n < 2; n++ { fmt.Printf("[%d] %d\n", x, n) time.Sleep(time.Second * 1) } } // 写锁。 m.Lock() 163 // 启动多个读 goroutinue。 for i := 0; i < 3; i++ { go r(i) } time.Sleep(time.Second * 1) // 释放写锁,使得多个读锁可以进⾏。 fmt.Println("Unlock...") m.Unlock() // 再次请求写锁,必须等到所有读锁释放。 m.Lock() fmt.Println("Lock...") } 输出: [0] RLock... [1] RLock... [2] RLock... Unlock... [0] 0 [1] 0 [2] 0 [0] 1 [1] 1 [2] 1 Lock... 19.2 Cond Cond 在 Locker 的基础上增加了⼀种 "通知 " 机制。其内部通过⼀个计数器和信号量来实现通知和 ⼲播的效果。 • 当调⽤ Wait 时,内部计数器增加,然后阻塞,请求系统信号量。 • 当调⽤ Signal 时,计数器 -1,释放信号量,使某个 Wait goroutinue 解除阻塞。 • 当调⽤ Broadcast 时,释放和计数器等量的信号量,使得所有 Wait goroutinue 解除阻塞。 那为什么还需要外部传⼊⼀个 Locker 呢?同时⽤ cond.L 对 Wait 和 Signal/Broadcast 进⾏保护, 可以确保: • 在发送信号的同时,不会有新的 goroutinue 进⼊ Wait。 • 在 Wait 逻辑未完成前,不会有新的事件发⽣。 需要注意的是,在调⽤ Signal、 Broadcast 前,应该确保⺫标进⼊ Wait 阻塞状态。 164 对 Wait 使⽤ L.Lock 是必须的 (该函数内部调⽤ L.Lock,不保护会导致出错 )。 Wait 内部在请求信号 量进⼊阻塞前,会释放保护锁,使得其他代码可以获得保护锁 (⽐如⽤来做事件通知操作 )。在获得 信号量后,会⾃动调⽤ L.Lock 再次获取保护锁。 import ( "fmt" "time" "sync" ) func main() { locker := new(sync.Mutex) cod := sync.NewCond(locker) for i := 0; i < 2; i++ { go func(x int) { cod.L.Lock() cod.Wait() cod.L.Unlock() for n := 0; n < 3; n++ { fmt.Printf("[%d] %d...\n", x, n) } }(i) } // 等待全部进⼊ Wait 状态 time.Sleep(time.Second * 1) cod.L.Lock() cod.Broadcast() cod.L.Unlock() time.Sleep(time.Second * 5) } 输出 : [0] 0... [1] 0... [1] 1... [0] 1... [1] 2... [0] 2... 我们可以调整 Wait 的 Lock 保护范围,使得接收到⼲播的 goroutinue 顺序执⾏。 func main() { locker := new(sync.Mutex) cod := sync.NewCond(locker) for i := 0; i < 2; i++ { go func(x int) { cod.L.Lock() 165 cod.Wait() defer cod.L.Unlock() // !!!! for n := 0; n < 3; n++ { fmt.Printf("[%d] %d...\n", x, n) } }(i) } time.Sleep(time.Second * 1) cod.L.Lock() cod.Broadcast() cod.L.Unlock() time.Sleep(time.Second * 5) } 输出: [0] 0... [0] 1... [0] 2... [1] 0... [1] 1... [1] 2... 上⾯的例⼦中,尽管对 Broadcast 的保护不是必须的,但依然建议这么做。⽼实说,这个 Cond ⽤ 起来很别扭,远没有 Python 中的库好⽤、⾃然。完全可以把这个外部 Lock 交给开发⼈员⾃⾏处 理,反正 Cond 内部对计数器已经做了同步保护。 19.3 Once Once.Do 可以确保⺫标仅会被执⾏⼀次。⽤来做并发环境初始化挺好的。 import ( "fmt" "time" "sync" ) func main() { once := new(sync.Once) for i := 0; i < 3; i++ { go func(x int) { once.Do(func() { fmt.Printf("once %d\n", x) }) fmt.Printf("%d ...\n", x) }(i) } 166 time.Sleep(time.Second * 3) } 输出 : once 0 0 ... 1 ... 2 ... 19.4 WaitGroup Go 进程并不会等待所有 goroutinue 退出,因此要实现⼀个完整的并发逻辑,必须让主进程等待。 通常的做法是⽤⼀个 channel 等待结束通知,但多数时候,我们都⽆法确定哪个 goroutinue 是最 后完成的。只好⽤丑陋的 time.Sleep。 现在有了 WaitGroup,这个问题就⽐较好处理了。 import ( "fmt" "sync" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) wg := sync.WaitGroup{} for i := 0; i < 3; i++ { wg.Add(1) go func(x, n int) { for y := 0; y < n; y++ { fmt.Printf("[%d]: %d\n", x, y) } wg.Done() }(i, rand.Intn(5)) } wg.Wait() } 输出 : [0]: 0 [1]: 0 [2]: 0 [0]: 1 [2]: 1 [2]: 2 167 [2]: 3 19.5 atomic sync/atomic 包可以确保对⺫标数据进⾏原⼦操作,但包中也提到这样⼀句话。 These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory. 168 第 20 章 os 系统编程⾃然少不了和操作系统打交道。 20.1 System 相关函数使⽤⽐较简单,仅挑⼏个 "异类 " 说明⼀下。 • MkdirAll: 创建多级⼦⺫录。 • RemoveAll: 删除多级⼦⺫录。 检查⽂件或⺫录是否存在,与我们以往先判断后操作相反。直接进⾏操作,然后根据错误对象,来 判断原因。 • IsExist: 根据返回错误对象,判断是否因为 "已存在 " ⽽导致错误发⽣。 • IsNotExist: 根据返回错误对象,判断是否因为⺫标 "不存在 " ⽽导致错误发⽣。 • IsPermission: 根据返回对象,判断是否因为权限不够⽽导致错误发⽣。 func main() { if err := os.Mkdir("a", 0755); os.IsExist(err) { fmt.Println("Directory a exists! Mkdir Error!") } if file, err := os.Open("./a/test.txt"); os.IsNotExist(err) { fmt.Println("File not exists! Can't Open ...") } else if os.IsPermission(err) { fmt.Println("Permission Denied!") } else { file.Close() } } Stat/Lstat ⽤于获取⽂件信息,但 Lstat 只读取符号链接⽂件⾃⾝。 import ( "os" "time" ) func main() { name := "./a.txt" os.Chmod(name, 0600) info, _ := os.Stat(name) os.Chtimes(name, time.Now(), info.ModTime()) } 在操作链接⽂件时,需要注意符号链接。 169 func main() { os.Link("./a.txt", "./b.txt") sa, _ := os.Stat("./a.txt") sb, _ := os.Stat("./b.txt") fmt.Println(os.SameFile(sa, sb)) os.Symlink("./a.txt", "./c.txt") fmt.Println(os.Readlink("./c.txt")) } os.Exit 会⽴即终⽌进程,不会调⽤ defer 函数。 package main import ( "os" ) func main() { defer println("exit...") // 这个不会被调⽤ ... os.Exit(-1) } 20.2 Environ Environ 返回全部的环境变量 (key=value), Getenv 获得单个变量值。 package main import ( "fmt" "os" ) func main() { envs := os.Environ() for _, s := range envs { fmt.Println(s) } fmt.Printf("$HOME = %s\n", os.Getenv("HOME")) } 输出 : HOME=/Users/yuhen GOROOT=/usr/local/go GOPATH=/usr/local/go.lib:/Users/yuhen/Documents/Projects/go:/Users/yuhen/ ... ... $HOME = /Users/yuhen 170 还有两个类似模板的简便函数。 fmt.Println(os.Expand("$HOME; $GOROOT", func(s string) string { return s + " = " + os.Getenv(s) })) fmt.Println(os.ExpandEnv("HOME = $HOME; GOROOT = $GOROOT")) 输出 : HOME = /Users/yuhen; GOROOT = /usr/local/go HOME = /Users/yuhen; GOROOT = /usr/local/go ExpandEnv ⽤来展开环境变量更⽅便,但 Expand 完全可以作为⼀个轻便的字符串模板引擎。 20.3 Process 使⽤ Process 可以创建进程以执⾏其他程序,不过我们常⽤的是其封装版本 os/exec.Cmd。 package main import ( "fmt" "log" "os/exec" "io/ioutil" ) func logErr(err interface{}) { if err != nil { log.Fatalln(err) } } func main() { cmd := exec.Command("ls", "-lh", "/usr/local/go/bin") stdout, _ := cmd.StdoutPipe() logErr(cmd.Start()) bs, _ := ioutil.ReadAll(stdout) fmt.Println(string(bs)) logErr(cmd.Wait()) } 使⽤⽅法很简单, Command 创建 Cmd 对象,然后⽤ Start ⽅法启动,⽤ Wait 等待进程结束。在 启动前,我们可以挂接 stdin、 stdout、 stderr 等输⼊输出管道。 注意:在 *nix 体系下,主进程需要获取⼦进程的退出状态,否则会导致僵⼫进程,除⾮主进程先咯 屁,由 init 完成状态检查⼯作。 171 func main() { cmd := exec.Command("ls", "-lh", "/usr/local/go/bin") logErr(cmd.Start()) fmt.Println(cmd.Process.Pid) time.Sleep(time.Minute) } 输出 : 2095 执⾏上⾯的代码后,另启动⼀个终端,你会找到⼀个僵⼫进程。 $ ps aux | grep 2095 | grep -v grep yuhen 2095 0.0 0.0 0 0 s000 Z+ 8:35下午 0:00.00 (ls) 加上 Wait,这个⼦进程会及时释放掉遗留的状态信息,彻底消散。 另有 Run ⽅法,包装了 Start + Wait 这个过程。⽽ Output ⽅法则会调⽤ Run,然后返回 stdout 数据。⽤来改造前⾯的例⼦,会使代码更加简洁。 CombinedOutput 和 Output 的区别是同时返回 stdout、 stderr 信息。 func main() { bs, err := exec.Command("ls", "-lh", "/usr/local/go/bin").Output() logErr(err) fmt.Println(string(bs)) } 透过 Cmd.Process 我们可以给⼦进程发送信号,获取 PID。⽽由 Wait 提取的 ProcessStat 则包括 了退出信息等。 package main import ( "fmt" "log" "os/exec" ) func logErr(err interface{}) { if err != nil { log.Fatalln(err) } } func main() { cmd := exec.Command("ls", "-lh", "/usr/local/go") logErr(cmd.Run()) fmt.Println(cmd.Path) fmt.Println(cmd.ProcessState.Exited()) 172 fmt.Println(cmd.ProcessState.Success()) fmt.Println(cmd.ProcessState.SystemTime()) } 输出 : /bin/ls true true 2.471ms Process 有个特殊的⽤途,就是直接通过 FindProcess 挂到某个已经运⾏的程序上。如下⾯的例 ⼦,我们先⽤ exec.Cmd 执⾏ ps 命令获取 top 进程 pid,然后⽤ Process 发送终⽌信号给它。 package main import ( "log" "os" "os/exec" "strings" "strconv" ) func main() { cmd := exec.Command("sh", "-c", "ps aux | grep top | grep -v grep") bs, err := cmd.Output() if err != nil { log.Fatalln(err) } ss := strings.Fields(string(bs)) pid, _ := strconv.Atoi(ss[1]) proc, _ := os.FindProcess(pid) proc.Kill() proc.Wait() } 20.4 Signal 信号处理⽅法⾮常简单:创建 channel,接收 os.signal 数据,然后作出相应的动作即可。 package main import ( "fmt" "os" "os/signal" ) func main() { 173 fmt.Println(os.Getpid()) sig := make(chan os.Signal) signal.Notify(sig) for s := range sig { fmt.Println(s) } } 输出 : 2615 我们另起⼀个终端,看看信号测试效果。 $ kill -s INT 2615 $ kill -s TERM 2615 $ kill -s USR1 2615 $ kill -s USR2 2615 $ kill -s KILL 2615 输出 : 2615 ^Cinterrupt interrupt terminated user defined signal 1 user defined signal 2 Killed: 9 如果仅捕获特定的信号,可以将其作为参数传递给 Notify ⽅法。⽐如下⾯的例⼦中,通过忽略 Interrupt 信号,避免 Ctrl + C 终端。在接收到退出信号时,可以使⽤⾃定义退出逻辑。 var ( SigTerm = syscall.Signal(15) ) func main() { fmt.Println(os.Getpid()) sig := make(chan os.Signal) signal.Notify(sig, os.Interrupt, SigTerm) for { switch <-sig { case os.Interrupt: println("Interrupt") case SigTerm: println("Terminated") os.Exit(0) } } } 174 可以⽤ signal 代替 channel 阻塞 main 函数退出。 func waitExit() { sigTerm := syscall.Signal(15) sig := make(chan os.Signal) signal.Notify(sig, os.Interrupt, sigTerm) for { switch <-sig { case os.Interrupt, sigTerm: return // 或 os.Exit(0) } } } func main() { ... waitExit() } 20.5 User 在 *nix 下编程,免不了要受帐号权限设置限制。 Current 返回当前帐号, Lookup 可按⽤户名或 uid 进⾏查找。 import ( "fmt" "os/user" ) func main() { u, _ := user.Current() fmt.Printf("%#v\n", u) } 输出 : &user.User{Uid:"501", Gid:"20", Username:"yuhen", Name:"Q.yuhen", HomeDir:"/Users/yuhen"} 175 第三部分 扩展库 现在知名且有⼈⽓的扩展库作品还不多,还需时间慢慢积累和沉淀。如果缺乏优秀的扩展库⽀持, 仅凭语⾔⾃⾝的优秀是⾛不远的。 Go 还缺⼀个真正意义上的资源聚合平台,如同 Python/PyPI、 Ruby/RubyGems、 NodeJS/NPM 那样。 http://gonuts.io 才刚刚开始,但愿不会向 GoPkgDoc 那样搞个半吊⼦。 176 mgo - MongoDB Driver MongoDB 可以适应⼤部分 RDBMS 数据库场景,且拥有⾼效、轻便等诸多特点。 基本操作 : package main import ( "fmt" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" "runtime/debug" "log" ) func logError(err interface{}) { if err != nil { debug.PrintStack() log.Fatalln(err) } } func insert(coll *mgo.Collection) { users := []interface{}{ bson.M{"name": "user1", "age": 18}, bson.M{"name": "user2", "age": 19}, } err := coll.Insert(users...) // 注意变参展开,否则就成⼀个⽂档了。 logError(err) } func update(coll *mgo.Collection) { selector := bson.M{"age": 18} change := bson.M{"$set": bson.M{"age": 100}} err := coll.Update(selector, change) logError(err) } func remove(coll *mgo.Collection) { selector := bson.M{"age": 19} err := coll.Remove(selector) logError(err) } func query(coll *mgo.Collection) { filter := bson.M{"age": bson.M{"$gte": 18}} selector := bson.M{"name": 1, "age": 1} 177 iter := coll.Find(filter).Select(selector).Iter() for { u := bson.M{} // 为嘛⽤指针,因为 interface{} 是 value-copy。 if !iter.Next(&u) { break } fmt.Println(u) } } func main() { session, err := mgo.Dial("localhost") logError(err) defer session.Close() coll := session.DB("test").C("users") logError(coll.DropCollection()) insert(coll) update(coll) remove(coll) query(coll) } Session Dial 连接数据库,并创建⽤于后续操作的 Session 对象。如果找不到⺫标数据服务器,会在⼀段时 间后引发 Timeout 错误。不过我们可以⽤ DialWithTimeout ⾃定义超时时间。 Clone ⽤于复制⼀个 Session,但须注意,复制品和原件共享底层的 Socket 连接,也就是说对服务 器⽽⾔它们只是同⼀个 Connection。 Close 复制品对原件没有影响。 session, err := mgo.DialWithTimeout("localhost", time.Second * 5) logError(err) defer session.Close() s2 := session.Clone() fmt.Println(s2.BuildInfo()) s2.Close() fmt.Println(session.DatabaseNames()) 如果允许数据丢失,性能优先,那么可以考虑 Setsafe(nil),也就是不做任何 getLastError 操作。 通过 SetMode 我们可以调整⼀致性模型,这会影响 RepliSet 结构下的数据的读写效率。 178 • Strong: 默认模式。使⽤唯⼀连接在 master 服务器上进⾏读写,可确保数据完整、有续且总 是最新的。适⽤于数据安全性优先,⽽⾮性能。 • Monotonic: 不能保证数据是最新的,但确保连续查询操作使⽤相同的连接会话。对于查询操 作,该模式会尝试连接某个可⽤的 slave 服务器。⼀旦需要修改数据 (insert/update) 时,将 会切换到 master,从⽽确保查询到刚刚更新的数据。 • Eventual: 连接任何可⽤的 slave 服务器,不确保连续查询操作使⽤同⼀连接会话。也就是说 ⽆法保证数据连续性,因为不同的 slave 服务器同步可能不⼀致。更新时同样切换到 master, 但使⽤独⽴连接,且不保证写顺序。因此更新的数据可能不会影响原读操作会话。 Eventual 速度最快,且耗费资源最少,但对于读写操作不能提供有效保证。另外可以⽤ SetMode 的 refresh 参数让写操作完成后的 session 恢复到最初的状态,⽐如重新连接 slave。 Database 使⽤很简单, CollectionNames 返回当前数据库中所有已存数据集合, C 返回或创建某个特定名称 的集合。 和 Session ⼀样, DB 提供了 Run ⽅法,⽐如下⾯例⼦中我们创建 Capped Collection。 func main() { session, err := mgo.DialWithTimeout("localhost", time.Second * 5) logError(err) defer session.Close() db := session.DB("test") logError(db.C("mycap").DropCollection()) result := bson.M{} err = db.Run(bson.D{{"create", "mycap"}, {"capped", true}, {"size", 100}}, &result) logError(err) fmt.Println(result) } bson 包提供了 M 和 D 两种数据结构。考虑到 MongoDB 将第⼀个 Doc Key 当作命令执⾏,故 M 这种 map ⽆序类型时不适合的,⽽该⽤ D slice。 type M map[string]interface{} type DocElem struct { Name string Value interface{} } type D []DocElem 179 Collection Query Capped Collection Tailable 是 MongoDB ⼀个很重要的功能。 需要注意的⼏点 : • 如果 capped collection 为空,那么 iter.Next() == false、 Err() == nil,只能重新创建 iter, 直到 capped collection 中有⽂档保存。 • 只有 iter.Next 曾成功读取过⽂档,才会在没有新⽂档出现时引发 Timeout,此时⽆需重新构 建 iter 对象。 • 当 capped collection cross overrwrite 时, Next 返回 false,且 Err() 引发错误,此时需要重 新 Find(>lastId)。 package main import ( "fmt" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" "time" ) func main() { session, _ := mgo.Dial("localhost") defer session.Close() collection := session.DB("test").C("data") for { iter := collection.Find(nil).Sort("$natural").Tail(5 * time.Second) for { result := bson.M{} // 如果没有新的⽂档,返回 false。 for iter.Next(&result) { fmt.Println(result) } // 检查错误。 if err := iter.Err(); err != nil { panic(err) } // iter.Next 成功读取过⽂档,当没有新⽂档时引发 Timeout。 if iter.Timeout() { println("timeout") continue 180 } // capped collection 为空, iter.Next ⾃然从未成功读取过⽂档。 // 不会引发 Timeout,且 Err = nil。 // 继续循环,重新构建 iter,期望有⽂档进⼊。 println("no document") time.Sleep(5 * time.Second) break } } } DBRef GridFS 181 snappy - Compress/Decompress Library zlib 压缩性能实在差了些,对于海量数据处理系统,我们需要在压缩⽐和性能间找到平衡。 package main import ( "bytes" "code.google.com/p/snappy-go/snappy" "fmt" "strings" ) func main() { src := []byte(strings.Repeat("Hello, World!", 10)) dst := make([]byte, snappy.MaxEncodedLen(len(src))) dst, _ = snappy.Encode(dst, src) n, _ := snappy.DecodedLen(dst) src2 := make([]byte, n) src2, _ = snappy.Decode(src2, dst) fmt.Printf("%d, %v\n", len(src), bytes.Equal(src, src2)) fmt.Printf("%d, %v\n", len(dst), dst) } 输出 : 130, true 22, [130 1 48 72 101 108 108 111 44 32 87 111 114 108 100 33 254 13 0 210 13 0] 注意 dst slice,创建指定 len ⽽不仅是 cap 相符的字节切⽚。 snappy/encode.go func Encode(dst, src []byte) ([]byte, error) { if n := MaxEncodedLen(len(src)); len(dst) < n { dst = make([]byte, n) } ... } 182 附录 183
还剩182页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 2 人已下载

下载pdf

pdf贡献者

淡淡风轻

贡献于2013-04-07

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf