Go 學習筆記第一部分 語言

2018-09-28 18:50 更新

第一部分 語言

1.1 變量

Go 是靜態(tài)類型語言,不能在運行期改變變量類型。使用關鍵字 var 定義變量,自動初始化為零值。如果提供初始化值,可省略變量類型,由編譯器自動推斷。

var x int
var f float32 = 1.6
var s = "abc"

在函數(shù)內(nèi)部,可用更簡略的 ":=" 方式定義變量。

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" 規(guī)避)
}

注意重新賦值與定義新同名變量的區(qū)別。

s := "abc"
println(&s)

s, y := "hello", 20 // 重新賦值: 與前 s 在同一層次的代碼塊中,且有新的變量被定義。
println(&s, y) // 通常函數(shù)多返回值 err 會被重復使用。

{
    s, z := 1000, 30 // 定義新同名變量: 不在同一層次代碼塊。
    println(&s, z)
}

輸出:

0x2210230f30
0x2210230f30 20
0x2210230f18 30

1.2 常量

常量值必須是編譯期可確定的數(shù)字、字符串、布爾值。

const x, y int = 1, 2 // 多常量初始化
const s = "Hello, World!" // 類型推斷

const ( // 常量組
    a, b = 10, 100
    c bool = false
)

func main() {
    const x = "xxx" // 未使用局部常量不會引發(fā)編譯錯誤。
}

不支持 1UL、2LL 這樣的類型后綴。

在常量組中,如不提供類型和初始化值,那么視作與上一常量相同。

const (
    s = "abc"
    x // x = "abc"
)

常量值還可以是 len、cap、unsafe.Sizeof 等編譯期可確定結果的函數(shù)返回值。

const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(b)
)

如果常量類型足以存儲初始化值,那么不會引發(fā)溢出錯誤。

const (
    a byte = 100 // int to byte
    b int = 1e20 // float64 to int, overflows
)

枚舉

關鍵字 iota 定義常量組中從0開始按行計數(shù)的自增枚舉值。

const (
    Sunday = iota // 0
    Monday // 1,通常省略后續(xù)行表達式。
    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,顯式恢復。注意計數(shù)包含了 C、D 兩行。
    F // 5
)

可通過自定義類型來實現(xiàn)枚舉類型限制。

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 基本類型

更明確的數(shù)字類型命名,支持 Unicode,支持常用數(shù)據(jù)結構。

支持八進制、十六進制,以及科學記數(shù)法。標準庫 math 定義了各數(shù)字類型取值范圍。

a, b, c, d := 071, 0x1F, 1e9, math.MinInt16

空指針值 nil,而非 C/C++ NULL。

1.4 引用類型

引用類型包括 slice、map 和 channel。它們有復雜的內(nèi)部結構,除了申請內(nèi)存外,還需要初始化相關屬性。

內(nèi)置函數(shù) new 計算類型大小,為其分配零值內(nèi)存,返回指針。而 make 會被編譯器翻譯成具體的創(chuàng)建函數(shù),由其分配內(nèi)存和初始化成員結構,返回對象而非指針。

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)

有關引用類型具體的內(nèi)存布局,可參考后續(xù)章節(jié)。

1.5 類型轉換

不支持隱式類型轉換,即便是從窄向?qū)掁D換也不行。

var b byte = 100
// var n int = b // Error: cannot use b (type byte) as type int in assignment
var n int = int(b) // 顯式轉換

使用括號避免優(yōu)先級錯誤。

*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 字符串

字符串是不可變值類型,內(nèi)部用指針指向 UTF-8 字節(jié)數(shù)組。

  • 默認值是空字符串 ""。
  • 用索引號訪問某字節(jié),如 s[i]。
  • 不能用序號獲取字節(jié)元素指針,&s[i] 非法。
  • 不可變類型,無法修改字節(jié)數(shù)組。
  • 字節(jié)數(shù)組尾部不包含 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

支持用兩個索引號返回子串。子串依然指向原字節(jié)數(shù)組,僅修改了指針和長度屬性。

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。無論哪種轉換,都會重新分配內(nèi)存,并復制字節(jié)數(shù)組。

func main() {
    s := "abcd"
    bs := []byte(s)

    bs[1] = 'B'
    println(string(bs))

    u := "電腦"
    us := []rune(u)

    us[1] = '話'
    println(string(us))
}

輸出:

aBcd
電話

用 for 循環(huán)遍歷字符串時,也有 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

返回局部變量指針是安全的,編譯器會根據(jù)需要將其分配在 GC Heap 上。

func test() *int {
    x := 100
    return &x // 在堆上分配 x 內(nèi)存。但在內(nèi)聯(lián)時,也可能直接分配在目標棧。
}

將 Pointer 轉換成 uintptr,可變相實現(xiàn)指針運算。

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 當成普通整數(shù)對象,它無法阻止 "關聯(lián)" 對象被回收。

1.8 自定義類型

可將類型分為命名和未命名兩大類。命名類型包括 bool、int、string 等,而 array、slice、map 等和具體元素類型、長度等有關,屬于未命名類型。

具有相同聲明的未命名類型被視為同一類型。

  • 具有相同基類型的指針。
  • 具有相同元素類型和長度的 array。
  • 具有相同元素類型的 slice。
  • 具有相同鍵值類型的 map。
  • 具有相同元素類型和傳送方向的 channel。
  • 具有相同字段序列 (字段名、類型、標簽、順序) 的匿名 struct。
  • 簽名相同 (參數(shù)和返回值,不包括參數(shù)名稱) 的 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 在全局或函數(shù)內(nèi)定義新類型。

func main() {
    type bigint int64

    var x bigint = 100
    println(x)
}

新類型不是原類型的別名,除擁有相同數(shù)據(jù)存儲結構外,它們之間沒有任何關系,不會持有原類型任何信息。除非目標類型是未命名類型,否則必須顯式轉換。

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

支持三種循環(huán)方式,包括類 while 語法。

s := "abc"

for i, n := 0, len(s); i < n; i++ { // 常見的 for 循環(huán),支持初始化語句。
    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++ { // 避免多次調(diào)用 length 函數(shù)。
        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 { // 在修改前,我們先修改原數(shù)組。
        a[1], a[2] = 999, 999
        fmt.Println(a) // 確認修改有效,輸出 [0, 999, 999]。
    }

    a[i] = v + 100 // 使用復制品中取出的 value 修改原數(shù)組。
}

fmt.Println(a) // 輸出 [100, 101, 102]。

建議改用引用類型,其底層數(shù)據(jù)不會被復制。

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 // 對底層數(shù)據(jù)的修改。
    }

    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

如需要繼續(xù)下一分支,可使用 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

支持在函數(shù)內(nèi) goto 跳轉。標簽名區(qū)分大小寫,未使用標簽引發(fā)錯誤。

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 可在多級嵌套循環(huán)中跳出。

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 循環(huán)。

x := 100

switch {
case x >= 0:
    if x == 0 { break }
    println(x)
}

第3章 函數(shù)

3.1 函數(shù)定義

不支持 嵌套 (nested)、重載 (overload) 和 默認參數(shù) (default parameter)。

  • 無需聲明原型。
  • 支持不定長變參。
  • 支持多返回值。
  • 支持命名返回參數(shù)。
  • 支持匿名函數(shù)和閉包。

使用關鍵字 func 定義函數(shù),左大括號依舊不能另起一行。

func test(x, y int, s string) (int, string) { // 類型相同的相鄰參數(shù)可合并。
    n := x + y // 多返回值必須用括號。
    return n, fmt.Sprintf(s, n)
}

函數(shù)是第一類對象,可作為參數(shù)傳遞。建議將復雜簽名定義為函數(shù)類型,以便于閱讀。

func test(fn func() int) int {
    return fn()
}

type FormatFunc func(s string, x, y int) string // 定義函數(shù)類型。

func format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
}

func main() {
    s1 := test(func() int { return 100 }) // 直接將匿名函數(shù)當參數(shù)。

    s2 := format(func(s string, x, y int) string {
        return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)

    println(s1, s2)
}

有返回值的函數(shù),必須有明確的終止語句,否則會引發(fā)編譯錯誤。

3.2 變參

變參本質(zhì)上就是 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)
}

多返回值可直接作為其他函數(shù)調(diào)用實參。

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()))
}

命名返回參數(shù)可看做與形參類似的局部變量,最后由 return 隱式返回。

func add(x, y int) (z int) {
    z = x + y
    return
}

func main() {
    println(add(1, 2))
}

命名返回參數(shù)可被同名局部變量遮蔽,此時需要顯式返回。

func add(x, y int) (z int) {
    { // 不能在一個級別,引發(fā) "z redeclared in this block" 錯誤。
        var z = x + y
        // return // Error: z is shadowed during return
        return z // 必須顯式返回。
    }
}

命名返回參數(shù)允許 defer 延遲調(diào)用通過閉包讀取和修改。

func add(x, y int) (z int) {
    defer func() {
        z += 100
    }()

    z = x + y
    return
}

func main() {
    println(add(1, 2)) // 輸出: 103
}

顯式 return 返回前,會先修改命名返回參數(shù)。

func add(x, y int) (z int) {
    defer func() {
        println(z) // 輸出: 203
    }()

    z = x + y
    return z + 200 // 執(zhí)行順序: (z = z + 200) -> (call defer) -> (ret)
}

func main() {
    println(add(1, 2)) // 輸出: 203
}

3.4 匿名函數(shù)

匿名函數(shù)可賦值給變量,做為結構字段,或者在 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)())

閉包復制的是原對象指針,這就很容易解釋延遲引用現(xiàn)象。

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 對象,其中包含了匿名函數(shù)地址、閉包對象指針。當調(diào)用匿名函數(shù)時,只需以某個寄存器傳遞該對象即可。

FuncVal { func_address, closure_var_pointer ... }

3.5 延遲調(diào)用

關鍵字 defer 用于注冊延遲調(diào)用。這些調(diào)用直到 ret 前才被執(zhí)行,通常用于釋放資源或錯誤處理。

func test() error {
    f, err := os.Create("test.txt")
    if err != nil { return err }

    defer f.Close() // 注冊調(diào)用,而不是注冊函數(shù)。必須提供參數(shù),哪怕為空。

    f.WriteString("Hello, World!")
    return nil
}

多個 defer 注冊,按 FILO 次序執(zhí)行。哪怕函數(shù)或某個延遲調(diào)用發(fā)生錯誤,這些調(diào)用依舊會被執(zhí)行。

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

延遲調(diào)用參數(shù)在注冊時求值或復制,可用指針或閉包 "延遲" 讀取。

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 可能會導致性能問題,尤其是在一個 "大循環(huán)" 里。

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 參數(shù)類型為 interface{},因此可拋出任何類型對象。

func panic(v interface{})
func recover() interface{}

延遲調(diào)用中引發(fā)的錯誤,可被后續(xù)延遲調(diào)用捕獲,但僅最后一個錯誤可被捕獲。

func test() {
    defer func() {
        fmt.Println(recover())
    }()

    defer func() {
        panic("defer panic")
    }()

    panic("test panic")
}

func main() {
    test()
}

輸出:

defer panic

捕獲函數(shù) recover 只有在延遲調(diào)用內(nèi)直接調(diào)用才會終止錯誤,否則總是返回 nil。任何未捕獲的錯誤都會沿調(diào)用堆棧向外傳遞。

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

使用延遲匿名函數(shù)或下面這樣都是有效的。

func except() {
    recover()
}

func test() {
    defer except()
    panic("test panic")
}

如果需要保護代碼片段,可將代碼塊重構成匿名函數(shù),如此可確保后續(xù)代碼被執(zhí)行。

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 引發(fā)中斷性錯誤外,還可返回 error 類型錯誤對象來表示函數(shù)調(diào)用狀態(tài)。

type error interface {
    Error() string
}

標準庫 errors.New 和 fmt.Errorf 函數(shù)用于創(chuàng)建實現(xiàn) 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)
    }
}

如何區(qū)別使用 panic 和 error 兩種方式?慣例是:導致關鍵流程出現(xiàn)不可修復性錯誤的使用 panic,其他使用 error。

第4章 數(shù)據(jù)

4.1 Array

和以往認知的數(shù)組有很大不同。

  • 數(shù)組是值類型,賦值和傳參會復制整個數(shù)組,而不是指針。
  • 數(shù)組長度必須是常量,且是類型的組成部分。[2]int 和 [3]int 是不同類型。
  • 支持 "=="、"!=" 操作符,因為內(nèi)存總是被初始化過的。
  • 指針數(shù)組 [n]T,數(shù)組指針 [n]T。

可用復合語句初始化。

a := [3]int{1, 2} // 未初始化元素值為0。
b := [...]int{1, 2, 3, 4} // 通過初始化值確定數(shù)組長度。
c := [5]int{2: 100, 4:200} // 使用索引號初始化元素。

d := [...]struct {
    name string
    age uint8
}{
    {"user1", 10}, // 可省略元素類型。
    {"user2", 20}, // 別忘了最后一行的逗號。
}

支持多維數(shù)組。

a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 緯度不能用 "..."。

值拷貝行為會造成性能問題,通常會建議使用 slice,或數(shù)組指針。

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]

內(nèi)置函數(shù) len 和 cap 都返回數(shù)組長度 (元素數(shù)量)。

a := [2]int{}
println(len(a), cap(a)) // 2, 2

4.2 Slice

需要說明,slice 并不是數(shù)組或數(shù)組指針。它通過內(nèi)部指針和相關屬性引用數(shù)組片段,以實現(xiàn)變長方案。

runtime.h

struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
  • 引用類型。但自身是結構體,值拷貝傳遞。
  • 屬性 len 表示可用元素數(shù)量,讀寫操作不能超過該限制。
  • 屬性 cap 表示最大擴張容量,不能超出數(shù)組限制。
  • 如果 slice == nil,那么 len、cap 結果都等于 0。
data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]

創(chuàng)建表達式使用的是元素索引號,而非數(shù)量。

讀寫操作實際目標是底層數(shù)組,只需注意索引號的差別。

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]

可直接創(chuàng)建 slice 對象,自動分配底層數(shù)組。

s1 := []int{0, 1, 2, 3, 8: 100} // 通過初始化表達式構造,可使用索引號。
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8) // 使用 make 創(chuàng)建,指定 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 動態(tài)創(chuàng)建 slice,避免了數(shù)組必須用常量做長度的麻煩。還可用指針直接訪問底層數(shù)組,退化成普通數(shù)組操作。

s := []int{0, 1, 2, 3}
p := &s[2] // *int, 獲取底層數(shù)組元素指針。
*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 創(chuàng)建新 slice 對象,以便在 cap 允許范圍內(nèi)調(diào)整屬性。

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

新對象依舊指向原底層數(shù)組。

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 尾部添加數(shù)據(jù),返回新的 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] 寫數(shù)據(jù)。

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 限制,就會重新分配底層數(shù)組,即便原數(shù)組并未填滿。

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) // 重新分配底層數(shù)組,與原數(shù)組無關。
fmt.Println(&s[0], &data[0]) // 比對底層數(shù)組起始指針。

輸出:

[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0x20819c180 0x20817c0c0

從輸出結果可以看出,append 后的 s 重新分配了底層數(shù)組,并復制數(shù)據(jù)。如果只追加一個值,則不會超過 s.cap 限制,也就不會重新分配。

通常以2倍容量重新分配底層數(shù)組。在大批量添加數(shù)據(jù)時,建議一次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復制開銷?;虺跏蓟銐蜷L的 len 屬性,改用索引號進行操作。及時釋放不再使用的 slice 對象,避免持有過期數(shù)組,造成 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

函數(shù) copy 在兩個 slice 間復制數(shù)據(jù),復制長度以 len 小的為準。兩個 slice 可指向同一底層數(shù)組,允許元素區(qū)間重疊。

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]

應及時將所需數(shù)據(jù) copy 到較小的 slice,以便釋放超大號底層數(shù)組內(nèi)存。

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 函數(shù)一個合理元素數(shù)量參數(shù),有助于提升性能。因為事先申請一大塊內(nèi)存,可避免后續(xù)操作時頻繁擴張。

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)) // 獲取鍵值對數(shù)量。cap 無效。

for k, v := range m { // 迭代,可僅返回 key。隨機順序返回,每次都不相同。
    println(k, v)
}

不能保證迭代返回次序,通常是隨機結果,具體和版本實現(xiàn)有關。

從 map 中取回的是一個 value 臨時復制品,對其成員的修改是沒有任何意義的。

type user struct{ name string }

m := map[int]user{ // 當 map 因擴張而重新哈希時,各鍵值項存儲位置都會發(fā)生改變。 因此,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

值類型,賦值和傳參會復制全部內(nèi)容??捎?"_" 定義補位字段,支持指向自身類型的指針成員。

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

空結構 "節(jié)省" 內(nèi)存,比如用來實現(xiàn) set 數(shù)據(jù)結構,或者實現(xiàn)沒有 "狀態(tài)" 只有方法的 "靜態(tài)類"。

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",
}

可以像普通字段那樣訪問匿名字段成員,編譯器從外向內(nèi)逐級查找所有層次的匿名字段,直到發(fā)現(xiàn)目標或出錯。

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 面向?qū)ο?/h3>

面向?qū)ο笕筇卣骼?,Go 僅支持封裝,盡管匿名字段的內(nèi)存布局和行為類似繼承。沒有 class 關鍵字,沒有繼承、多態(tài)等等。

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
                 // 沒有繼承,自然也不會有多態(tài)。
var u User = m.User // 同類型拷貝。

內(nèi)存布局和 C struct 相同,沒有任何附加的 object 信息。

可用 unsafe 包相關函數(shù)輸出內(nèi)存地址信息。

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)。

  • 只能為當前包內(nèi)命名類型定義方法。
  • 參數(shù) receiver 可任意命名。如方法中未曾使用,可省略參數(shù)名。
  • 參數(shù) receiver 類型可以是 T 或 *T?;愋?T 不能是接口或指針。
  • 不支持方法重載,receiver 只是參數(shù)簽名的組成部分。
  • 可用實例 value 或 pointer 調(diào)用全部方法,編譯器自動轉換。

沒有構造和析構方法,通常用簡單工廠模式返回對象實例。

type Queue struct {
    elements []interface{}
}

func NewQueue() *Queue { // 創(chuàng)建對象實例。
    return &Queue{make([]interface{}, 10)}
}

func (*Queue) Push(e interface{}) error { // 省略 receiver 參數(shù)名。
    panic("not implemented")
}

// func (Queue) Push(e int) error { // Error: method redeclared: Queue.Push
// panic("not implemented")
// }

func (self *Queue) length() int { // receiver 參數(shù)名可以是 self、this 或其他。
    return len(self.elements)
}

方法不過是一種特殊的函數(shù),只需將其還原,就知道 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}

通過匿名字段,可獲得和繼承類似的復用能力。依據(jù)編譯器查找次序,只需在外層定義同名方法,就可以實現(xiàn) "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 方法集

每個類型都有與之關聯(lián)的方法集,這會影響到接口實現(xiàn)規(guī)則。

  • 類型 T 方法集包含全部 receiver T 方法。
  • 類型 T 方法集包含全部 receiver T + T 方法。
  • 如類型 S 包含匿名字段 T,則 S 方法集包含 T 方法。
  • 如類型 S 包含匿名字段 T,則 S 方法集包含 T + T 方法。
  • 不管嵌入 T 或 T,S 方法集總是包含 T + *T 方法。

用實例 value 和 pointer 調(diào)用方法 (含匿名字段) 不受方法集約束,編譯器總是查找全部方法,并自動轉換 receiver 實參。

5.4 表達式

根據(jù)調(diào)用者不同,方法分為兩種表現(xiàn)形式:

instance.method(args...) ---> <type>.func(instance, args...)

前者稱為 method value,后者 method expression。

兩者都可像普通函數(shù)那樣賦值和傳參,區(qū)別在于 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,因為不是指針類型,不受后續(xù)修改影響。

    u.id, u.name = 2, "Jack"
    u.Test()

    mValue()
}

輸出:

{2 Jack}
{1 Tom}

在匯編層面,method value 和閉包的實現(xiàn)方式相同,實際返回 FuncVal 類型對象。

FuncVal { method_address, receiver_copy }

可依據(jù)方法集轉換 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)                  // 簽名變?yōu)?func TestValue(self *User)。
}                            // 實際依然是 receiver value copy。

輸出:

User : 0x210231000, {1 Tom}
TestValue : 0x210231060, {1 Tom}
TestPointer: 0x210231000, &{1 Tom}
TestValue : 0x2102310c0, {1 Tom}

將方法 "還原" 成函數(shù),就容易理解下面的代碼了。

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 接口定義

接口是一個或多個方法簽名的集合,任何類型的方法集中只要擁有與之對應的全部方法,就表示它 "實現(xiàn)" 了該接口,無須在該類型上顯式添加接口聲明。

所謂對應方法,是指有相同名稱、參數(shù)列表 (不包括參數(shù)名) 以及返回值。當然,該類型還可以有其他方法。

  • 接口命名習慣以 er 結尾,結構體。
  • 接口只有方法簽名,沒有實現(xiàn)。
  • 接口沒有數(shù)據(jù)字段。
  • 可在接口中嵌入其他接口。
  • 類型可實現(xiàn)多個接口。
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{} 沒有任何方法簽名,也就意味著任何類型都實現(xiàn)了空接口。其作用類似面向?qū)ο笳Z言中的根對象 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 執(zhí)行機制

接口對象由接口表 (interface table) 指針和數(shù)據(jù)指針組成。

runtime.h

struct Iface
{
    Itab* tab;
    void* data;
};

struct Itab
{
    InterfaceType* inter;
    Type* type;
    void (*fun[])(void);
};

接口表存儲元數(shù)據(jù)信息,包括接口類型、動態(tài)類型,以及實現(xiàn)接口的方法指針。無論是反射還是通過接口調(diào)用方法,都會用到這些信息。

數(shù)據(jù)指針持有的是目標對象的只讀復制品,復制完整對象或指針。

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}

接口轉型返回臨時對象,只有使用指針才能修改其狀態(tài)。

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 接口技巧

讓編譯器檢查,以確保某個類型實現(xiàn)接口。

var _ fmt.Stringer = (*Data)(nil)

某些時候,讓函數(shù)直接 "實現(xiàn)" 接口能省不少事。

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 章 并發(fā)

7.1 Goroutine

Go 在語言層面對并發(fā)編程提供支持,一種類似協(xié)程,稱作 goroutine 的機制。

只需在函數(shù)調(diào)用語句前添加 go 關鍵字,就可創(chuàng)建并發(fā)執(zhí)行單元。開發(fā)人員無需了解任何執(zhí)行細節(jié),調(diào)度器會自動將其安排到合適的系統(tǒng)線程上執(zhí)行。goroutine 是一種非常輕量級的實現(xiàn),可在單個進程里執(zhí)行成千上萬的并發(fā)任務。

事實上,入口函數(shù) main 就以 goroutine 運行。另有與之配套的 channel 類型,用以實現(xiàn) "以通訊來共享內(nèi)存" 的 CSP 模式。相關實現(xiàn)細節(jié)可參考本書第二部分的源碼剖析。

go func() {
    println("Hello, World!")
}()

調(diào)度器不能保證多個 goroutine 執(zhí)行次序,且進程退出時不會等待它們結束。

默認情況下,進程啟動后僅允許一個系統(tǒng)線程服務于 goroutine??墒褂铆h(huán)境變量或標準庫函數(shù) runtime.GOMAXPROCS 修改,讓調(diào)度器用多個線程實現(xiàn)多核并行,而不僅僅是并發(fā)。

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 // 用戶態(tài)所使用 CPU 時間片 (多核累加)
sys 0.01 // 內(nèi)核態(tài)所使用 CPU 時間片

$ GOMAXPROCS=2 time -p ./test

0 9223372030412324865
1 9223372030412324865

real 4.18
user 7.61 // 雖然總時間差不多,但由 2 個核并行,real 時間自然少了許多。
sys 0.02

調(diào)用 runtime.Goexit 將立即終止當前 goroutine 執(zhí)行,調(diào)度器確保所有已注冊 defer 延遲調(diào)用被執(zhí)行。

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") // 不會執(zhí)行
        }()

        println("A") // 不會執(zhí)行
    }()

    wg.Wait()
}

輸出:

B.defer
A.defer

和協(xié)程 yield 作用類似,Gosched 讓出底層線程,將當前 goroutine 暫停,放回隊列等待下次被調(diào)度執(zhí)行。

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 模式的具體實現(xiàn),用于多個 goroutine 通訊。其內(nèi)部實現(xiàn)了同步,確保并發(fā)安全。

默認為同步模式,需要發(fā)送和接收配對。否則會被阻塞,直到另一方準備好后被喚醒。

func main() {
    data := make(chan int) // 數(shù)據(jù)交換隊列
    exit := make(chan bool) // 退出通知

    go func() {
        for d := range data { // 從隊列迭代接收數(shù)據(jù),直到 close 。
            fmt.Println(d)
        }

        fmt.Println("recv over.")
        exit <- true // 發(fā)出退出通知。
    }()

    data <- 1 // 發(fā)送數(shù)據(jù)。
    data <- 2
    data <- 3
    close(data) // 關閉隊列。

    fmt.Println("send over.")
    <-exit // 等待退出通知。
}

輸出:

1
2
3
send over.
recv over.

異步方式通過判斷緩沖區(qū)來決定是否阻塞。如果緩沖區(qū)已滿,發(fā)送被阻塞;緩沖區(qū)為空,接收被阻塞。

通常情況下,異步 channel 可減少排隊阻塞,具備更高的效率。但應該考慮使用指針規(guī)避大對象拷貝,將多個元素打包,減小緩沖區(qū)大小等。

func main() {
    data := make(chan int, 3) // 緩沖區(qū)可以存儲 3 個元素
    exit := make(chan bool)

    data <- 1 // 在緩沖區(qū)未滿前,不會阻塞。
    data <- 2
    data <- 3

    go func() {
        for d := range data { // 在緩沖區(qū)未空前,不會阻塞。
            fmt.Println(d)
        }

        exit <- true
    }()

    data <- 4 // 如果緩沖區(qū)已滿,阻塞。
    data <- 5
    close(data)

    <-exit
}

緩沖區(qū)是內(nèi)部屬性,并非類型構成要素。

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 發(fā)送數(shù)據(jù)引發(fā) panic 錯誤,接收立即返回零值。而 nil channel,無論收發(fā)都會被阻塞。

內(nèi)置函數(shù) len 返回未被讀取的緩沖元素數(shù)量,cap 返回緩沖區(qū)大小。

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 隱式轉換為單向隊列,只收或只發(fā)。

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 做收發(fā)操作,或執(zhí)行 default case。

func main() {
    a, b := make(chan int, 3), make(chan int)

    go func() {
        v, ok, s := 0, false, ""

        for {
            select { // 隨機選擇可用 channel,接收數(shù)據(jù)。
            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,發(fā)送數(shù)據(jù)。
        case a <- i:
        case b <- i:
        }
    }

    close(a)
    select {} // 沒有可用 channel,阻塞 main goroutine。
}

輸出:

b 3
a 0
a 1
a 2
b 4

在循環(huán)中使用 select default case 需要小心,避免形成洪水。

7.2.3 模式

用簡單工廠模式打包并發(fā)任務和 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 實現(xiàn)信號量 (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 發(fā)送數(shù)據(jù),阻塞或者成功。

            for x := 0; x < 3; x++ {
                fmt.Println(id, x)
            }

            <-sem // 接收數(shù)據(jù),使得其他阻塞 goroutine 可以發(fā)送數(shù)據(jù)。
        }(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 發(fā)出退出通知。

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: // 執(zhí)行正常任務。
                    task()
                }
            }
        }(i)
    }

    time.Sleep(time.Second * 5) // 讓測試 goroutine 運行一會。

    close(quit) // 發(fā)出退出通知。
    wg.Wait()
}

用 select 實現(xiàn)超時 (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 // 注釋掉,引發(fā) timeout。
    <-w
}

channel 是第一類對象,可傳參 (內(nèi)部實現(xiàn)為指針) 或者作為結構成員。

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 環(huán)境變量列表中添加多個工作空間,但不能和 GOROOT 相同。

export GOPATH=$HOME/projects/golib:$HOME/projects/go

通常 go get 使用第一個工作空間保存下載的第三方庫。

8.2 源文件

編碼:源碼文件必須是 UTF-8 格式,否則會導致編譯器出錯。結束:語句以 ";" 結束,多數(shù)時候可以省略。注釋:支持 "//"、"/**/" 兩種注釋方式,不能嵌套。命名:采用 camelCasing 風格,不建議使用下劃線。

8.3 包結構

所有代碼都必須組織在 package 中。

  • 源文件頭部以 "package " 聲明包名稱。
  • 包由同一目錄下的多個源碼文件組成。
  • 包名類似 namespace,與包所在目錄名、編譯文件名無關。
  • 目錄名最好不用 main、all、std 這三個保留名稱。
  • 可執(zhí)行文件必須包含 package main,入口函數(shù) main。
說明:os.Args 返回命令行參數(shù),os.Exit 終止進程。
     要獲取正確的可執(zhí)行文件路徑,可用 filepath.Abs(exec.LookPath(os.Args[0]))。

包中成員以名稱首字母大小寫決定訪問權限。

  • public: 首字母大寫,可被包外訪問。
  • internal: 首字母小寫,僅包內(nèi)成員可以訪問。
  • 該規(guī)則適用于全局變量、全局常量、類型、結構字段、函數(shù)、方法等。

8.3.1 導入包

使用包成員前,必須先用 import 關鍵字導入,但不能形成導入循環(huán)。

import "相對目錄/包主文件名"

相對目錄是指從 /pkg/ 開始的子目錄,以標準庫為例:

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" // 非導入模式: 僅讓該包執(zhí)行初始化函數(shù)。

未使用的導入包,會被編譯器視為錯誤 (不包括 "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!")
}

如繼續(xù)用 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 初始化

初始化函數(shù):

  • 每個源文件都可以定義一個或多個初始化函數(shù)。
  • 編譯器不保證多個初始化函數(shù)執(zhí)行次序。
  • 初始化函數(shù)在單一線程被調(diào)用,僅執(zhí)行一次。
  • 初始化函數(shù)在包所有全局變量初始化后執(zhí)行。
  • 在所有初始化函數(shù)結束后才執(zhí)行 main.main。
  • 無法調(diào)用初始化函數(shù)。
  • 因為無法保證初始化函數(shù)執(zhí)行順序,因此全局變量應該直接用 var 初始化。

var now = time.Now()

func init() {
    fmt.Printf("now: %v\n", now)
}

func init() {
    fmt.Printf("since: %v\n", time.Now().Sub(now))
}

可在初始化函數(shù)中使用 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

不應該濫用初始化函數(shù),僅適合完成當前文件中的相關環(huán)境設置。

8.4 文檔

擴展工具 godoc 能自動提取注釋生成幫助文檔。

  • 僅和成員相鄰 (中間沒有空行) 的注釋被當做幫助信息。
  • 相鄰行會合并成同一段落,用空行分隔段落。
  • 縮進表示格式化文本,比如示例代碼。
  • 自動轉換 URL 為鏈接。
  • 自動合并多個源碼文件中的 package 文檔。
  • 無法顯式 package main 中的成員文檔。

8.4.1 Package

  • 建議用專門的 doc.go 保存 package 幫助信息。
  • 包文檔第一整句 (中英文句號結束) 被當做 packages 列表說明。

8.4.2 Example

只要 Example 測試函數(shù)名稱符合以下規(guī)范即可。

說明:使用 suffix 作為示例名稱,其首字母必須小寫。如果文件中僅有一個 Example 函數(shù),且調(diào)用了該文件中的其他成員,那么示例會顯示整個文件內(nèi)容,而不僅僅是測試函數(shù)自己。

8.4.3 Bug

非測試源碼文件中以 BUG(author) 開始的注釋,會在幫助文檔 Bugs 節(jié)點中顯示。

// BUG(yuhen): memory leak.

第 9 章 進階

9.1 內(nèi)存布局

了解對象內(nèi)存布局,有助于理解值傳遞、引用傳遞等概念。

string

struct

slice

interface

new

make

9.2 指針陷阱

對象內(nèi)存分配會受編譯參數(shù)影響。舉個例子,當函數(shù)返回對象指針時,必然在堆上分配。

可如果該函數(shù)被內(nèi)聯(lián),那么這個指針就不會跨棧幀使用,就有可能直接在棧上分配,以實現(xiàn)代碼優(yōu)化目的。因此,是否阻止內(nèi)聯(lián)對指針輸出結果有很大影響。

允許指針指向?qū)ο蟪蓡T,并確保該對象是可達狀態(tài)。

除正常指針外,指針還有 unsafe.Pointer 和 uintptr 兩種形態(tài)。其中 uintptr 被 GC 當做普通整數(shù)對象,它不能阻止所 "引用" 對象被回收。

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

指向?qū)ο蟪蓡T的 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 創(chuàng)建 "dangling pointer" 等非法指針,所以在使用時需要特別小心。另外,cgo C.malloc 等函數(shù)所返回指針,與 GC 無關。

指針構成的 "循環(huán)引用" 加上 runtime.SetFinalizer 會導致內(nèi)存泄露。

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

垃圾回收器能正確處理 "指針循環(huán)引用",但無法確定 Finalizer 依賴次序,也就無法調(diào)用Finalizer 函數(shù),這會導致目標對象無法變成不可達狀態(tài),其所占用內(nèi)存無法被回收。

9.3 cgo

通過 cgo,可在 Go 和 C/C++ 代碼間相互調(diào)用。受 CGO_ENABLED 參數(shù)限制。

package main

/*
    #include <stdio.h>
    #include <stdlib.h>

    void hello() {
        printf("Hello, World!\n");
    }
*/
import "C"

func main() {
C.hello()
}

調(diào)試 cgo 代碼是件很麻煩的事,建議單獨保存到 .c 文件中。這樣可以將其當做獨立的 C 程序進行調(diào)試。

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()
}

編譯和調(diào)試 C,只需在命令行提供宏定義即可。

$ gcc -g -D__TEST__ -o test test.c

由于 cgo 僅掃描當前目錄,如果需要包含其他 C 項目,可在當前目錄新建一個 C 文件,然后用 #include 指令將所需的 .h、.c 都包含進來,記得在 CFLAGS 中使用 "-I" 參數(shù)指定原路徑。某些時候,可能還需指定 "-std" 參數(shù)。

9.3.1 Flags

可使用 #cgo 命令定義 CFLAGS、LDFLAGS 等參數(shù),自動合并多個設置。

/*
    #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

數(shù)據(jù)類型對應關系。

可將 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

字符串轉換函數(shù)。

/*
    #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) // 該函數(shù)在 C heap 分配內(nèi)存,需要調(diào)用 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 內(nèi)存。

/*
    #include <stdlib.h>
*/
import "C"

func main() {
    m := unsafe.Pointer(C.malloc(4 * 8))
    defer C.free(m) // 注釋釋放內(nèi)存。

    p := (*[4]int)(m) // 轉換為數(shù)組指針。
    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 會被轉換成字節(jié)數(shù)組。如果沒使用 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 函數(shù)給 C 調(diào)用,須使用 "//export" 標記。建議在獨立頭文件中聲明函數(shù)原型,避免 "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 查看動態(tài)庫使用狀態(tài)。 靜態(tài)庫使用方法類似。

9.4 Reflect

沒有運行期類型對象,實例也沒有附加字段用來表明身份。只有轉換成接口時,才會在其 itab 內(nèi)部存儲與該類型有關的信息,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

字段標簽可實現(xiàn)簡單元數(shù)據(jù)編程,比如標記 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 判斷是否實現(xiàn)了某個具體接口,AssignableTo、ConvertibleTo 用于賦值和轉換判斷。

type Data struct {
}

func (*Data) String() string {
    return ""
}

func main() {
    var d *Data
    t := reflect.TypeOf(d)

    // 沒法直接獲取接口類型,好在接口本身是個 struct,創(chuàng)建
    // 一個空指針對象,這樣傳遞給 TypeOf 轉換成 interface{}
    // 時就有類型信息了。。
    it := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()

    // 為啥不是 t.Implements(fmt.Stringer),完全可以由編譯器生成。
    fmt.Println(t.Implements(it))
}

某些時候,獲取對齊信息對于內(nèi)存自動分析是很有用的。

type Data struct {
    b byte
    x int32
}

func main() {
    var d Data

    t := reflect.TypeOf(d)
    fmt.Println(t.Size(), t.Align()) // sizeof,以及最寬字段的對齊模數(shù)。

    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

當然,轉換成具體類型不會引發(fā) 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

將對象轉換為接口,會發(fā)生復制行為。該復制品只讀,無法被修改。所以要通過接口改變目標對象狀態(tài),必須是 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

可獲取方法參數(shù)、返回值類型等信息。

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

動態(tài)調(diào)用方法很簡單,按 In 列表準備好所需參數(shù)即可 (不包括 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())
    }
}

非導出方法無法調(diào)用,甚至無法透過指針操作,因為接口類型信息中沒有該方法地址。

9.4.4 Make

利用 Make、New 等函數(shù),可實現(xiàn)近似泛型操作。

var (
    Int = reflect.TypeOf(0)
    String = reflect.TypeOf("")
)

func Make(T reflect.Type, fptr interface{}) {

    // 實際創(chuàng)建 slice 的包裝函數(shù)。
    swap := func(in []reflect.Value) []reflect.Value {

        // --- 省略算法內(nèi)容 --- //
        // 返回和類型匹配的 slice 對象。
        return []reflect.Value{
            reflect.MakeSlice(
                reflect.SliceOf(T), // slice type
                int(in[0].Int()), // len
                int(in[1].Int()) // cap
            ),
        }
    }

    // 傳入的是函數(shù)變量指針,因為我們要將變量指向 swap 函數(shù)。
    fn := reflect.ValueOf(fptr).Elem()

    // 獲取函數(shù)指針類型,生成所需 swap function value。
    v := reflect.MakeFunc(fn.Type(), swap)

    // 修改函數(shù)指針實際指向,也就是 swap。
    fn.Set(v)
}

func main() {
    var makeints func(int, int) []int
    var makestrings func(int, int) []string

    // 用相同算法,生成不同類型創(chuàng)建函數(shù)。
    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{"", "", ""}

原理并不復雜。

  1. 核心是提供一個 swap 函數(shù),其中利用 reflect.MakeSlice 生成最終 slice 對象,因此需要傳入 element type、len、cap 參數(shù)。
  2. 接下來,利用 MakeFunc 函數(shù)生成 swap value,并修改函數(shù)變量指向,以達到調(diào)用 swap 的目的。
  3. 當調(diào)用具體類型的函數(shù)變量時,實際內(nèi)部調(diào)用的是 swap,相關代碼會自動轉換參數(shù)列表,并將返回結果還原成具體類型返回值。

如此,在共享算法的前提下,無須用 interface{},無須做類型轉換,頗有泛型的效果。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號