Go学习笔记第三版

网友雨痕的Go语言学习笔记,记录比较仔细。
展开查看详情

1.Go 学习笔记

2. Go 学习笔记, 第 3 版 前⾔言 从 2012 年初开始学习和研究 Go 语⾔言,虽不敢说精通,但总体上还是 ⽐比较熟悉和了解的。这⼏几年踏踏实实读着它的核⼼心源码⼀一路⾛走过来,有 收获,也有困扰,但还算⽤用得顺⼿手。 就我个⼈人看来,要想了解 Go 的本质,是需要⼀一些 C 的底⼦子。换句话说,抛 开 goroutine 等等时髦概念,其本质上还是 Next C 的路⼦子。 这本笔记原则上⼒力求简洁,属于⼯工具书⽽而⾮非教程。⺫⽬目的是在需要的时候,花上很短的时间 就可从头到尾复习完所有知识点。对熟练的程序员⽽而⾔言,⼯工具书似乎⽐比教程更重要些。 ⾄至于第⼆二部分源码剖析,则属于可选部分,算是写给那些和我⼀一样有好奇⼼心的童鞋看的。 本书在不侵犯作者个⼈人权利的情况下,可⾃自由散播。 • 下载:不定期更新,https://github.com/qyuhen/book。 • 联系:qyuhen@hotmail.com 雨痕 ⼆二〇⼀一四年夏初 2

3. Go 学习笔记, 第 3 版 更新 2012-01-11 开始学习 Go。 2012-01-15 第⼀一版,基于 R60。 2012-03-29 升级到 1.0。 2012-06-15 升级到 1.0.2。 2013-03-26 升级到 1.1。 2013-12-12 第⼆二版,基于 1.2。 2014-05-22 第三版,基于 1.3。 2014-06-05 增加 reflect。 2014-06-28 补充 slice。 3

4. Go 学习笔记, 第 3 版 ⺫⽬目录 第⼀一部分 语⾔言 8 第 1 章 类型 9 1.1 变量 9 1.2 常量 10 1.3 基本类型 13 1.4 引⽤用类型 14 1.5 类型转换 14 1.6 字符串 15 1.7 指针 17 1.8 ⾃自定义类型 19 第 2 章 表达式 21 2.1 保留字 21 2.2 运算符 21 2.3 初始化 22 2.4 控制流 23 第 3 章 函数 29 3.1 函数定义 29 3.2 变参 30 3.3 返回值 30 3.4 匿名函数 32 3.5 延迟调⽤用 34 3.6 错误处理 35 第 4 章 数据 39 4.1 Array 39 4.2 Slice 40 4.3 Map 45 4

5. Go 学习笔记, 第 3 版 4.4 Struct 47 第 5 章 ⽅方法 53 5.1 ⽅方法定义 53 5.2 匿名字段 54 5.3 ⽅方法集 55 5.4 表达式 56 第 6 章 接⼝口 59 6.1 接⼝口定义 59 6.2 执⾏行机制 61 6.3 接⼝口转换 62 6.4 接⼝口技巧 64 第 7 章 并发 65 7.1 Goroutine 65 7.2 Channel 67 第 8 章 包 75 8.1 ⼯工作空间 75 8.2 源⽂文件 75 8.3 包结构 76 8.4 ⽂文档 78 第 9 章 进阶 80 9.1 内存布局 80 9.2 指针陷阱 81 9.3 cgo 84 9.4 Reflect 92 第⼆二部分 源码 107 1. Memory Allocator 108 1.1 初始化 111 1.2 分配流程 114 5

6. Go 学习笔记, 第 3 版 1.3 释放流程 123 1.4 其他 127 2. Garbage Collector 131 2.1 垃圾回收 131 2.2 内存释放 137 2.3 状态输出 140 3. Goroutine Scheduler 146 3.1 初始化 146 3.2 创建任务 152 3.3 执⾏行任务 155 3.4 连续栈 165 3.5 系统调⽤用 173 3.6 系统监控 178 3.7 状态输出 180 4. Channel 181 4.1 Send 182 4.2 Receive 185 4.3 Select 188 5. Defer 197 第三部分 附录 204 A. ⼯工具 205 1. ⼯工具集 205 2. 条件编译 206 3. 跨平台编译 208 B. 调试 209 1. GDB 209 2. Data Race 209 C. 测试 212 6

7. Go 学习笔记, 第 3 版 1. Test 212 2. Benchmark 213 3. Example 214 4. Cover 215 5. PProf 216 7

8. Go 学习笔记, 第 3 版 第⼀一部分 语⾔言 8

9. Go 学习笔记, 第 3 版 第 1 章 类型 1.1 变量 Go 是静态类型语⾔言,不能在运⾏行期改变变量类型。 使⽤用关键字 var 定义变量,⾃自动初始化为零值。如果提供初始化值,可省略变量类型,由 编译器⾃自动推断。 var x int var f float32 = 1.6 var s = "abc" 在函数内部,可⽤用更简略的 ":=" ⽅方式定义变量。 func main() { x := 123 // 注意检查,是定义新局部变量,还是修改全局变量。该⽅方式容易造成错误。 } 可⼀一次定义多个变量。 var x, y, z int var s, n = "abc", 123 var ( a int b float32 ) func main() { n, s := 0x1234, "Hello, World!" println(x, s, n) } 多变量赋值时,先计算所有相关值,然后再从左到右依次赋值。 data, i := [3]int{0, 1, 2}, 0 i, data[i] = 2, 100 // (i = 0) -> (i = 2), (data[0] = 100) 特殊只写变量 "_",⽤用于忽略值占位。 9

10. Go 学习笔记, 第 3 版 func test() (int, string) { return 1, "abc" } func main() { _, s := test() println(s) } 编译器会将未使⽤用的局部变量当做错误。 var s string // 全局变量没问题。 func main() { i := 0 // Error: i declared and not used。(可使⽤用 "_ = i" 规避) } 注意重新赋值与定义新同名变量的区别。 s := "abc" println(&s) s, y := "hello", 20 // 重新赋值: 与前 s 在同⼀一层次的代码块中,且有新的变量被定义。 println(&s, y) // 通常函数多返回值 err 会被重复使⽤用。 { s, z := 1000, 30 // 定义新同名变量: 不在同⼀一层次代码块。 println(&s, z) } 输出: 0x2210230f30 0x2210230f30 20 0x2210230f18 30 1.2 常量 常量值必须是编译期可确定的数字、字符串、布尔值。 const x, y int = 1, 2 // 多常量初始化 const s = "Hello, World!" // 类型推断 const ( // 常量组 a, b = 10, 100 c bool = false 10

11. Go 学习笔记, 第 3 版 ) func main() { const x = "xxx" // 未使⽤用局部常量不会引发编译错误。 } 不⽀支持 1UL、2LL 这样的类型后缀。 在常量组中,如不提供类型和初始化值,那么视作与上⼀一常量相同。 const ( s = "abc" x // x = "abc" ) 常量值还可以是 len、cap、unsafe.Sizeof 等编译期可确定结果的函数返回值。 const ( a = "abc" b = len(a) c = unsafe.Sizeof(b) ) 如果常量类型⾜足以存储初始化值,那么不会引发溢出错误。 const ( a byte = 100 // int to byte b int = 1e20 // float64 to int, overflows ) 枚举 关键字 iota 定义常量组中从 0 开始按⾏行计数的⾃自增枚举值。 const ( Sunday = iota // 0 Monday // 1,通常省略后续⾏行表达式。 Tuesday // 2 Wednesday // 3 Thursday // 4 Friday // 5 Saturday // 6 ) 11

12. Go 学习笔记, 第 3 版 const ( _ = iota // iota = 0 KB int64 = 1 << (10 * iota) // iota = 1 MB // 与 KB 表达式相同,但 iota = 2 GB TB ) 在同⼀一常量组中,可以提供多个 iota,它们各⾃自增⻓长。 const ( A, B = iota, iota << 10 // 0, 0 << 10 C, D // 1, 1 << 10 ) 如果 iota ⾃自增被打断,须显式恢复。 const ( A = iota // 0 B // 1 C = "c" // c D // c,与上⼀一⾏行相同。 E = iota // 4,显式恢复。注意计数包含了 C、D 两⾏行。 F // 5 ) 可通过⾃自定义类型来实现枚举类型限制。 type Color int const ( Black Color = iota Red Blue ) func test(c Color) {} func main() { c := Black test(c) x := 1 test(x) // Error: cannot use x (type int) as type Color in function argument 12

13. Go 学习笔记, 第 3 版 test(1) // 常量会被编译器⾃自动转换。 } 1.3 基本类型 更明确的数字类型命名,⽀支持 Unicode,⽀支持常⽤用数据结构。 类型 ⻓长度 默认值 说明 bool 1 false byte 1 0 uint8 rune 4 0 Unicode Code Point, int32 int, uint 4或8 0 32 或 64 位 int8, uint8 1 0 -128 ~ 127, 0 ~ 255 int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535 int32, uint32 4 0 -21亿 ~ 21 亿, 0 ~ 42 亿 int64, uint64 8 0 float32 4 0.0 float64 8 0.0 complex64 8 complex128 16 uintptr 4或8 ⾜足以存储指针的 uint32 或 uint64 整数 array 值类型 struct 值类型 string "" UTF-8 字符串 slice nil 引⽤用类型 map nil 引⽤用类型 channel nil 引⽤用类型 interface nil 接⼝口 function nil 函数 13

14. Go 学习笔记, 第 3 版 ⽀支持⼋八进制、⼗十六进制,以及科学记数法。标准库 math 定义了各数字类型取值范围。 a, b, c, d := 071, 0x1F, 1e9, math.MinInt16 空指针值 nil,⽽而⾮非 C/C++ NULL。 1.4 引⽤用类型 引⽤用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需 要初始化相关属性。 内置函数 new 计算类型⼤大⼩小,为其分配零值内存,返回指针。⽽而 make 会被编译器翻译 成具体的创建函数,由其分配内存和初始化成员结构,返回对象⽽而⾮非指针。 a := []int{0, 0, 0} // 提供初始化表达式。 a[1] = 10 b := make([]int, 3) // slice.goc: makeslice b[1] = 10 c := new([]int) c[1] = 10 // Error: invalid operation: c[1] (index of type *[]int) 有关引⽤用类型具体的内存布局,可参考后续章节。 1.5 类型转换 不⽀支持隐式类型转换,即便是从窄向宽转换也不⾏行。 var b byte = 100 // var n int = b // Error: cannot use b (type byte) as type int in assignment var n int = int(b) // 显式转换 使⽤用括号避免优先级错误。 *Point(p) // 相当于 *(Point(p)) (*Point)(p) <-chan int(c) // 相当于 <-(chan int(c)) (<-chan int)(c) 14

15. Go 学习笔记, 第 3 版 同样不能将其他类型当 bool 值使⽤用。 a := 100 if a { // Error: non-bool a (type int) used as if condition println("true") } 1.6 字符串 字符串是不可变值类型,内部⽤用指针指向 UTF-8 字节数组。 • 默认值是空字符串 ""。 • ⽤用索引号访问某字节,如 s[i]。 • 不能⽤用序号获取字节元素指针。&s[i] ⾮非法。 • 不可变类型,⽆无法修改字节数组。 • 字节数组尾部不包含 NULL。 runtime.h struct String { byte* str; intgo len; }; 使⽤用索引号访问字符 (byte)。 s := "abc" println(s[0] == '\x61', s[1] == 'b', s[2] == 0x63) 输出: true true true 使⽤用 "`" 定义不做转义处理的原始字符串,⽀支持跨⾏行。 s := `a b\r\n\x00 c` println(s) 输出: 15

16. Go 学习笔记, 第 3 版 a b\r\n\x00 c 连接跨⾏行字符串时,"+" 必须在上⼀一⾏行末尾,否则导致编译错误。 s := "Hello, " + "World!" s2 := "Hello, " + "World!" // Error: invalid operation: + untyped string ⽀支持⽤用两个索引号返回⼦子串。⼦子串依然指向原字节数组,仅修改了指针和⻓长度属性。 s := "Hello, World!" s1 := s[:5] // Hello s2 := s[7:] // World! s3 := s[1:5] // ello 单引号字符常量表⽰示 Unicode Code Point,⽀支持 \uFFFF、\U7FFFFFFF、\xFF 格式。 对应 rune 类型,UCS-4。 func main() { fmt.Printf("%T\n", 'a') var c1, c2 rune = '\u6211', '们' println(c1 == '我', string(c2) == "\xe4\xbb\xac") } 输出: int32 // rune 是 int32 的别名 true true 要修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string。⽆无论哪种转 换,都会重新分配内存,并复制字节数组。 func main() { s := "abcd" bs := []byte(s) bs[1] = 'B' println(string(bs)) u := "电脑" 16

17. Go 学习笔记, 第 3 版 us := []rune(u) us[1] = '话' println(string(us)) } 输出: aBcd 电话 ⽤用 for 循环遍历字符串时,也有 byte 和 rune 两种⽅方式。 func main() { s := "abc汉字" for i := 0; i < len(s); i++ { // byte fmt.Printf("%c,", s[i]) } fmt.Println() for _, r := range s { // rune fmt.Printf("%c,", r) } } 输出: a,b,c,æ,±,,å,­,, a,b,c,汉,字, 1.7 指针 ⽀支持指针类型 *T,指针的指针 **T,以及包含包名前缀的 *<package>.T。 • 默认值 nil,没有 NULL 常量。 • 操作符 "&" 取变量地址,"*" 透过指针访问⺫⽬目标对象。 • 不⽀支持指针运算,不⽀支持 "->" 运算符,直接⽤用 "." 访问⺫⽬目标成员。 func main() { type data struct{ a int } var d = data{1234} var p *data p = &d 17

18. Go 学习笔记, 第 3 版 fmt.Printf("%p, %v\n", p, p.a) // 直接⽤用指针访问⺫⽬目标对象成员,⽆无须转换。 } 输出: 0x2101ef018, 1234 不能对指针做加减法等运算。 x := 1234 p := &x p++ // Error: invalid operation: p += 1 (mismatched types *int and int) 可以在 unsafe.Pointer 和任意类型指针间进⾏行转换。 func main() { x := 0x12345678 p := unsafe.Pointer(&x) // *int -> Pointer n := (*[4]byte)(p) // Pointer -> *[4]byte for i := 0; i < len(n); i++ { fmt.Printf("%X ", n[i]) } } 输出: 78 56 34 12 返回局部变量指针是安全的,编译器会根据需要将其分配在 GC Heap 上。 func test() *int { x := 100 return &x // 使⽤用 runtime.new 分配 x 内存。但在内联时,也可能直接分配在⺫⽬目标栈。 } 将 Pointer 转换成 uintptr,可变相实现指针运算。 func main() { d := struct { s string x int }{"abc", 100} p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr p += unsafe.Offsetof(d.x) // uintptr + offset 18

19. Go 学习笔记, 第 3 版 p2 := unsafe.Pointer(p) // uintptr -> Pointer px := (*int)(p2) // Pointer -> *int *px = 200 // d.x = 200 fmt.Printf("%#v\n", d) } 输出: struct { s string; x int }{s:"abc", x:200} 注意:GC 把 uintptr 当成普通整数对象,它⽆无法阻⽌止 "关联" 对象被回收。 1.8 ⾃自定义类型 可将类型分为命名和未命名两⼤大类。命名类型包括 bool、int、string 等,⽽而 array、 slice、map 等和具体元素类型、⻓长度等有关,属于未命名类型。 具有相同声明的未命名类型被视为同⼀一类型。 • 具有相同基类型的指针。 • 具有相同元素类型和⻓长度的 array。 • 具有相同元素类型的 slice。 • 具有相同键值类型的 map。 • 具有相同元素类型和传送⽅方向的 channel。 • 具有相同字段序列 (字段名、类型、标签、顺序) 的匿名 struct。 • 签名相同 (参数和返回值,不包括参数名称) 的 function。 • ⽅方法集相同 (⽅方法名、⽅方法签名相同,和次序⽆无关) 的 interface。 var a struct { x int `a` } var b struct { x int `ab` } // cannot use a (type struct { x int "a" }) as type struct { x int "ab" } in assignment b = a 可⽤用 type 在全局或函数内定义新类型。 func main() { type bigint int64 var x bigint = 100 println(x) } 19

20. Go 学习笔记, 第 3 版 新类型不是原类型的别名,除拥有相同数据存储结构外,它们之间没有任何关系,不会持 有原类型任何信息。除⾮非⺫⽬目标类型是未命名类型,否则必须显式转换。 x := 1234 var b bigint = bigint(x) // 必须显式转换,除⾮非是常量。 var b2 int64 = int64(b) var s myslice = []int{1, 2, 3} // 未命名类型,隐式转换。 var s2 []int = s 20

21. Go 学习笔记, 第 3 版 第 2 章 表达式 2.1 保留字 语⾔言设计简练,保留字不多。 break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var 2.2 运算符 全部运算符、分隔符,以及其他符号。 + & += &= && == != ( ) - | -= |= || < <= [ ] * ^ *= ^= <- > >= { } / << /= <<= ++ = := , ; % >> %= >>= -- ! ... . : &^ &^= 运算符结合律全部从左到右。 优先级 运算符 说明 ------------+---------------------------------------------+---------------------------- high * / & << >> & &^ + - |" ^ == != < <= < >= <- channel && low || 简单位运算演⽰示。 0110 & 1011 = 0010 AND 都为 1。 0110 | 1011 = 1111 OR ⾄至少⼀一个为 1。 0110 ^ 1011 = 1101 XOR 只能⼀一个为 1。 0110 &^ 1011 = 0100 AND NOT 清除标志位。 21

22. Go 学习笔记, 第 3 版 标志位操作。 a := 0 a |= 1 << 2 // 0000100: 在 bit2 设置标志位。 a |= 1 << 6 // 1000100: 在 bit6 设置标志位 a = a &^ (1 << 6) // 0000100: 清除 bit6 标志位。 不⽀支持运算符重载。尤其需要注意,"++"、"--" 是语句⽽而⾮非表达式。 n := 0 p := &n // b := n++ // syntax error // if n++ == 1 {} // syntax error // ++n // syntax error n++ *p++ // (*p)++ 没有 "~",取反运算也⽤用 "^"。 x := 1 x, ^x // 0001, -0010 2.3 初始化 初始化复合对象,必须使⽤用类型标签,且左⼤大括号必须在类型尾部。 // var a struct { x int } = { 100 } // syntax error // var b []int = { 1, 2, 3 } // syntax error // c := struct {x int; y string} // syntax error: unexpected semicolon or newline // { // } var a = struct{ x int }{100} var b = []int{1, 2, 3} 初始化值以 "," 分隔。可以分多⾏行,但最后⼀一⾏行必须以 "," 或 "}" 结尾。 a := []int{ 22

23. Go 学习笔记, 第 3 版 1, 2 // Error: need trailing comma before newline in composite literal } a := []int{ 1, 2, // ok } b := []int{ 1, 2 } // ok 2.4 控制流 2.4.1 IF 很特别的写法: • 可省略条件表达式括号。 • ⽀支持初始化语句,可定义代码块局部变量。 • 代码块左⼤大括号必须在条件表达式尾部。 x := 0 // if x > 10 // Error: missing condition in if statement // { // } if n := "abc"; x > 0 { // 初始化语句未必就是定义变量,⽐比如 println("init") 也是可以的。 println(n[2]) } else if x < 0 { // 注意 else if 和 else 左⼤大括号位置。 println(n[1]) } else { println(n[0]) } 不⽀支持三元操作符 "a > b ? a : b"。 23

24. Go 学习笔记, 第 3 版 2.4.2 For ⽀支持三种循环⽅方式,包括类 while 语法。 s := "abc" for i, n := 0, len(s); i < n; i++ { // 常⻅见的 for 循环,⽀支持初始化语句。 println(s[i]) } n := len(s) for n > 0 { // 替代 while (n > 0) {} println(s[n]) // 替代 for (; n > 0;) {} n-- } for { // 替代 while (true) {} println(s) // 替代 for (;;) {} } 不要期望编译器能理解你的想法,在初始化语句中计算出全部结果是个好主意。 func length(s string) int { println("call length.") return len(s) } func main() { s := "abcd" for i, n := 0, length(s); i < n; i++ { // 避免多次调⽤用 length 函数。 println(i, s[i]) } } 输出: call length. 0 97 1 98 2 99 3 100 2.4.3 Range 类似迭代器操作,返回 (索引, 值) 或 (键, 值)。 24

25. Go 学习笔记, 第 3 版 1st value 2nd value ------------------+-------------------+------------------+------------------- string index s[index] unicode, rune array/slice index s[index] map key m[key] channel element 可忽略不想要的返回值,或⽤用 "_" 这个特殊变量。 s := "abc" for i := range s { // 忽略 2nd value,⽀支持 string/array/slice/map。 println(s[i]) } for _, c := range s { // 忽略 index。 println(c) } m := map[string]int{"a": 1, "b": 2} for k, v := range m { // 返回 (key, value)。 println(k, v) } 注意,range 会复制对象。 a := [3]int{0, 1, 2} for i, v := range a { // index、value 都是从复制品中取出。 if i == 0 { // 在修改前,我们先修改原数组。 a[1], a[2] = 999, 999 fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。 } a[i] = v + 100 // 使⽤用复制品中取出的 value 修改原数组。 } fmt.Println(a) // 输出 [100, 101, 102]。 建议改⽤用引⽤用类型,其底层数据不会被复制。 s := []int{1, 2, 3, 4, 5} 25

26. Go 学习笔记, 第 3 版 for i, v := range s { // 复制 struct slice { pointer, len, cap }。 if i == 0 { s = s[:3] // 对 slice 的修改,不会影响 range。 s[2] = 100 // 对底层数据的修改。 } println(i, v) } 输出: 0 1 1 2 2 100 3 4 4 5 另外两种引⽤用类型 map、channel 是指针包装,⽽而不像 slice 是 struct。 2.4.4 Switch 分⽀支表达式可以是任意类型,不限于常量。可省略 break,默认⾃自动终⽌止。 x := []int{1, 2, 3} i := 2 switch i { case x[1]: println("a") case 1, 3: println("b") default: println("c") } 输出: a 如需要继续下⼀一分⽀支,可使⽤用 fallthrough,但不再判断条件。 x := 10 switch x { case 10: println("a") 26

27. Go 学习笔记, 第 3 版 fallthrough case 0: println("b") } 输出: a b 省略条件表达式,可当 if...else if...else 使⽤用。 switch { case x[1] > 0: println("a") case x[1] < 0: println("b") default: println("c") } switch i := x[2]; { // 带初始化语句 case i > 0: println("a") case i < 0: println("b") default: println("c") } 2.4.5 Goto, Break, Continue ⽀支持在函数内 goto 跳转。标签名区分⼤大⼩小写,未使⽤用标签引发错误。 func main() { var i int for { println(i) i++ if i > 2 { goto BREAK } } BREAK: println("break") EXIT: // Error: label EXIT defined and not used 27

28. Go 学习笔记, 第 3 版 } 配合标签,break 和 continue 可在多级嵌套循环中跳出。 func main() { L1: for x := 0; x < 3; x++ { L2: for y := 0; y < 5; y++ { if y > 2 { continue L2 } if x > 1 { break L1 } print(x, ":", y, " ") } println() } } 输出: 0:0 0:1 0:2 1:0 1:1 1:2 附:break 可⽤用于 for、switch、select,⽽而 continue 仅能⽤用于 for 循环。 x := 100 switch { case x >= 0: if x == 0 { break } println(x) } 28

29. Go 学习笔记, 第 3 版 第 3 章 函数 3.1 函数定义 不⽀支持 嵌套 (nested)、重载 (overload) 和 默认参数 (default parameter)。 • ⽆无需声明原型。 • ⽀支持不定⻓长变参。 • ⽀支持多返回值。 • ⽀支持命名返回参数。 • ⽀支持匿名函数和闭包。 使⽤用关键字 func 定义函数,左⼤大括号依旧不能另起⼀一⾏行。 func test(x, y int, s string) (int, string) { // 类型相同的相邻参数可合并。 n := x + y // 多返回值必须⽤用括号。 return n, fmt.Sprintf(s, n) } 函数是第⼀一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。 func test(fn func() int) int { return fn() } type FormatFunc func(s string, x, y int) string // 定义函数类型。 func format(fn FormatFunc, s string, x, y int) string { return fn(s, x, y) } func main() { s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。 s2 := format(func(s string, x, y int) string { return fmt.Sprintf(s, x, y) }, "%d, %d", 10, 20) println(s1, s2) } 29