Go学习笔记01-语言
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)
特殊只写变量 "_",用于忽略值占位。
- 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
- )
- 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
- )
- 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
- test(1) // 常量会被编译器自动转换。
- }
1.3 基本类型
更明确的数字类型命名,支持 Unicode,支持常用数据结构。
支持八进制、十六进制,以及科学记数法。标准库 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) // 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)
同样不能将其他类型当 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)
输出:
- 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 := "电脑"
- 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,以及包含包名前缀的 .T。
- 默认值 nil,没有 NULL 常量。
- 操作符 "&" 取变量地址,"*" 透过指针访问目标对象。
- 不支持指针运算,不支持 "->" 运算符,直接用 "." 访问目标成员。
- func main() {
- type data struct{ a int }
- var d = data{1234}
- var p *data
- p = &d
- 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 // 在堆上分配 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
- 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)
- }
新类型不是原类型的别名,除拥有相同数据存储结构外,它们之间没有任何关系,不会持有原类型任何信息。除非目标类型是未命名类型,否则必须显式转换。
- x := 1234
- var b bigint = bigint(x) // 必须显式转换,除非是常量。
- var b2 int64 = int64(b)
- var s myslice = []int{1, 2, 3} // 未命名类型,隐式转换。
- var s2 []int = s
第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 运算符
全部运算符、分隔符,以及其他符号。
运算符结合律全部从左到右。
简单位运算演示。
- 0110 & 1011 = 0010 AND 都为1。
- 0110 | 1011 = 1111 OR 至少一个为1。
- 0110 ^ 1011 = 1101 XOR 只能一个为1。
- 0110 &^ 1011 = 0100 AND NOT 清除标志位。
标志位操作。
- 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{
- 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"。
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
类似迭代器操作,返回 (索引, 值) 或 (键, 值)。
可忽略不想要的返回值,或用 "_" 这个特殊变量。
- s := "abc"
- for i := range s { // 忽略 2nd value,支持 string/array/slice/map。
- println(s[i])
- }
- for _, c := range s { // 忽略 index。
- println(c)
- }
- for range s { // 忽略全部返回值,仅迭代。
- ...
- }
- 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}
- 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")
- 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
- }
配合标签,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)
- }
第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)
- }
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
3.2 变参
变参本质上就是 slice。只能有一个,且必须是最后一个。
- func test(s string, n ...int) string {
- var x int
- for _, i := range n {
- x += i
- }
- return fmt.Sprintf(s, x)
- }
- func main() {
- println(test("sum: %d", 1, 2, 3))
- }
使用 slice 对象做变参时,必须展开。
- func main() {
- s := []int{1, 2, 3}
- println(test("sum: %d", s...))
- }
3.3 返回值
不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。
- func test() (int, int) {
- return 1, 2
- }
- func main() {
- // s := make([]int, 2)
- // s = test() // Error: multiple-value test() in single-value context
- x, _ := test()
- println(x)
- }
多返回值可直接作为其他函数调用实参。
- func test() (int, int) {
- return 1, 2
- }
- func add(x, y int) int {
- return x + y
- }
- func sum(n ...int) int {
- var x int
- for _, i := range n {
- x += i
- }
- return x
- }
- func main() {
- println(add(test()))
- println(sum(test()))
- }
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
- func add(x, y int) (z int) {
- z = x + y
- return
- }
- func main() {
- println(add(1, 2))
- }
命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
- func add(x, y int) (z int) {
- { // 不能在一个级别,引发 "z redeclared in this block" 错误。
- var z = x + y
- // return // Error: z is shadowed during return
- return z // 必须显式返回。
- }
- }
命名返回参数允许 defer 延迟调用通过闭包读取和修改。
- func add(x, y int) (z int) {
- defer func() {
- z += 100
- }()
- z = x + y
- return
- }
- func main() {
- println(add(1, 2)) // 输出: 103
- }
显式 return 返回前,会先修改命名返回参数。
- func add(x, y int) (z int) {
- defer func() {
- println(z) // 输出: 203
- }()
- z = x + y
- return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
- }
- func main() {
- println(add(1, 2)) // 输出: 203
- }
3.4 匿名函数
匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
- // --- function variable ---
- fn := func() { println("Hello, World!") }
- fn()
- // --- function collection ---
- fns := [](func(x int) int){
- func(x int) int { return x + 1 },
- func(x int) int { return x + 2 },
- }
- println(fns[0](100))
- // --- function as field ---
- d := struct {
- fn func() string
- }{
- fn: func() string { return "Hello, World!" },
- }
- println(d.fn())
- // --- channel of function ---
- fc := make(chan func() string, 2)
- fc <- func() string { return "Hello, World!" }
- println((<-fc)())
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
- func test() func() {
- x := 100
- fmt.Printf("x (%p) = %d\n", &x, x)
- return func() {
- fmt.Printf("x (%p) = %d\n", &x, x)
- }
- }
- func main() {
- f := test()
- f()
- }
输出:
- x (0x2101ef018) = 100
- x (0x2101ef018) = 100
在汇编层面,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调用匿名函数时,只需以某个寄存器传递该对象即可。
- FuncVal { func_address, closure_var_pointer ... }
3.5 延迟调用
关键字 defer 用于注册延迟调用。这些调用直到 ret 前才被执行,通常用于释放资源或错误处理。
- func test() error {
- f, err := os.Create("test.txt")
- if err != nil { return err }
- defer f.Close() // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
- f.WriteString("Hello, World!")
- return nil
- }
多个 defer 注册,按 FILO 次序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
- func test(x int) {
- defer println("a")
- defer println("b")
- defer func() {
- println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
- }()
- defer println("c")
- }
- func main() {
- test(0)
- }
输出:
- c
- b
- a
- panic: runtime error: integer divide by zero
延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。
- func test() {
- x, y := 10, 20
- defer func(i int) {
- println("defer:", i, y) // y 闭包引用
- }(x) // x 被复制
- x += 10
- y += 100
- println("x =", x, "y =", y)
- }
输出:
- x = 20 y = 120
- defer: 10 120
滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。
- var lock sync.Mutex
- func test() {
- lock.Lock()
- lock.Unlock()
- }
- func testdefer() {
- lock.Lock()
- defer lock.Unlock()
- }
- func BenchmarkTest(b *testing.B) {
- for i := 0; i < b.N; i++ {
- test()
- }
- }
- func BenchmarkTestDefer(b *testing.B) {
- for i := 0; i < b.N; i++ {
- testdefer()
- }
- }
输出:
- BenchmarkTest" 50000000 43 ns/op
- BenchmarkTestDefer 20000000 128 ns/op
3.6 错误处理
没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
- func test() {
- defer func() {
- if err := recover(); err != nil {
- println(err.(string)) // 将 interface{} 转型为具体类型。
- }
- }()
- panic("panic error!")
- }
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
- func panic(v interface{})
- func recover() interface{}
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
- func test() {
- defer func() {
- fmt.Println(recover())
- }()
- defer func() {
- panic("defer panic")
- }()
- panic("test panic")
- }
- func main() {
- test()
- }
输出:
- defer panic
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
- func test() {
- defer recover() // 无效!
- defer fmt.Println(recover()) // 无效!
- defer func() {
- func() {
- println("defer inner")
- recover() // 无效!
- }()
- }()
- panic("test panic")
- }
- func main() {
- test()
- }
输出:
- defer inner
- <nil>
- panic: test panic
使用延迟匿名函数或下面这样都是有效的。
- func except() {
- recover()
- }
- func test() {
- defer except()
- panic("test panic")
- }
如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。
- func test(x, y int) {
- var z int
- func() {
- defer func() {
- if recover() != nil { z = 0 }
- }()
- z = x / y
- return
- }()
- println("x / y =", z)
- }
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
- type error interface {
- Error() string
- }
标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
- var ErrDivByZero = errors.New("division by zero")
- func div(x, y int) (int, error) {
- if y == 0 { return 0, ErrDivByZero }
- return x / y, nil
- }
- func main() {
- switch z, err := div(10, 0); err {
- case nil:
- println(z)
- case ErrDivByZero:
- panic(err)
- }
- }
如何区别使用 panic 和 error 两种方式?惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
第4章 数据
4.1 Array
和以往认知的数组有很大不同。
- 数组是值类型,赋值和传参会复制整个数组,而不是指针。
- 数组长度必须是常量,且是类型的组成部分。[2]int 和 [3]int 是不同类型。
- 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
- 指针数组 [n]T,数组指针 [n]T。
可用复合语句初始化。
- a := [3]int{1, 2} // 未初始化元素值为0。
- b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
- c := [5]int{2: 100, 4:200} // 使用索引号初始化元素。
- d := [...]struct {
- name string
- age uint8
- }{
- {"user1", 10}, // 可省略元素类型。
- {"user2", 20}, // 别忘了最后一行的逗号。
- }
支持多维数组。
- a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
- b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
- func test(x [2]int) {
- fmt.Printf("x: %p\n", &x)
- x[1] = 1000
- }
- func main() {
- a := [2]int{}
- fmt.Printf("a: %p\n", &a)
- test(a)
- fmt.Println(a)
- }
输出:
- a: 0x2101f9150
- x: 0x2101f9170
- [0 0]
内置函数 len 和 cap 都返回数组长度 (元素数量)。
- a := [2]int{}
- println(len(a), cap(a)) // 2, 2
4.2 Slice
需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
runtime.h
- struct Slice
- { // must not move anything
- byte* array; // actual data
- uintgo len; // number of elements
- uintgo cap; // allocated number of elements
- };
- 引用类型。但自身是结构体,值拷贝传递。
- 属性 len 表示可用元素数量,读写操作不能超过该限制。
- 属性 cap 表示最大扩张容量,不能超出数组限制。
- 如果 slice == nil,那么 len、cap 结果都等于 0。
- data := [...]int{0, 1, 2, 3, 4, 5, 6}
- slice := data[1:4:5] // [low : high : max]
创建表达式使用的是元素索引号,而非数量。
读写操作实际目标是底层数组,只需注意索引号的差别。
- data := [...]int{0, 1, 2, 3, 4, 5}
- s := data[2:4]
- s[0] += 100
- s[1] += 200
- fmt.Println(s)
- fmt.Println(data)
输出:
- [102 203]
- [0 1 102 203 4 5]
可直接创建 slice 对象,自动分配底层数组。
- s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
- fmt.Println(s1, len(s1), cap(s1))
- s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
- fmt.Println(s2, len(s2), cap(s2))
- s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
- fmt.Println(s3, len(s3), cap(s3))
输出:
- [0 1 2 3 0 0 0 0 100] 9 9
- [0 0 0 0 0 0] 6 8
- [0 0 0 0 0 0] 6 6
使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。
- s := []int{0, 1, 2, 3}
- p := &s[2] // *int, 获取底层数组元素指针。
- *p += 100
- fmt.Println(s)
输出:
- [0 1 102 3]
至于 [][]T,是指元素类型为 []T 。
- data := [][]int{
- []int{1, 2, 3},
- []int{100, 200},
- []int{11, 22, 33, 44},
- }
可直接修改 struct array/slice 成员。
- d := [5]struct {
- x int
- }{}
- s := d[:]
- d[1].x = 10
- s[2].x = 20
- fmt.Println(d)
- fmt.Printf("%p, %p\n", &d, &d[0])
输出:
- [{0} {10} {20} {0} {0}]
- 0x20819c180, 0x20819c180
4.2.1 reslice
所谓 reslice,是基于已有 slice 创建新 slice 对象,以便在 cap 允许范围内调整属性。
- s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
- s1 := s[2:5] // [2 3 4]
- s2 := s1[2:6:7] // [4 5 6 7]
- s3 := s2[3:6] // Error
新对象依旧指向原底层数组。
- s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
- s1 := s[2:5] // [2 3 4]
- s1[2] = 100
- s2 := s1[2:6] // [100 5 6 7]
- s2[3] = 200
- fmt.Println(s)
输出:
- [0 1 2 3 100 5 6 200 8 9]
4.2.2 append
向 slice 尾部添加数据,返回新的 slice 对象。
- s := make([]int, 0, 5)
- fmt.Printf("%p\n", &s)
- s2 := append(s, 1)
- fmt.Printf("%p\n", &s2)
- fmt.Println(s, s2)
输出:
- 0x210230000
- 0x210230040
- [] [1]
简单点说,就是在 array[slice.high] 写数据。
- data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
- s := data[:3]
- s2 := append(s, 100, 200) // 添加多个值。
- fmt.Println(data)
- fmt.Println(s)
- fmt.Println(s2)
输出:
- [0 1 2 100 200 5 6 7 8 9]
- [0 1 2]
- [0 1 2 100 200]
一旦超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。
- data := [...]int{0, 1, 2, 3, 4, 10: 0}
- s := data[:2:3]
- s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。
- fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
- fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。
输出:
- [0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
- 0x20819c180 0x20817c0c0
从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。
通常以2倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。
- s := make([]int, 0, 1)
- c := cap(s)
- for i := 0; i < 50; i++ {
- s = append(s, i)
- if n := cap(s); n > c {
- fmt.Printf("cap: %d -> %d\n", c, n)
- c = n
- }
- }
输出:
- cap: 1 -> 2
- cap: 2 -> 4
- cap: 4 -> 8
- cap: 8 -> 16
- cap: 16 -> 32
- cap: 32 -> 64
4.2.3 copy
函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
- data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
- s := data[8:]
- s2 := data[:5]
- copy(s2, s) // dst:s2, src:s
- fmt.Println(s2)
- fmt.Println(data)
输出:
- [8 9 2 3 4]
- [8 9 2 3 4 5 6 7 8 9]
应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。
4.3 Map
引用类型,哈希表。键必须是支持相等运算符 (==、!=) 类型,比如 number、string、pointer、array、struct,以及对应的 interface。值可以是任意类型,没有限制。
- m := map[int]struct {
- name string
- age int
- }{
- 1: {"user1", 10}, // 可省略元素类型。
- 2: {"user2", 20},
- }
- println(m[1].name)
预先给 make 函数一个合理元素数量参数,有助于提升性能。因为事先申请一大块内存,可避免后续操作时频繁扩张。
- m := make(map[string]int, 1000)
常见操作:
- m := map[string]int{
- "a": 1,
- }
- if v, ok := m["a"]; ok { // 判断 key 是否存在。
- println(v)
- }
- println(m["c"]) // 对于不存在的 key,直接返回 \0,不会出错。
- m["b"] = 2 // 新增或修改。
- delete(m, "c") // 删除。如果 key 不存在,不会出错。
- println(len(m)) // 获取键值对数量。cap 无效。
- for k, v := range m { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
- println(k, v)
- }
不能保证迭代返回次序,通常是随机结果,具体和版本实现有关。
从 map 中取回的是一个 value 临时复制品,对其成员的修改是没有任何意义的。
- type user struct{ name string }
- m := map[int]user{ // 当 map 因扩张而重新哈希时,各键值项存储位置都会发生改变。 因此,map
- 1: {"user1"}, // 被设计成 not addressable。 类似 m[1].name 这种期望透过原 value
- } // 指针修改成员的行为自然会被禁止。
- m[1].name = "Tom" // Error: cannot assign to m[1].name
正确做法是完整替换 value 或使用指针。
- u := m[1]
- u.name = "Tom"
- m[1] = u // 替换 value。
- m2 := map[int]*user{
- 1: &user{"user1"},
- }
- m2[1].name = "Jack" // 返回的是指针复制品。透过指针修改原对象是允许的。
可以在迭代时安全删除键值。但如果期间有新增操作,那么就不知道会有什么意外了。
- for i := 0; i < 5; i++ {
- m := map[int]string{
- 0: "a", 1: "a", 2: "a", 3: "a", 4: "a",
- 5: "a", 6: "a", 7: "a", 8: "a", 9: "a",
- }
- for k := range m {
- m[k+k] = "x"
- delete(m, k)
- }
- fmt.Println(m)
- }
输出:
- map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
- map[12:x 16:x 20:x 28:x 36:x]
- map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
- map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
- map[12:x 16:x 20:x 28:x 36:x]
4.4 Struct
值类型,赋值和传参会复制全部内容。可用 "_" 定义补位字段,支持指向自身类型的指针成员。
- type Node struct {
- _ int
- id int
- data *byte
- next *Node
- }
- func main() {
- n1 := Node{
- id: 1,
- data: nil,
- }
- n2 := Node{
- id: 2,
- data: nil,
- next: &n1,
- }
- }
顺序初始化必须包含全部字段,否则会出错。
- type User struct {
- name string
- age int
- }
- u1 := User{"Tom", 20}
- u2 := User{"Tom"} // Error: too few values in struct initializer
支持匿名结构,可用作结构成员或定义变量。
- type File struct {
- name string
- size int
- attr struct {
- perm int
- owner int
- }
- }
- f := File{
- name: "test.txt",
- size: 1025,
- // attr: {0755, 1}, // Error: missing type in composite literal
- }
- f.attr.owner = 1
- f.attr.perm = 0755
- var attr = struct {
- perm int
- owner int
- }{2, 0755}
- f.attr = attr
支持 "=="、"!=" 相等操作符,可用作 map 键类型。
- type User struct {
- id int
- name string
- }
- m := map[User]int{
- User{1, "Tom"}: 100,
- }
可定义字段标签,用反射读取。标签是类型的组成部分。
- var u1 struct { name string "username" }
- var u2 struct { name string }
- u2 = u1 // Error: cannot use u1 (type struct { name string "username" }) as
- // type struct { name string } in assignment
空结构 "节省" 内存,比如用来实现 set 数据结构,或者实现没有 "状态" 只有方法的 "静态类"。
- var null struct{}
- set := make(map[string]struct{})
- set["a"] = null
4.4.1 匿名字段
匿名字段不过是一种语法糖,从根本上说,就是一个与成员类型同名 (不含包名) 的字段。被匿名嵌入的可以是任何类型,当然也包括指针。
- type User struct {
- name string
- }
- type Manager struct {
- User
- title string
- }
- m := Manager{
- User: User{"Tom"}, // 匿名字段的显式字段名,和类型名相同。
- title: "Administrator",
- }
可以像普通字段那样访问匿名字段成员,编译器从外向内逐级查找所有层次的匿名字段,直到发现目标或出错。
- type Resource struct {
- id int
- }
- type User struct {
- Resource
- name string
- }
- type Manager struct {
- User
- title string
- }
- var m Manager
- m.id = 1
- m.name = "Jack"
- m.title = "Administrator"
外层同名字段会遮蔽嵌入字段成员,相同层次的同名字段也会让编译器无所适从。解决方法是使用显式字段名。
- type Resource struct {
- id int
- name string
- }
- type Classify struct {
- id int
- }
- type User struct {
- Resource // Resource.id 与 Classify.id 处于同一层次。
- Classify
- name string // 遮蔽 Resource.name。
- }
- u := User{
- Resource{1, "people"},
- Classify{100},
- "Jack",
- }
- println(u.name) // User.name: Jack
- println(u.Resource.name) // people
- // println(u.id) // Error: ambiguous selector u.id
- println(u.Classify.id) // 100
不能同时嵌入某一类型和其指针类型,因为它们名字相同。
- type Resource struct {
- id int
- }
- type User struct {
- *Resource
- // Resource // Error: duplicate field Resource
- name string
- }
- u := User{
- &Resource{1},
- "Administrator",
- }
- println(u.id)
- println(u.Resource.id)
4.4.2 面向对象
面向对象三大特征里,Go 仅支持封装,尽管匿名字段的内存布局和行为类似继承。没有 class 关键字,没有继承、多态等等。
- type User struct {
- id int
- name string
- }
- type Manager struct {
- User
- title string
- }
- m := Manager{User{1, "Tom"}, "Administrator"}
- // var u User = m // Error: cannot use m (type Manager) as type User in assignment
- // 没有继承,自然也不会有多态。
- var u User = m.User // 同类型拷贝。
内存布局和 C struct 相同,没有任何附加的 object 信息。
可用 unsafe 包相关函数输出内存地址信息。
- m : 0x2102271b0, size: 40, align: 8
- m.id : 0x2102271b0, offset: 0
- m.name : 0x2102271b8, offset: 8
- m.title: 0x2102271c8, offset: 24
第 5 章 方法
5.1 方法定义
方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
- 只能为当前包内命名类型定义方法。
- 参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。
- 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
- 不支持方法重载,receiver 只是参数签名的组成部分。
- 可用实例 value 或 pointer 调用全部方法,编译器自动转换。
没有构造和析构方法,通常用简单工厂模式返回对象实例。
- type Queue struct {
- elements []interface{}
- }
- func NewQueue() *Queue { // 创建对象实例。
- return &Queue{make([]interface{}, 10)}
- }
- func (*Queue) Push(e interface{}) error { // 省略 receiver 参数名。
- panic("not implemented")
- }
- // func (Queue) Push(e int) error { // Error: method redeclared: Queue.Push
- // panic("not implemented")
- // }
- func (self *Queue) length() int { // receiver 参数名可以是 self、this 或其他。
- return len(self.elements)
- }
方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。
- type Data struct{
- x int
- }
- func (self Data) ValueTest() { // func ValueTest(self Data);
- fmt.Printf("Value: %p\n", &self)
- }
- func (self *Data) PointerTest() { // func PointerTest(self *Data);
- fmt.Printf("Pointer: %p\n", self)
- }
- func main() {
- d := Data{}
- p := &d
- fmt.Printf("Data: %p\n", p)
- d.ValueTest() // ValueTest(d)
- d.PointerTest() // PointerTest(&d)
- p.ValueTest() // ValueTest(*p)
- p.PointerTest() // PointerTest(p)
- }
输出:
- Data : 0x2101ef018
- Value : 0x2101ef028
- Pointer: 0x2101ef018
- Value : 0x2101ef030
- Pointer: 0x2101ef018
从1.4开始,不再支持多级指针查找方法成员。
- type X struct{}
- func (*X) test() {
- println("X.test")
- }
- func main() {
- p := &X{}
- p.test()
- // Error: calling method with receiver &p (type **X) requires explicit dereference
- // (&p).test()
- }
5.2 匿名字段
可以像字段成员那样访问匿名字段方法,编译器负责查找。
- type User struct {
- id int
- name string
- }
- type Manager struct {
- User
- }
- func (self *User) ToString() string { // receiver = &(Manager.User)
- return fmt.Sprintf("User: %p, %v", self, self)
- }
- func main() {
- m := Manager{User{1, "Tom"}}
- fmt.Printf("Manager: %p\n", &m)
- fmt.Println(m.ToString())
- }
输出:
- Manager: 0x2102281b0
- User : 0x2102281b0, &{1 Tom}
通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。
- type User struct {
- id int
- name string
- }
- type Manager struct {
- User
- title string
- }
- func (self *User) ToString() string {
- return fmt.Sprintf("User: %p, %v", self, self)
- }
- func (self *Manager) ToString() string {
- return fmt.Sprintf("Manager: %p, %v", self, self)
- }
- func main() {
- m := Manager{User{1, "Tom"}, "Administrator"}
- fmt.Println(m.ToString())
- fmt.Println(m.User.ToString())
- }
输出:
- Manager: 0x2102271b0, &{{1 Tom} Administrator}
- User : 0x2102271b0, &{1 Tom}
5.3 方法集
每个类型都有与之关联的方法集,这会影响到接口实现规则。
- 类型 T 方法集包含全部 receiver T 方法。
- 类型 T 方法集包含全部 receiver T + T 方法。
- 如类型 S 包含匿名字段 T,则 S 方法集包含 T 方法。
- 如类型 S 包含匿名字段 T,则 S 方法集包含 T + T 方法。
- 不管嵌入 T 或 T,S 方法集总是包含 T + *T 方法。
用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
5.4 表达式
根据调用者不同,方法分为两种表现形式:
- instance.method(args...) ---> <type>.func(instance, args...)
前者称为 method value,后者 method expression。
两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。
- type User struct {
- id int
- name string
- }
- func (self *User) Test() {
- fmt.Printf("%p, %v\n", self, self)
- }
- func main() {
- u := User{1, "Tom"}
- u.Test()
- mValue := u.Test
- mValue() // 隐式传递 receiver
- mExpression := (*User).Test
- mExpression(&u) // 显式传递 receiver
- }
输出:
- 0x210230000, &{1 Tom}
- 0x210230000, &{1 Tom}
- 0x210230000, &{1 Tom}
需要注意,method value 会复制 receiver。
- type User struct {
- id int
- name string
- }
- func (self User) Test() {
- fmt.Println(self)
- }
- func main() {
- u := User{1, "Tom"}
- mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。
- u.id, u.name = 2, "Jack"
- u.Test()
- mValue()
- }
输出:
- {2 Jack}
- {1 Tom}
在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。
- FuncVal { method_address, receiver_copy }
可依据方法集转换 method expression,注意 receiver 类型的差异。
- type User struct {
- id int
- name string
- }
- func (self *User) TestPointer() {
- fmt.Printf("TestPointer: %p, %v\n", self, self)
- }
- func (self User) TestValue() {
- fmt.Printf("TestValue: %p, %v\n", &self, self)
- }
- func main() {
- u := User{1, "Tom"}
- fmt.Printf("User: %p, %v\n", &u, u)
- mv := User.TestValue
- mv(u)
- mp := (*User).TestPointer
- mp(&u)
- mp2 := (*User).TestValue // *User 方法集包含 TestValue。
- mp2(&u) // 签名变为 func TestValue(self *User)。
- } // 实际依然是 receiver value copy。
输出:
- User : 0x210231000, {1 Tom}
- TestValue : 0x210231060, {1 Tom}
- TestPointer: 0x210231000, &{1 Tom}
- TestValue : 0x2102310c0, {1 Tom}
将方法 "还原" 成函数,就容易理解下面的代码了。
- type Data struct{}
- func (Data) TestValue() {}
- func (*Data) TestPointer() {}
- func main() {
- var p *Data = nil
- p.TestPointer()
- (*Data)(nil).TestPointer() // method value
- (*Data).TestPointer(nil) // method expression
- // p.TestValue() // invalid memory address or nil pointer dereference
- // (Data)(nil).TestValue() // cannot convert nil to type Data
- // Data.TestValue(nil) // cannot use nil as type Data in function argument
- }
第 6 章 接口
6.1 接口定义
接口是一个或多个方法签名的集合,任何类型的方法集中只要拥有与之对应的全部方法,就表示它 "实现" 了该接口,无须在该类型上显式添加接口声明。
所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。当然,该类型还可以有其他方法。
- 接口命名习惯以 er 结尾,结构体。
- 接口只有方法签名,没有实现。
- 接口没有数据字段。
- 可在接口中嵌入其他接口。
- 类型可实现多个接口。
- type Stringer interface {
- String() string
- }
- type Printer interface {
- Stringer // 接口嵌入。
- Print()
- }
- type User struct {
- id int
- name string
- }
- func (self *User) String() string {
- return fmt.Sprintf("user %d, %s", self.id, self.name)
- }
- func (self *User) Print() {
- fmt.Println(self.String())
- }
- func main() {
- var t Printer = &User{1, "Tom"} // *User 方法集包含 String、Print。
- t.Print()
- }
输出:
- user 1, Tom
空接口 interface{} 没有任何方法签名,也就意味着任何类型都实现了空接口。其作用类似面向对象语言中的根对象 object。
- func Print(v interface{}) {
- fmt.Printf("%T: %v\n", v, v)
- }
- func main() {
- Print(1)
- Print("Hello, World!")
- }
输出:
- int: 1
- string: Hello, World!
匿名接口可用作变量类型,或结构成员。
- type Tester struct {
- s interface {
- String() string
- }
- }
- type User struct {
- id int
- name string
- }
- func (self *User) String() string {
- return fmt.Sprintf("user %d, %s", self.id, self.name)
- }
- func main() {
- t := Tester{&User{1, "Tom"}}
- fmt.Println(t.s.String())
- }
输出:
- user 1, Tom
6.2 执行机制
接口对象由接口表 (interface table) 指针和数据指针组成。
runtime.h
- struct Iface
- {
- Itab* tab;
- void* data;
- };
- struct Itab
- {
- InterfaceType* inter;
- Type* type;
- void (*fun[])(void);
- };
接口表存储元数据信息,包括接口类型、动态类型,以及实现接口的方法指针。无论是反射还是通过接口调用方法,都会用到这些信息。
数据指针持有的是目标对象的只读复制品,复制完整对象或指针。
- type User struct {
- id int
- name string
- }
- func main() {
- u := User{1, "Tom"}
- var i interface{} = u
- u.id = 2
- u.name = "Jack"
- fmt.Printf("%v\n", u)
- fmt.Printf("%v\n", i.(User))
- }
输出:
- {2 Jack}
- {1 Tom}
接口转型返回临时对象,只有使用指针才能修改其状态。
- type User struct {
- id int
- name string
- }
- func main() {
- u := User{1, "Tom"}
- var vi, pi interface{} = u, &u
- // vi.(User).name = "Jack" // Error: cannot assign to vi.(User).name
- pi.(*User).name = "Jack"
- fmt.Printf("%v\n", vi.(User))
- fmt.Printf("%v\n", pi.(*User))
- }
输出:
- {1 Tom}
- &{1 Jack}
只有 tab 和 data 都为 nil 时,接口才等于 nil。
- var a interface{} = nil // tab = nil, data = nil
- var b interface{} = (*int)(nil) // tab 包含 *int 类型信息, data = nil
- type iface struct {
- itab, data uintptr
- }
- ia := *(*iface)(unsafe.Pointer(&a))
- ib := *(*iface)(unsafe.Pointer(&b))
- fmt.Println(a == nil, ia)
- fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
输出:
- true {0 0}
- false {505728 0} true
6.3 接口转换
利用类型推断,可判断接口对象是否某个具体的接口或类型。
- type User struct {
- id int
- name string
- }
- func (self *User) String() string {
- return fmt.Sprintf("%d, %s", self.id, self.name)
- }
- func main() {
- var o interface{} = &User{1, "Tom"}
- if i, ok := o.(fmt.Stringer); ok { // ok-idiom
- fmt.Println(i)
- }
- u := o.(*User)
- // u := o.(User) // panic: interface is *main.User, not main.User
- fmt.Println(u)
- }
还可用 switch 做批量类型判断,不支持 fallthrough。
- func main() {
- var o interface{} = &User{1, "Tom"}
- switch v := o.(type) {
- case nil: // o == nil
- fmt.Println("nil")
- case fmt.Stringer: // interface
- fmt.Println(v)
- case func() string: // func
- fmt.Println(v())
- case *User: // *struct
- fmt.Printf("%d, %s\n", v.id, v.name)
- default:
- fmt.Println("unknown")
- }
- }
超集接口对象可转换为子集接口,反之出错。
- type Stringer interface {
- String() string
- }
- type Printer interface {
- String() string
- Print()
- }
- type User struct {
- id int
- name string
- }
- func (self *User) String() string {
- return fmt.Sprintf("%d, %v", self.id, self.name)
- }
- func (self *User) Print() {
- fmt.Println(self.String())
- }
- func main() {
- var o Printer = &User{1, "Tom"}
- var s Stringer = o
- fmt.Println(s.String())
- }
6.4 接口技巧
让编译器检查,以确保某个类型实现接口。
- var _ fmt.Stringer = (*Data)(nil)
某些时候,让函数直接 "实现" 接口能省不少事。
- type Tester interface {
- Do()
- }
- type FuncDo func()
- func (self FuncDo) Do() { self() }
- func main() {
- var t Tester = FuncDo(func() { println("Hello, World!") })
- t.Do()
- }
第 7 章 并发
7.1 Goroutine
Go 在语言层面对并发编程提供支持,一种类似协程,称作 goroutine 的机制。
只需在函数调用语句前添加 go 关键字,就可创建并发执行单元。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务。
事实上,入口函数 main 就以 goroutine 运行。另有与之配套的 channel 类型,用以实现 "以通讯来共享内存" 的 CSP 模式。相关实现细节可参考本书第二部分的源码剖析。
- go func() {
- println("Hello, World!")
- }()
调度器不能保证多个 goroutine 执行次序,且进程退出时不会等待它们结束。
默认情况下,进程启动后仅允许一个系统线程服务于 goroutine。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。
- func sum(id int) {
- var x int64
- for i := 0; i < math.MaxUint32; i++ {
- x += int64(i)
- }
- println(id, x)
- }
- func main() {
- wg := new(sync.WaitGroup)
- wg.Add(2)
- for i := 0; i < 2; i++ {
- go func(id int) {
- defer wg.Done()
- sum(id)
- }(i)
- }
- wg.Wait()
- }
输出:
- $ go build -o test
- $ time -p ./test
- 0 9223372030412324865
- 1 9223372030412324865
- real 7.70 // 程序开始到结束时间差 (非 CPU 时间)
- user 7.66 // 用户态所使用 CPU 时间片 (多核累加)
- sys 0.01 // 内核态所使用 CPU 时间片
- $ GOMAXPROCS=2 time -p ./test
- 0 9223372030412324865
- 1 9223372030412324865
- real 4.18
- user 7.61 // 虽然总时间差不多,但由 2 个核并行,real 时间自然少了许多。
- sys 0.02
调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。
- func main() {
- wg := new(sync.WaitGroup)
- wg.Add(1)
- go func() {
- defer wg.Done()
- defer println("A.defer")
- func() {
- defer println("B.defer")
- runtime.Goexit() // 终止当前 goroutine
- println("B") // 不会执行
- }()
- println("A") // 不会执行
- }()
- wg.Wait()
- }
输出:
- B.defer
- A.defer
和协程 yield 作用类似,Gosched 让出底层线程,将当前 goroutine 暂停,放回队列等待下次被调度执行。
- func main() {
- wg := new(sync.WaitGroup)
- wg.Add(2)
- go func() {
- defer wg.Done()
- for i := 0; i < 6; i++ {
- println(i)
- if i == 3 { runtime.Gosched() }
- }
- }()
- go func() {
- defer wg.Done()
- println("Hello, World!")
- }()
- wg.Wait()
- }
输出:
- $ go run main.go
- 0
- 1
- 2
- 3
- Hello, World!
- 4
- 5
7.2 Channel
引用类型 channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
默认为同步模式,需要发送和接收配对。否则会被阻塞,直到另一方准备好后被唤醒。
- func main() {
- data := make(chan int) // 数据交换队列
- exit := make(chan bool) // 退出通知
- go func() {
- for d := range data { // 从队列迭代接收数据,直到 close 。
- fmt.Println(d)
- }
- fmt.Println("recv over.")
- exit <- true // 发出退出通知。
- }()
- data <- 1 // 发送数据。
- data <- 2
- data <- 3
- close(data) // 关闭队列。
- fmt.Println("send over.")
- <-exit // 等待退出通知。
- }
输出:
- 1
- 2
- 3
- send over.
- recv over.
异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。
通常情况下,异步 channel 可减少排队阻塞,具备更高的效率。但应该考虑使用指针规避大对象拷贝,将多个元素打包,减小缓冲区大小等。
- func main() {
- data := make(chan int, 3) // 缓冲区可以存储 3 个元素
- exit := make(chan bool)
- data <- 1 // 在缓冲区未满前,不会阻塞。
- data <- 2
- data <- 3
- go func() {
- for d := range data { // 在缓冲区未空前,不会阻塞。
- fmt.Println(d)
- }
- exit <- true
- }()
- data <- 4 // 如果缓冲区已满,阻塞。
- data <- 5
- close(data)
- <-exit
- }
缓冲区是内部属性,并非类型构成要素。
- var a, b chan int = make(chan int), make(chan int, 3)
除用 range 外,还可用 ok-idiom 模式判断 channel 是否关闭。
- for {
- if d, ok := <-data; ok {
- fmt.Println(d)
- } else {
- break
- }
- }
向 closed channel 发送数据引发 panic 错误,接收立即返回零值。而 nil channel,无论收发都会被阻塞。
内置函数 len 返回未被读取的缓冲元素数量,cap 返回缓冲区大小。
- d1 := make(chan int)
- d2 := make(chan int, 3)
- d2 <- 1
- fmt.Println(len(d1), cap(d1)) // 0 0
- fmt.Println(len(d2), cap(d2)) // 1 3
7.2.1 单向
可以将 channel 隐式转换为单向队列,只收或只发。
- c := make(chan int, 3)
- var send chan<- int = c // send-only
- var recv <-chan int = c // receive-only
- send <- 1
- // <-send // Error: receive from send-only type chan<- int
- <-recv
- // recv <- 2 // Error: send to receive-only type <-chan int
不能将单向 channel 转换为普通 channel。
- d := (chan int)(send) // Error: cannot convert type chan<- int to type chan int
- d := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int
7.2.2 选择
如果需要同时处理多个 channel,可使用 select 语句。它随机选择一个可用 channel 做收发操作,或执行 default case。
- func main() {
- a, b := make(chan int, 3), make(chan int)
- go func() {
- v, ok, s := 0, false, ""
- for {
- select { // 随机选择可用 channel,接收数据。
- case v, ok = <-a: s = "a"
- case v, ok = <-b: s = "b"
- }
- if ok {
- fmt.Println(s, v)
- } else {
- os.Exit(0)
- }
- }
- }()
- for i := 0; i < 5; i++ {
- select { // 随机选择可用 channel,发送数据。
- case a <- i:
- case b <- i:
- }
- }
- close(a)
- select {} // 没有可用 channel,阻塞 main goroutine。
- }
输出:
- b 3
- a 0
- a 1
- a 2
- b 4
在循环中使用 select default case 需要小心,避免形成洪水。
7.2.3 模式
用简单工厂模式打包并发任务和 channel。
- func NewTest() chan int {
- c := make(chan int)
- rand.Seed(time.Now().UnixNano())
- go func() {
- time.Sleep(time.Second)
- c <- rand.Int()
- }()
- return c
- }
- func main() {
- t := NewTest()
- println(<-t) // 等待 goroutine 结束返回。
- }
用 channel 实现信号量 (semaphore)。
- func main() {
- wg := sync.WaitGroup{}
- wg.Add(3)
- sem := make(chan int, 1)
- for i := 0; i < 3; i++ {
- go func(id int) {
- defer wg.Done()
- sem <- 1 // 向 sem 发送数据,阻塞或者成功。
- for x := 0; x < 3; x++ {
- fmt.Println(id, x)
- }
- <-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。
- }(i)
- }
- wg.Wait()
- }
输出:
- $ GOMAXPROCS=2 go run main.go
- 0 0
- 0 1
- 0 2
- 1 0
- 1 1
- 1 2
- 2 0
- 2 1
- 2 2
用 closed channel 发出退出通知。
- func main() {
- var wg sync.WaitGroup
- quit := make(chan bool)
- for i := 0; i < 2; i++ {
- wg.Add(1)
- go func(id int) {
- defer wg.Done()
- task := func() {
- println(id, time.Now().Nanosecond())
- time.Sleep(time.Second)
- }
- for {
- select {
- case <-quit: // closed channel 不会阻塞,因此可用作退出通知。
- return
- default: // 执行正常任务。
- task()
- }
- }
- }(i)
- }
- time.Sleep(time.Second * 5) // 让测试 goroutine 运行一会。
- close(quit) // 发出退出通知。
- wg.Wait()
- }
用 select 实现超时 (timeout)。
- func main() {
- w := make(chan bool)
- c := make(chan int, 2)
- go func() {
- select {
- case v := <-c: fmt.Println(v)
- case <-time.After(time.Second * 3): fmt.Println("timeout.")
- }
- w <- true
- }()
- // c <- 1 // 注释掉,引发 timeout。
- <-w
- }
channel 是第一类对象,可传参 (内部实现为指针) 或者作为结构成员。
- type Request struct {
- data []int
- ret chan int
- }
- func NewRequest(data ...int) *Request {
- return &Request{ data, make(chan int, 1) }
- }
- func Process(req *Request) {
- x := 0
- for _, i := range req.data {
- x += i
- }
- req.ret <- x
- }
- func main() {
- req := NewRequest(10, 20, 30)
- Process(req)
- fmt.Println(<-req.ret)
- }
第 8 章 包
8.1 工作空间
编译工具对源码目录有严格要求,每个工作空间 (workspace) 必须由 bin、pkg、src 三个目录组成。
可在 GOPATH 环境变量列表中添加多个工作空间,但不能和 GOROOT 相同。
- export GOPATH=$HOME/projects/golib:$HOME/projects/go
通常 go get 使用第一个工作空间保存下载的第三方库。
8.2 源文件
编码:源码文件必须是 UTF-8 格式,否则会导致编译器出错。 结束:语句以 ";" 结束,多数时候可以省略。 注释:支持 "//"、"/**/" 两种注释方式,不能嵌套。 命名:采用 camelCasing 风格,不建议使用下划线。
8.3 包结构
所有代码都必须组织在 package 中。
- 源文件头部以 "package " 声明包名称。
- 包由同一目录下的多个源码文件组成。
- 包名类似 namespace,与包所在目录名、编译文件名无关。
- 目录名最好不用 main、all、std 这三个保留名称。
- 可执行文件必须包含 package main,入口函数 main。
- 说明:os.Args 返回命令行参数,os.Exit 终止进程。
- 要获取正确的可执行文件路径,可用 filepath.Abs(exec.LookPath(os.Args[0]))。
包中成员以名称首字母大小写决定访问权限。
- public: 首字母大写,可被包外访问。
- internal: 首字母小写,仅包内成员可以访问。
该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。
8.3.1 导入包
使用包成员前,必须先用 import 关键字导入,但不能形成导入循环。
- import "相对目录/包主文件名"
相对目录是指从 /pkg/ <os_arch>开始的子目录,以标准库为例:
- import "fmt" -> /usr/local/go/pkg/darwin_amd64/fmt.a
- import "os/exec" -> /usr/local/go/pkg/darwin_amd64/os/exec.a
在导入时,可指定包成员访问方式。比如对包重命名,以避免同名冲突。
- import "yuhen/test" // 默认模式: test.A
- import M "yuhen/test" // 包重命名: M.A
- import . "yuhen/test" // 简便模式: A
- import _ "yuhen/test" // 非导入模式: 仅让该包执行初始化函数。
未使用的导入包,会被编译器视为错误 (不包括 "import _")。
- ./main.go:4: imported and not used: "fmt"
对于当前目录下的子包,除使用默认完整导入路径外,还可使用 local 方式。
main.go
- import "learn/test" // 正常模式
- import "./test" // 本地模式,仅对 go run main.go 有效。
8.3.2 自定义路径
可通过 meta 设置为代码库设置自定义路径。
server.go
- package main
- import (
- "fmt"
- "net/http"
- )
- func handler(w http.ResponseWriter, r *http.Request) {
- fmt.Fprint(w, `<meta name="go-import"
- content="test.com/qyuhen/test git https://github.com/qyuhen/test">`)
- }
- func main() {
- http.HandleFunc("/qyuhen/test", handler)
- http.ListenAndServe(":80", nil)
- }
该示例使用自定义域名 test.com 重定向到 github。
- $ go get -v test.com/qyuhen/test
- Fetching https://test.com/qyuhen/testgo-get=1
- https fetch failed.
- Fetching http://test.com/qyuhen/testgo-get=1
- Parsing meta tags from http://test.com/qyuhen/testgo-get=1 (status code 200)
- get "test.com/qyuhen/test": found meta tag http://test.com/qyuhen/testgo-get=1
- test.com/qyuhen/test (download)
- test.com/qyuhen/test
如此,该库就有两个有效导入路径,可能会导致存储两个本地副本。为此,可以给库添加专门的 "import comment"。当 go get 下载完成后,会检查本地存储路径和该注释是否一致。
github.com/qyuhen/test/abc.go
- package test // import "test.com/qyuhen/test"
- func Hello() {
- println("Hello, Custom import path!")
- }
如继续用 github 路径,会导致 go build 失败。
- $ go get -v github.com/qyuhen/test
- github.com/qyuhen/test (download)
- package github.com/qyuhen/test
- " imports github.com/qyuhen/test
- " imports github.com/qyuhen/test: expects import "test.com/qyuhen/test"
这就强制包用户使用唯一路径,也便于日后将包迁移到其他位置。
资源:Go 1.4 Custom Import Path Checking
8.3.3 初始化
初始化函数:
- 每个源文件都可以定义一个或多个初始化函数。
- 编译器不保证多个初始化函数执行次序。
- 初始化函数在单一线程被调用,仅执行一次。
- 初始化函数在包所有全局变量初始化后执行。
- 在所有初始化函数结束后才执行 main.main。
- 无法调用初始化函数。
因为无法保证初始化函数执行顺序,因此全局变量应该直接用 var 初始化。
- var now = time.Now()
- func init() {
- fmt.Printf("now: %v\n", now)
- }
- func init() {
- fmt.Printf("since: %v\n", time.Now().Sub(now))
- }
可在初始化函数中使用 goroutine,可等待其结束。
- var now = time.Now()
- func main() {
- fmt.Println("main:", int(time.Now().Sub(now).Seconds()))
- }
- func init() {
- fmt.Println("init:", int(time.Now().Sub(now).Seconds()))
- w := make(chan bool)
- go func() {
- time.Sleep(time.Second * 3)
- w <- true
- }()
- <-w
- }
输出:
- init: 0
- main: 3
不应该滥用初始化函数,仅适合完成当前文件中的相关环境设置。
8.4 文档
扩展工具 godoc 能自动提取注释生成帮助文档。
- 仅和成员相邻 (中间没有空行) 的注释被当做帮助信息。
- 相邻行会合并成同一段落,用空行分隔段落。
- 缩进表示格式化文本,比如示例代码。
- 自动转换 URL 为链接。
- 自动合并多个源码文件中的 package 文档。
- 无法显式 package main 中的成员文档。
8.4.1 Package
- 建议用专门的 doc.go 保存 package 帮助信息。
- 包文档第一整句 (中英文句号结束) 被当做 packages 列表说明。
8.4.2 Example
只要 Example 测试函数名称符合以下规范即可。
- 说明:使用 suffix 作为示例名称,其首字母必须小写。如果文件中仅有一个 Example 函数,且调用了该文件中的其他成员,那么示例会显示整个文件内容,而不仅仅是测试函数自己。
8.4.3 Bug
非测试源码文件中以 BUG(author) 开始的注释,会在帮助文档 Bugs 节点中显示。
- // BUG(yuhen): memory leak.
第 9 章 进阶
9.1 内存布局
了解对象内存布局,有助于理解值传递、引用传递等概念。
string
struct
slice
interface
new
make
9.2 指针陷阱
对象内存分配会受编译参数影响。举个例子,当函数返回对象指针时,必然在堆上分配。
可如果该函数被内联,那么这个指针就不会跨栈帧使用,就有可能直接在栈上分配,以实现代码优化目的。因此,是否阻止内联对指针输出结果有很大影响。
允许指针指向对象成员,并确保该对象是可达状态。
除正常指针外,指针还有 unsafe.Pointer 和 uintptr 两种形态。其中 uintptr 被 GC 当做普通整数对象,它不能阻止所 "引用" 对象被回收。
- type data struct {
- x [1024 * 100]byte
- }
- func test() uintptr {
- p := &data{}
- return uintptr(unsafe.Pointer(p))
- }
- func main() {
- const N = 10000
- cache := new([N]uintptr)
- for i := 0; i < N; i++ {
- cache[i] = test()
- time.Sleep(time.Millisecond)
- }
- }
输出:
- $ go build -o test && GODEBUG="gctrace=1" ./test
- gc607(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3070-3025) objects
- gc611(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3090-3045) objects
- gc613(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3100-3055) objects
合法的 unsafe.Pointer 被当做普通指针对待。
- func test() unsafe.Pointer {
- p := &data{}
- return unsafe.Pointer(p)
- }
- func main() {
- const N = 10000
- cache := new([N]unsafe.Pointer)
- for i := 0; i < N; i++ {
- cache[i] = test()
- time.Sleep(time.Millisecond)
- }
- }
输出:
- $ go build -o test && GODEBUG="gctrace=1" ./test
- gc12(1): 0+0+0 ms, 199 -> 199 MB 2088 -> 2088 (2095-7) objects
- gc13(1): 0+0+0 ms, 399 -> 399 MB 4136 -> 4136 (4143-7) objects
- gc14(1): 0+0+0 ms, 799 -> 799 MB 8232 -> 8232 (8239-7) objects
指向对象成员的 unsafe.Pointer,同样能确保对象不被回收。
- type data struct {
- x [1024 * 100]byte
- y int
- }
- func test() unsafe.Pointer {
- d := data{}
- return unsafe.Pointer(&d.y)
- }
- func main() {
- const N = 10000
- cache := new([N]unsafe.Pointer)
- for i := 0; i < N; i++ {
- cache[i] = test()
- time.Sleep(time.Millisecond)
- }
- }
输出:
- $ go build -o test && GODEBUG="gctrace=1" ./test
- gc12(1): 0+0+0 ms, 207 -> 207 MB 2088 -> 2088 (2095-7) objects
- gc13(1): 1+0+0 ms, 415 -> 415 MB 4136 -> 4136 (4143-7) objects
- gc14(1): 3+1+0 ms, 831 -> 831 MB 8232 -> 8232 (8239-7) objects
由于可以用 unsafe.Pointer、uintptr 创建 "dangling pointer" 等非法指针,所以在使用时需要特别小心。另外,cgo C.malloc 等函数所返回指针,与 GC 无关。
指针构成的 "循环引用" 加上 runtime.SetFinalizer 会导致内存泄露。
- type Data struct {
- d [1024 * 100]byte
- o *Data
- }
- func test() {
- var a, b Data
- a.o = &b
- b.o = &a
- runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
- runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
- }
- func main() {
- for {
- test()
- time.Sleep(time.Millisecond)
- }
- }
输出:
- $ go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test
- gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
- gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
- gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects
垃圾回收器能正确处理 "指针循环引用",但无法确定 Finalizer 依赖次序,也就无法调用Finalizer 函数,这会导致目标对象无法变成不可达状态,其所占用内存无法被回收。
9.3 cgo
通过 cgo,可在 Go 和 C/C++ 代码间相互调用。受 CGO_ENABLED 参数限制。
- package main
- /*
- #include <stdio.h>
- #include <stdlib.h>
- void hello() {
- printf("Hello, World!\n");
- }
- */
- import "C"
- func main() {
- C.hello()
- }
调试 cgo 代码是件很麻烦的事,建议单独保存到 .c 文件中。这样可以将其当做独立的 C 程序进行调试。
test.h
- #ifndef __TEST_H__
- #define __TEST_H__
- void hello();
- #endif
test.c
- #include <stdio.h>
- #include "test.h"
- void hello() {
- printf("Hello, World!\n");
- }
- #ifdef __TEST__ // 避免和 Go bootstrap main 冲突。
- int main(int argc, char *argv[]) {
- hello();
- return 0;
- }
- #endif
main.go
- package main
- /*
- #include "test.h"
- */
- import "C"
- func main() {
- C.hello()
- }
编译和调试 C,只需在命令行提供宏定义即可。
- $ gcc -g -D__TEST__ -o test test.c
由于 cgo 仅扫描当前目录,如果需要包含其他 C 项目,可在当前目录新建一个 C 文件,然后用 #include 指令将所需的 .h、.c 都包含进来,记得在 CFLAGS 中使用 "-I" 参数指定原路径。某些时候,可能还需指定 "-std" 参数。
9.3.1 Flags
可使用 #cgo 命令定义 CFLAGS、LDFLAGS 等参数,自动合并多个设置。
- /*
- #cgo CFLAGS: -g
- #cgo CFLAGS: -I./lib -D__VER__=1
- #cgo LDFLAGS: -lpthread
- #include "test.h"
- */
- import "C"
可设置 GOOS、GOARCH 编译条件,空格表示 OR,逗号 AND,感叹号 NOT。
- #cgo windows,386 CFLAGS: -I./lib -D__VER__=1
9.3.2 DataType
数据类型对应关系。
可将 cgo 类型转换为标准 Go 类型。
- /*
- int add(int x, int y) {
- return x + y;
- }
- */
- import "C"
- func main() {
- var x C.int = C.add(1, 2)
- var y int = int(x)
- fmt.Println(x, y)
- }
9.3.3 String
字符串转换函数。
- /*
- #include <stdio.h>
- #include <stdlib.h>
- void test(char *s) {
- printf("%s\n", s);
- }
- char* cstr() {
- return "abcde";
- }
- */
- import "C"
- func main() {
- s := "Hello, World!"
- cs := C.CString(s) // 该函数在 C heap 分配内存,需要调用 free 释放。
- defer C.free(unsafe.Pointer(cs)) // #include <stdlib.h>
- C.test(cs)
- cs = C.cstr()
- fmt.Println(C.GoString(cs))
- fmt.Println(C.GoStringN(cs, 2))
- fmt.Println(C.GoBytes(unsafe.Pointer(cs), 2))
- }
输出:
- Hello, World!
- abcde
- ab
- [97 98]
用 C.malloc/free 分配 C heap 内存。
- /*
- #include <stdlib.h>
- */
- import "C"
- func main() {
- m := unsafe.Pointer(C.malloc(4 * 8))
- defer C.free(m) // 注释释放内存。
- p := (*[4]int)(m) // 转换为数组指针。
- for i := 0; i < 4; i++ {
- p[i] = i + 100
- }
- fmt.Println(p)
- }
输出:
- &[100 101 102 103]
9.3.4 Struct/Enum/Union
对 struct、enum 支持良好,union 会被转换成字节数组。如果没使用 typedef 定义,那么必须添加 struct、enum、union_ 前缀。
struct
- /*
- #include <stdlib.h>
- struct Data {
- int x;
- };
- typedef struct {
- int x;
- } DataType;
- struct Data* testData() {
- return malloc(sizeof(struct Data));
- }
- DataType* testDataType() {
- return malloc(sizeof(DataType));
- }
- */
- import "C"
- func main() {
- var d *C.struct_Data = C.testData()
- defer C.free(unsafe.Pointer(d))
- var dt *C.DataType = C.testDataType()
- defer C.free(unsafe.Pointer(dt))
- d.x = 100
- dt.x = 200
- fmt.Printf("%#v\n", d)
- fmt.Printf("%#v\n", dt)
- }
输出:
- &main._Ctype_struct_Data{x:100}
- &main._Ctype_DataType{x:200}
enum
- /*
- enum Color { BLACK = 10, RED, BLUE };
- typedef enum { INSERT = 3, DELETE } Mode;
- */
- import "C"
- func main() {
- var c C.enum_Color = C.RED
- var x uint32 = c
- fmt.Println(c, x)
- var m C.Mode = C.INSERT
- fmt.Println(m)
- }
union
- /*
- #include <stdlib.h>
- union Data {
- char x;
- int y;
- };
- union Data* test() {
- union Data* p = malloc(sizeof(union Data));
- p->x = 100;
- return p;
- }
- */
- import "C"
- func main() {
- var d *C.union_Data = C.test()
- defer C.free(unsafe.Pointer(d))
- fmt.Println(d)
- }
输出:
- &[100 0 0 0]
9.3.5 Export
导出 Go 函数给 C 调用,须使用 "//export" 标记。建议在独立头文件中声明函数原型,避免 "duplicate symbol" 错误。
main.go
- package main
- import "fmt"
- /*
- #include "test.h"
- */
- import "C"
- //export hello
- func hello() {
- fmt.Println("Hello, World!\n")
- }
- func main() {
- C.test()
- }
test.h
- #ifndef __TEST_H__
- #define __TEST_H__
- extern void hello();
- void test();
- #endif
test.c
- #include <stdio.h>
- #include "test.h"
- void test() {
- hello();
- }
9.3.6 Shared Library
在 cgo 中使用 C 共享库。
test.h
- #ifndef __TEST_HEAD__
- #define __TEST_HEAD__
- int sum(int x, int y);
- #endif
test.c
- #include <stdio.h>
- #include <stdlib.h>
- #include "test.h"
- int sum(int x, int y)
- {
- return x + y + 100;
- }
编译成 .so 或 .dylib。
- $ gcc -c -fPIC -o test.o test.c
- $ gcc -dynamiclib -o libtest.dylib test.o
将共享库和头文件拷贝到 Go 项目目录。
main.go
- package main
- /*
- #cgo CFLAGS: -I.
- #cgo LDFLAGS: -L. -ltest
- #include "test.h"
- */
- import "C"
- func main() {
- println(C.sum(10, 20))
- }
输出:
- $ go build -o test && ./test
- 130
编译成功后可用 ldd 或 otool 查看动态库使用状态。 静态库使用方法类似。
9.4 Reflect
没有运行期类型对象,实例也没有附加字段用来表明身份。只有转换成接口时,才会在其 itab 内部存储与该类型有关的信息,Reflect 所有操作都依赖于此。
9.4.1 Type
以 struct 为例,可获取其全部成员字段信息,包括非导出和匿名字段。
- type User struct {
- Username string
- }
- type Admin struct {
- User
- title string
- }
- func main() {
- var u Admin
- t := reflect.TypeOf(u)
- for i, n := 0, t.NumField(); i < n; i++ {
- f := t.Field(i)
- fmt.Println(f.Name, f.Type)
- }
- }
输出:
- User main.User // 可进一步递归。
- title string
如果是指针,应该先使用 Elem 方法获取目标类型,指针本身是没有字段成员的。
- func main() {
- u := new(Admin)
- t := reflect.TypeOf(u)
- if t.Kind() == reflect.Ptr {
- t = t.Elem()
- }
- for i, n := 0, t.NumField(); i < n; i++ {
- f := t.Field(i)
- fmt.Println(f.Name, f.Type)
- }
- }
同样,value-interface 和 pointer-interface 也会导致方法集存在差异。
- type User struct {
- }
- type Admin struct {
- User
- }
- func (*User) ToString() {}
- func (Admin) test() {}
- func main() {
- var u Admin
- methods := func(t reflect.Type) {
- for i, n := 0, t.NumMethod(); i < n; i++ {
- m := t.Method(i)
- fmt.Println(m.Name)
- }
- }
- fmt.Println("--- value interface ---")
- methods(reflect.TypeOf(u))
- fmt.Println("--- pointer interface ---")
- methods(reflect.TypeOf(&u))
- }
输出:
- --- value interface ---
- test
- --- pointer interface ---
- ToString
- test
可直接用名称或序号访问字段,包括用多级序号访问嵌入字段成员。
- type User struct {
- Username string
- age int
- }
- type Admin struct {
- User
- title string
- }
- func main() {
- var u Admin
- t := reflect.TypeOf(u)
- f, _ := t.FieldByName("title")
- fmt.Println(f.Name)
- f, _ = t.FieldByName("User") // 访问嵌入字段。
- fmt.Println(f.Name)
- f, _ = t.FieldByName("Username") // 直接访问嵌入字段成员,会自动深度查找。
- fmt.Println(f.Name)
- f = t.FieldByIndex([]int{0, 1}) // Admin[0] -> User[1] -> age
- fmt.Println(f.Name)
- }
输出:
- title
- User
- Username
- age
字段标签可实现简单元数据编程,比如标记 ORM Model 属性。
- type User struct {
- Name string `field:"username" type:"nvarchar(20)"`
- Age int `field:"age" type:"tinyint"`
- }
- func main() {
- var u User
- t := reflect.TypeOf(u)
- f, _ := t.FieldByName("Name")
- fmt.Println(f.Tag)
- fmt.Println(f.Tag.Get("field"))
- fmt.Println(f.Tag.Get("type"))
- }
输出:
- field:"username" type:"nvarchar(20)"
- username
- nvarchar(20)
可从基本类型获取所对应复合类型。
- var (
- Int = reflect.TypeOf(0)
- String = reflect.TypeOf("")
- )
- func main() {
- c := reflect.ChanOf(reflect.SendDir, String)
- fmt.Println(c)
- m := reflect.MapOf(String, Int)
- fmt.Println(m)
- s := reflect.SliceOf(Int)
- fmt.Println(s)
- t := struct{ Name string }{}
- p := reflect.PtrTo(reflect.TypeOf(t))
- fmt.Println(p)
- }
输出:
- chan<- string
- map[string]int
- []int
- *struct { Name string }
与之对应,方法 Elem 可返回复合类型的基类型。
- func main() {
- t := reflect.TypeOf(make(chan int)).Elem()
- fmt.Println(t)
- }
方法 Implements 判断是否实现了某个具体接口,AssignableTo、ConvertibleTo 用于赋值和转换判断。
- type Data struct {
- }
- func (*Data) String() string {
- return ""
- }
- func main() {
- var d *Data
- t := reflect.TypeOf(d)
- // 没法直接获取接口类型,好在接口本身是个 struct,创建
- // 一个空指针对象,这样传递给 TypeOf 转换成 interface{}
- // 时就有类型信息了。。
- it := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
- // 为啥不是 t.Implements(fmt.Stringer),完全可以由编译器生成。
- fmt.Println(t.Implements(it))
- }
某些时候,获取对齐信息对于内存自动分析是很有用的。
- type Data struct {
- b byte
- x int32
- }
- func main() {
- var d Data
- t := reflect.TypeOf(d)
- fmt.Println(t.Size(), t.Align()) // sizeof,以及最宽字段的对齐模数。
- f, _ := t.FieldByName("b")
- fmt.Println(f.Type.FieldAlign()) // 字段对齐。
- }
输出:
- 8 4
- 1
9.4.2 Value
Value 和 Type 使用方法类似,包括使用 Elem 获取指针目标对象。
- type User struct {
- Username string
- age int
- }
- type Admin struct {
- User
- title string
- }
- func main() {
- u := &Admin{User{"Jack", 23}, "NT"}
- v := reflect.ValueOf(u).Elem()
- fmt.Println(v.FieldByName("title").String()) // 用转换方法获取字段值
- fmt.Println(v.FieldByName("age").Int()) // 直接访问嵌入字段成员
- fmt.Println(v.FieldByIndex([]int{0, 1}).Int()) // 用多级序号访问嵌入字段成员
- }
输出:
- NT
- 23
- 23
除具体的 Int、String 等转换方法,还可返回 interface{}。只是非导出字段无法使用,需用 CanInterface 判断一下。
- type User struct {
- Username string
- age int
- }
- func main() {
- u := User{"Jack", 23}
- v := reflect.ValueOf(u)
- fmt.Println(v.FieldByName("Username").Interface())
- fmt.Println(v.FieldByName("age").Interface())
- }
输出:
- Jack
- panic: reflect.Value.Interface: cannot return value obtained from unexported field or
- method
当然,转换成具体类型不会引发 panic。
- func main() {
- u := User{"Jack", 23}
- v := reflect.ValueOf(u)
- f := v.FieldByName("age")
- if f.CanInterface() {
- fmt.Println(f.Interface())
- } else {
- fmt.Println(f.Int())
- }
- }
除 struct,其他复合类型 array、slice、map 取值示例。
- func main() {
- v := reflect.ValueOf([]int{1, 2, 3})
- for i, n := 0, v.Len(); i < n; i++ {
- fmt.Println(v.Index(i).Int())
- }
- fmt.Println("---------------------------")
- v = reflect.ValueOf(map[string]int{"a": 1, "b": 2})
- for _, k := range v.MapKeys() {
- fmt.Println(k.String(), v.MapIndex(k).Int())
- }
- }
输出:
- 1
- 2
- 3
- ---------------------------
- a 1
- b 2
需要注意,Value 某些方法没有遵循 "comma ok" 模式,而是返回 ZeroValue,因此需要用 IsValid 判断一下是否可用。
- func (v Value) FieldByName(name string) Value {
- v.mustBe(Struct)
- if f, ok := v.typ.FieldByName(name); ok {
- return v.FieldByIndex(f.Index)
- }
- return Value{}
- }
- type User struct {
- Username string
- age int
- }
- func main() {
- u := User{}
- v := reflect.ValueOf(u)
- f := v.FieldByName("a")
- fmt.Println(f.Kind(), f.IsValid())
- }
输出:
- invalid false
另外,接口是否为 nil,需要 tab 和 data 都为空。可使用 IsNil 方法判断 data 值。
- func main() {
- var p *int
- var x interface{} = p
- fmt.Println(x == nil)
- v := reflect.ValueOf(p)
- fmt.Println(v.Kind(), v.IsNil())
- }
输出:
- false
- ptr true
将对象转换为接口,会发生复制行为。该复制品只读,无法被修改。所以要通过接口改变目标对象状态,必须是 pointer-interface。
就算是指针,我们依然没法将这个存储在 data 的指针指向其他对象,只能透过它修改目标对象。因为目标对象并没有被复制,被复制的只是指针。
- type User struct {
- Username string
- age int
- }
- func main() {
- u := User{"Jack", 23}
- v := reflect.ValueOf(u)
- p := reflect.ValueOf(&u)
- fmt.Println(v.CanSet(), v.FieldByName("Username").CanSet())
- fmt.Println(p.CanSet(), p.Elem().FieldByName("Username").CanSet())
- }
输出:
- false false
- false true
非导出字段无法直接修改,可改用指针操作。
- type User struct {
- Username string
- age int
- }
- func main() {
- u := User{"Jack", 23}
- p := reflect.ValueOf(&u).Elem()
- p.FieldByName("Username").SetString("Tom")
- f := p.FieldByName("age")
- fmt.Println(f.CanSet())
- // 判断是否能获取地址。
- if f.CanAddr() {
- age := (*int)(unsafe.Pointer(f.UnsafeAddr()))
- // age := (*int)(unsafe.Pointer(f.Addr().Pointer())) // 等同
- *age = 88
- }
- // 注意 p 是 Value 类型,需要还原成接口才能转型。
- fmt.Println(u, p.Interface().(User))
- }
输出:
- false
- {Tom 88} {Tom 88}
复合类型修改示例。
- func main() {
- s := make([]int, 0, 10)
- v := reflect.ValueOf(&s).Elem()
- v.SetLen(2)
- v.Index(0).SetInt(100)
- v.Index(1).SetInt(200)
- fmt.Println(v.Interface(), s)
- v2 := reflect.Append(v, reflect.ValueOf(300))
- v2 = reflect.AppendSlice(v2, reflect.ValueOf([]int{400, 500}))
- fmt.Println(v2.Interface())
- fmt.Println("----------------------")
- m := map[string]int{"a": 1}
- v = reflect.ValueOf(&m).Elem()
- v.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(100)) // update
- v.SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf(200)) // add
- fmt.Println(v.Interface(), m)
- }
输出:
- [100 200] [100 200]
- [100 200 300 400 500]
- ----------------------
- map[a:100 b:200] map[a:100 b:200]
9.4.3 Method
可获取方法参数、返回值类型等信息。
- type Data struct {
- }
- func (*Data) Test(x, y int) (int, int) {
- return x + 100, y + 100
- }
- func (*Data) Sum(s string, x ...int) string {
- c := 0
- for _, n := range x {
- c += n
- }
- return fmt.Sprintf(s, c)
- }
- func info(m reflect.Method) {
- t := m.Type
- fmt.Println(m.Name)
- for i, n := 0, t.NumIn(); i < n; i++ {
- fmt.Printf(" in[%d] %v\n", i, t.In(i))
- }
- for i, n := 0, t.NumOut(); i < n; i++ {
- fmt.Printf(" out[%d] %v\n", i, t.Out(i))
- }
- }
- func main() {
- d := new(Data)
- t := reflect.TypeOf(d)
- test, _ := t.MethodByName("Test")
- info(test)
- sum, _ := t.MethodByName("Sum")
- info(sum)
- }
输出:
- Test
- in[0] *main.Data // receiver
- in[1] int
- in[2] int
- out[0] int
- out[1] int
- Sum
- in[0] *main.Data
- in[1] string
- in[2] []int
- out[0] string
动态调用方法很简单,按 In 列表准备好所需参数即可 (不包括 receiver)。
- func main() {
- d := new(Data)
- v := reflect.ValueOf(d)
- exec := func(name string, in []reflect.Value) {
- m := v.MethodByName(name)
- out := m.Call(in)
- for _, v := range out {
- fmt.Println(v.Interface())
- }
- }
- exec("Test", []reflect.Value{
- reflect.ValueOf(1),
- reflect.ValueOf(2),
- })
- fmt.Println("-----------------------")
- exec("Sum", []reflect.Value{
- reflect.ValueOf("result = %d"),
- reflect.ValueOf(1),
- reflect.ValueOf(2),
- })
- }
输出:
- 101
- 102
- -----------------------
- result = 3
如改用 CallSlice,只需将变参打包成 slice 即可。
- func main() {
- d := new(Data)
- v := reflect.ValueOf(d)
- m := v.MethodByName("Sum")
- in := []reflect.Value{
- reflect.ValueOf("result = %d"),
- reflect.ValueOf([]int{1, 2}), // 将变参打包成 slice。
- }
- out := m.CallSlice(in)
- for _, v := range out {
- fmt.Println(v.Interface())
- }
- }
非导出方法无法调用,甚至无法透过指针操作,因为接口类型信息中没有该方法地址。
9.4.4 Make
利用 Make、New 等函数,可实现近似泛型操作。
- var (
- Int = reflect.TypeOf(0)
- String = reflect.TypeOf("")
- )
- func Make(T reflect.Type, fptr interface{}) {
- // 实际创建 slice 的包装函数。
- swap := func(in []reflect.Value) []reflect.Value {
- // --- 省略算法内容 --- //
- // 返回和类型匹配的 slice 对象。
- return []reflect.Value{
- reflect.MakeSlice(
- reflect.SliceOf(T), // slice type
- int(in[0].Int()), // len
- int(in[1].Int()) // cap
- ),
- }
- }
- // 传入的是函数变量指针,因为我们要将变量指向 swap 函数。
- fn := reflect.ValueOf(fptr).Elem()
- // 获取函数指针类型,生成所需 swap function value。
- v := reflect.MakeFunc(fn.Type(), swap)
- // 修改函数指针实际指向,也就是 swap。
- fn.Set(v)
- }
- func main() {
- var makeints func(int, int) []int
- var makestrings func(int, int) []string
- // 用相同算法,生成不同类型创建函数。
- Make(Int, &makeints)
- Make(String, &makestrings)
- // 按实际类型使用。
- x := makeints(5, 10)
- fmt.Printf("%#v\n", x)
- s := makestrings(3, 10)
- fmt.Printf("%#v\n", s)
- }
输出:
- []int{0, 0, 0, 0, 0}
- []string{"", "", ""}
原理并不复杂。
- 核心是提供一个 swap 函数,其中利用 reflect.MakeSlice 生成最终 slice 对象, 因此需要传入 element type、len、cap 参数。
- 接下来,利用 MakeFunc 函数生成 swap value,并修改函数变量指向,以达到调 用 swap 的目的。
- 当调用具体类型的函数变量时,实际内部调用的是 swap,相关代码会自动转换参 数列表,并将返回结果还原成具体类型返回值。
如此,在共享算法的前提下,无须用 interface{},无须做类型转换,颇有泛型的效果。
Go学习笔记01-语言的更多相关文章
- Redis:学习笔记-01
Redis:学习笔记-01 该部分内容,参考了 bilibili 上讲解 Redis 中,观看数最多的课程 Redis最新超详细版教程通俗易懂,来自 UP主 遇见狂神说 1. Redis入门 2.1 ...
- 软件测试之loadrunner学习笔记-01事务
loadrunner学习笔记-01事务<转载至网络> 事务又称为Transaction,事务是一个点为了衡量某个action的性能,需要在开始和结束位置插入一个范围,定义这样一个事务. 作 ...
- [java学习笔记]java语言核心----面向对象之this关键字
一.this关键字 体现:当成员变量和函数的局部变量重名时,可以使用this关键字来区别:在构造函数中调用其它构造函数 原理: 代表的是当前对象. this就是所在函数 ...
- [java学习笔记]java语言核心----面向对象之构造函数
1.构造函数概念 特点: 函数名与类名相同 不用定义返回值类型 没有具体的返回值 作用: 给对象进行初始化 注意: 默认构造函数 多个构造函数是以重载出现的 一个类中如果 ...
- Java学习笔记:语言基础
Java学习笔记:语言基础 2014-1-31 最近开始学习Java,目的倒不在于想深入的掌握Java开发,而是想了解Java的基本语法,可以阅读Java源代码,从而拓展一些知识面.同时为学习An ...
- IOS学习笔记07---C语言函数-printf函数
IOS学习笔记07---C语言函数-printf函数 0 7.C语言5-printf函数 ------------------------- ----------------------------- ...
- IOS学习笔记06---C语言函数
IOS学习笔记06---C语言函数 -------------------------------------------- qq交流群:创梦技术交流群:251572072 ...
- C++ GUI Qt4学习笔记01
C++ GUI Qt4学习笔记01 qtc++signalmakefile文档平台 这一章介绍了如何把基本的C++只是与Qt所提供的功能组合起来创建一些简单的图形用户界面应用程序. 引入两个重要概 ...
- SaToken学习笔记-01
SaToken学习笔记-01 SaToken版本为1.18 如果有排版方面的错误,请查看:传送门 springboot集成 根据官网步骤maven导入依赖 <dependency> < ...
- PHP 学习笔记 01
例子: 为什么要学PHP 主观原因: 前段时间在学校处理了毕业的一些事情,回到上海后开始了找工作的旅程.意向工作是WPF开发或者ASP.NET 作为后端的WEB开发. 陆陆续续一直在面试,其中有一家公 ...
随机推荐
- QQ邮箱开启SMTP服务的步骤
首先要确保你的QQ邮箱已经要开启超过一个月.对于新开启的邮箱,腾讯是不开放这些功能的. 方法/步骤 首先点QQ头像旁边的信封符号进入邮箱. 当然你也可以使用 mail.qq.com进邮箱 进入邮箱后点 ...
- Shell中$X的含义
$0 表示这个程序的执行名字,包含输入参数$n 表示这个程序的第n个参数值$* 表示这个程序的所有参数,此选项参数可超过9个.$# 表示这个程序的参数个数$$ 表示这个程序的PID(脚本运行的当 ...
- 将自己的代码托管到github上
这几天一直在做一个爬虫的小demo,代码基本写的差不多了,想着如何把他放在一个地方,如是乎注册了一个github账号,开始了自己的git之旅. 首先是下载git,这个我就不多说啦!到处都有推荐看看廖雪 ...
- SuperSocket基础一
SuperSocket基础(一)——————基本概念 项目中之前一直使用TCP socket服务框架,但是不利于扩展.最近刚接触到开源的superSocket感觉很不错,特记录一下.官方开源地址:ht ...
- set&enum小结(database)
今天发现mysql中有set这种数据类型,工作的业务中也使用到了.网上查阅资料后,小结一下 先总结一下两者的分别 set和enum类似表单中的多选和单选,set和enum在数据库内部是用整数表示的,显 ...
- -bash: warning: setlocale: LC_CTYPE: cannot change locale (EN_US.UTF-8): No such file or directory
出问题原因: 通过 su - 切换用户时候,LANG设置为en或者设置错误,切换角色会出现这个错误 如何解决? echo $LANG # 查看 LANG 参数 sed 's#LANG=en#LANG= ...
- MyEclipse中Lombok的安装及使用
lombok是一款通过注解的形式简化我们必须有又显得臃肿的代码的工具.最常用的就是@Data注解.实体类上用了这个注解,实体类的各个属性就不需要书写get和set方法. 安装步骤: 1.关闭Myecl ...
- PHP 运行 php-fpm 报错
报错如下: [27-Aug-2017 18:34:23] WARNING: Nothing matches the include pattern '/usr/local/php/etc/php- ...
- 异常org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 11; 注释中不允许出现字符串 "--"。的原因
是由于编码格式不统一导致的. 把ecplise的workpace的编码改成utf-8
- jenkins插件安装与升级[三]
标签(linux): jenkins 笔者Q:972581034 交流群:605799367.有任何疑问可与笔者或加群交流 默认的插件 Folders Plugin OWASP Markup Form ...