鍍金池/ 教程/ GO/ 第一部分 語言
第二部分 源碼
第三部分 附錄
第一部分 語言

第一部分 語言

1.1 變量

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

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

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

func main() {
    x := 123 // 注意檢查,是定義新局部變量,還是修改全局變量。該方式容易造成錯(cuò)誤。
}

可一次定義多個(gè)變量。

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

多變量賦值時(shí),先計(jì)算所有相關(guān)值,然后再?gòu)淖蟮接乙来钨x值。

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

編譯器會(huì)將未使用的局部變量當(dāng)做錯(cuò)誤。

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 會(huì)被重復(fù)使用。

{
    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" // 未使用局部常量不會(huì)引發(fā)編譯錯(cuò)誤。
}

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

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

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

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

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

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

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

枚舉

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

const (
    Sunday = iota // 0
    Monday // 1,通常省略后續(xù)行表達(dá)式。
    Tuesday // 2
    Wednesday // 3
    Thursday // 4
    Friday // 5
    Saturday // 6
)

const (
    _ = iota // iota = 0
    KB int64 = 1 << (10 * iota) // iota = 1
    MB // 與 KB 表達(dá)式相同,但 iota = 2
    GB
    TB
)

在同一常量組中,可以提供多個(gè) iota,它們各自增長(zhǎng)。

const (
    A, B = iota, iota << 10 // 0, 0 << 10
    C, D // 1, 1 << 10
)

如果 iota 自增被打斷,須顯式恢復(fù)。

const (
    A = iota // 0
    B // 1
    C = "c" // c
    D // c,與上一行相同。
    E = iota // 4,顯式恢復(fù)。注意計(jì)數(shù)包含了 C、D 兩行。
    F // 5
)

可通過自定義類型來實(shí)現(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) // 常量會(huì)被編譯器自動(dòng)轉(zhuǎn)換。
}

1.3 基本類型

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

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/1.png" alt="" />

支持八進(jìn)制、十六進(jìn)制,以及科學(xué)記數(shù)法。標(biāo)準(zhǔn)庫(kù) math 定義了各數(shù)字類型取值范圍。

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

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

1.4 引用類型

引用類型包括 slice、map 和 channel。它們有復(fù)雜的內(nèi)部結(jié)構(gòu),除了申請(qǐng)內(nèi)存外,還需要初始化相關(guān)屬性。

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

a := []int{0, 0, 0} // 提供初始化表達(dá)式。
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)

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

1.5 類型轉(zhuǎn)換

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

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

使用括號(hào)避免優(yōu)先級(jí)錯(cuò)誤。

*Point(p) // 相當(dāng)于 *(Point(p))
(*Point)(p)
<-chan int(c) // 相當(dāng)于 <-(chan int(c))
(<-chan int)(c)

同樣不能將其他類型當(dāng) 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ù)組。

  • 默認(rèn)值是空字符串 ""。
  • 用索引號(hào)訪問某字節(jié),如 s[i]。
  • 不能用序號(hào)獲取字節(jié)元素指針,&s[i] 非法。
  • 不可變類型,無法修改字節(jié)數(shù)組。
  • 字節(jié)數(shù)組尾部不包含 NULL。
  • runtime.h

struct String
{
    byte* str;
    intgo len;
};

使用索引號(hào)訪問字符 (byte)。

s := "abc"
println(s[0] == '\x61', s[1] == 'b', s[2] == 0x63)

輸出:

true true true

使用 "`" 定義不做轉(zhuǎn)義處理的原始字符串,支持跨行。

s := `a
b\r\n\x00
c`

println(s)

輸出:

a
b\r\n\x00
c

連接跨行字符串時(shí),"+" 必須在上一行末尾,否則導(dǎo)致編譯錯(cuò)誤。

s := "Hello, " +
    "World!"

s2 := "Hello, "
    + "World!" // Error: invalid operation: + untyped string

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

s := "Hello, World!"

s1 := s[:5] // Hello
s2 := s[7:] // World!
s3 := s[1:5] // ello

單引號(hào)字符常量表示 Unicode Code Point,支持 \uFFFF、\U7FFFFFFF、\xFF 格式。對(duì)應(yīng) 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

要修改字符串,可先將其轉(zhuǎn)換成 []rune 或 []byte,完成后再轉(zhuǎn)換為 string。無論哪種轉(zhuǎn)換,都會(huì)重新分配內(nèi)存,并復(fù)制字節(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)遍歷字符串時(shí),也有 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。

  • 默認(rèn)值 nil,沒有 NULL 常量。
  • 操作符 "&" 取變量地址,"*" 透過指針訪問目標(biāo)對(duì)象。
  • 不支持指針運(yùn)算,不支持 "->" 運(yùn)算符,直接用 "." 訪問目標(biāo)成員。
func main() {
    type data struct{ a int }

    var d = data{1234}
    var p *data

    p = &d
    fmt.Printf("%p, %v\n", p, p.a) // 直接用指針訪問目標(biāo)對(duì)象成員,無須轉(zhuǎn)換。
}

輸出:

0x2101ef018, 1234

不能對(duì)指針做加減法等運(yùn)算。

x := 1234
p := &x
p++ // Error: invalid operation: p += 1 (mismatched types *int and int)

可以在 unsafe.Pointer 和任意類型指針間進(jìn)行轉(zhuǎn)換。

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

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

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

將 Pointer 轉(zhuǎn)換成 uintptr,可變相實(shí)現(xiàn)指針運(yù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 當(dāng)成普通整數(shù)對(duì)象,它無法阻止 "關(guān)聯(lián)" 對(duì)象被回收。

1.8 自定義類型

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

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

  • 具有相同基類型的指針。
  • 具有相同元素類型和長(zhǎng)度的 array。
  • 具有相同元素類型的 slice。
  • 具有相同鍵值類型的 map。
  • 具有相同元素類型和傳送方向的 channel。
  • 具有相同字段序列 (字段名、類型、標(biāo)簽、順序) 的匿名 struct。
  • 簽名相同 (參數(shù)和返回值,不包括參數(shù)名稱) 的 function。
  • 方法集相同 (方法名、方法簽名相同,和次序無關(guān)) 的 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ù)存儲(chǔ)結(jié)構(gòu)外,它們之間沒有任何關(guān)系,不會(huì)持有原類型任何信息。除非目標(biāo)類型是未命名類型,否則必須顯式轉(zhuǎn)換。

x := 1234
var b bigint = bigint(x) // 必須顯式轉(zhuǎn)換,除非是常量。
var b2 int64 = int64(b)

var s myslice = []int{1, 2, 3} // 未命名類型,隱式轉(zhuǎn)換。
var s2 []int = s

第2章 表達(dá)式

2.1 保留字

語言設(shè)計(jì)簡(jiǎn)練,保留字不多。

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 運(yùn)算符

全部運(yùn)算符、分隔符,以及其他符號(hào)。

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/2.png" alt="" />

運(yùn)算符結(jié)合律全部從左到右。

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/3.png" alt="" />

簡(jiǎn)單位運(yùn)算演示。

0110 & 1011 = 0010 AND 都為1。
0110 | 1011 = 1111 OR 至少一個(gè)為1。
0110 ^ 1011 = 1101 XOR 只能一個(gè)為1。
0110 &^ 1011 = 0100 AND NOT 清除標(biāo)志位。

標(biāo)志位操作。

a := 0
a |= 1 << 2 // 0000100: 在 bit2 設(shè)置標(biāo)志位。
a |= 1 << 6 // 1000100: 在 bit6 設(shè)置標(biāo)志位
a = a &^ (1 << 6) // 0000100: 清除 bit6 標(biāo)志位。

不支持運(yùn)算符重載。尤其需要注意,"++"、"--" 是語句而非表達(dá)式。

n := 0
p := &n

// b := n++ // syntax error
// if n++ == 1 {} // syntax error
// ++n // syntax error

n++
*p++ // (*p)++

沒有 "~",取反運(yùn)算也用 "^"。

x := 1
x, ^x // 0001, -0010

2.3 初始化

初始化復(fù)合對(duì)象,必須使用類型標(biāo)簽,且左大括號(hào)必須在類型尾部。

// 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}

初始化值以 "," 分隔??梢苑侄嘈校詈笠恍斜仨氁?"," 或 "}" 結(jié)尾。

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

很特別的寫法:

  • 可省略條件表達(dá)式括號(hào)。
  • 支持初始化語句,可定義代碼塊局部變量。
  • 代碼塊左大括號(hào)必須在條件表達(dá)式尾部。
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 左大括號(hào)位置。
    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 (;;) {}
}

不要期望編譯器能理解你的想法,在初始化語句中計(jì)算出全部結(jié)果是個(gè)好主意。

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

類似迭代器操作,返回 (索引, 值) 或 (鍵, 值)。

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/4.png" alt="" />

可忽略不想要的返回值,或用 "_" 這個(gè)特殊變量。

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 會(huì)復(fù)制對(duì)象。

a := [3]int{0, 1, 2}

for i, v := range a { // index、value 都是從復(fù)制品中取出。

    if i == 0 { // 在修改前,我們先修改原數(shù)組。
        a[1], a[2] = 999, 999
        fmt.Println(a) // 確認(rèn)修改有效,輸出 [0, 999, 999]。
    }

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

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

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

s := []int{1, 2, 3, 4, 5}

for i, v := range s { // 復(fù)制 struct slice { pointer, len, cap }。

    if i == 0 {
        s = s[:3] // 對(duì) slice 的修改,不會(huì)影響 range。
        s[2] = 100 // 對(duì)底層數(shù)據(jù)的修改。
    }

    println(i, v)
}

輸出:

0 1
1 2
2 100
3 4
4 5

另外兩種引用類型 map、channel 是指針包裝,而不像 slice 是 struct。

2.4.4 Switch

分支表達(dá)式可以是任意類型,不限于常量??墒÷?break,默認(rèn)自動(dòng)終止。

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

省略條件表達(dá)式,可當(dāng) 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 跳轉(zhuǎn)。標(biāo)簽名區(qū)分大小寫,未使用標(biāo)簽引發(fā)錯(cuò)誤。

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
}

配合標(biāo)簽,break 和 continue 可在多級(jí)嵌套循環(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) 和 默認(rèn)參數(shù) (default parameter)。

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

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

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

函數(shù)是第一類對(duì)象,可作為參數(shù)傳遞。建議將復(fù)雜簽名定義為函數(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ù)當(dāng)參數(shù)。

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

    println(s1, s2)
}

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

3.2 變參

變參本質(zhì)上就是 slice。只能有一個(gè),且必須是最后一個(gè)。

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 對(duì)象做變參時(shí),必須展開。

func main() {
    s := []int{1, 2, 3}
    println(test("sum: %d", s...))
}

3.3 返回值

不能用容器對(duì)象接收多返回值。只能用多個(gè)變量,或 "_" 忽略。

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)用實(shí)參。

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ù)可被同名局部變量遮蔽,此時(shí)需要顯式返回。

func add(x, y int) (z int) {
    { // 不能在一個(gè)級(jí)別,引發(fā) "z redeclared in this block" 錯(cuò)誤。
        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 返回前,會(huì)先修改命名返回參數(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ù)可賦值給變量,做為結(jié)構(gòu)字段,或者在 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)())

閉包復(fù)制的是原對(duì)象指針,這就很容易解釋延遲引用現(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 實(shí)際返回的是 FuncVal 對(duì)象,其中包含了匿名函數(shù)地址、閉包對(duì)象指針。當(dāng)調(diào)用匿名函數(shù)時(shí),只需以某個(gè)寄存器傳遞該對(duì)象即可。

FuncVal { func_address, closure_var_pointer ... }

3.5 延遲調(diào)用

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

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

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

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

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

func test(x int) {
    defer println("a")
    defer println("b")

    defer func() {
        println(100 / x) // div0 異常未被捕獲,逐步往外傳遞,最終終止進(jìn)程。
    }()

    defer println("c")
}

func main() {
    test(0)
}

輸出:

c
b
a
panic: runtime error: integer divide by zero

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

func test() {
    x, y := 10, 20

    defer func(i int) {
        println("defer:", i, y) // y 閉包引用
    }(x) // x 被復(fù)制

    x += 10
    y += 100
    println("x =", x, "y =", y)
}

輸出:

x = 20 y = 120
defer: 10 120

濫用 defer 可能會(huì)導(dǎo)致性能問題,尤其是在一個(gè) "大循環(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 錯(cuò)誤處理

沒有結(jié)構(gòu)化異常,使用 panic 拋出錯(cuò)誤,recover 捕獲錯(cuò)誤。

func test() {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string)) // 將 interface{} 轉(zhuǎn)型為具體類型。
        }
    }()

    panic("panic error!")
}

由于 panic、recover 參數(shù)類型為 interface{},因此可拋出任何類型對(duì)象。

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

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

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)用才會(huì)終止錯(cuò)誤,否則總是返回 nil。任何未捕獲的錯(cuò)誤都會(huì)沿調(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")
}

如果需要保護(hù)代碼片段,可將代碼塊重構(gòu)成匿名函數(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ā)中斷性錯(cuò)誤外,還可返回 error 類型錯(cuò)誤對(duì)象來表示函數(shù)調(diào)用狀態(tài)。

type error interface {
    Error() string
}

標(biāo)準(zhǔn)庫(kù) errors.New 和 fmt.Errorf 函數(shù)用于創(chuàng)建實(shí)現(xiàn) error 接口的錯(cuò)誤對(duì)象。通過判斷錯(cuò)誤對(duì)象實(shí)例來確定具體錯(cuò)誤類型。

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 兩種方式?慣例是:導(dǎo)致關(guān)鍵流程出現(xiàn)不可修復(fù)性錯(cuò)誤的使用 panic,其他使用 error。

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

4.1 Array

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

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

可用復(fù)合語句初始化。

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

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

支持多維數(shù)組。

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

值拷貝行為會(huì)造成性能問題,通常會(huì)建議使用 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 都返回?cái)?shù)組長(zhǎng)度 (元素?cái)?shù)量)。

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

4.2 Slice

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

runtime.h

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

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/6.png" alt="" />

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

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/5.png" alt="" />

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

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 對(duì)象,自動(dòng)分配底層數(shù)組。

s1 := []int{0, 1, 2, 3, 8: 100} // 通過初始化表達(dá)式構(gòu)造,可使用索引號(hào)。
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,相當(dāng)于 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 動(dòng)態(tài)創(chuàng)建 slice,避免了數(shù)組必須用常量做長(zhǎng)度的麻煩。還可用指針直接訪問底層數(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 對(duì)象,以便在 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

http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/7.png" alt="" />

新對(duì)象依舊指向原底層數(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 對(duì)象。

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]

簡(jiǎn)單點(diǎn)說,就是在 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) // 添加多個(gè)值。

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

data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]

s = append(s, 100, 200) // 一次 append 兩個(gè)值,超出 s.cap 限制。

fmt.Println(s, data) // 重新分配底層數(shù)組,與原數(shù)組無關(guān)。
fmt.Println(&s[0], &data[0]) // 比對(duì)底層數(shù)組起始指針。

輸出:

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

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

通常以2倍容量重新分配底層數(shù)組。在大批量添加數(shù)據(jù)時(shí),建議一次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復(fù)制開銷。或初始化足夠長(zhǎng)的 len 屬性,改用索引號(hào)進(jìn)行操作。及時(shí)釋放不再使用的 slice 對(duì)象,避免持有過期數(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 在兩個(gè) slice 間復(fù)制數(shù)據(jù),復(fù)制長(zhǎng)度以 len 小的為準(zhǔn)。兩個(gè) 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]

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

4.3 Map

引用類型,哈希表。鍵必須是支持相等運(yùn)算符 (==、!=) 類型,比如 number、string、pointer、array、struct,以及對(duì)應(yīng)的 interface。值可以是任意類型,沒有限制。

m := map[int]struct {
    name string
    age int
}{
    1: {"user1", 10}, // 可省略元素類型。
    2: {"user2", 20},
}

println(m[1].name)

預(yù)先給 make 函數(shù)一個(gè)合理元素?cái)?shù)量參數(shù),有助于提升性能。因?yàn)槭孪壬暾?qǐng)一大塊內(nèi)存,可避免后續(xù)操作時(shí)頻繁擴(kuò)張。

m := make(map[string]int, 1000)

常見操作:

m := map[string]int{
    "a": 1,
}

if v, ok := m["a"]; ok { // 判斷 key 是否存在。
    println(v)
}

println(m["c"]) // 對(duì)于不存在的 key,直接返回 \0,不會(huì)出錯(cuò)。

m["b"] = 2 // 新增或修改。

delete(m, "c") // 刪除。如果 key 不存在,不會(huì)出錯(cuò)。

println(len(m)) // 獲取鍵值對(duì)數(shù)量。cap 無效。

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

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

從 map 中取回的是一個(gè) value 臨時(shí)復(fù)制品,對(duì)其成員的修改是沒有任何意義的。

type user struct{ name string }

m := map[int]user{ // 當(dāng) map 因擴(kuò)張而重新哈希時(shí),各鍵值項(xiàng)存儲(chǔ)位置都會(huì)發(fā)生改變。 因此,map
    1: {"user1"}, // 被設(shè)計(jì)成 not addressable。 類似 m[1].name 這種期望透過原 value
} // 指針修改成員的行為自然會(huì)被禁止。

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" // 返回的是指針復(fù)制品。透過指針修改原對(duì)象是允許的。

可以在迭代時(shí)安全刪除鍵值。但如果期間有新增操作,那么就不知道會(huì)有什么意外了。

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

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

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

順序初始化必須包含全部字段,否則會(huì)出錯(cuò)。

type User struct {
    name string
    age int
}

u1 := User{"Tom", 20}
u2 := User{"Tom"} // Error: too few values in struct initializer

支持匿名結(jié)構(gòu),可用作結(jié)構(gòu)成員或定義變量。

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

可定義字段標(biāo)簽,用反射讀取。標(biāo)簽是類型的組成部分。

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é)構(gòu) "節(jié)省" 內(nèi)存,比如用來實(shí)現(xiàn) set 數(shù)據(jù)結(jié)構(gòu),或者實(shí)現(xiàn)沒有 "狀態(tài)" 只有方法的 "靜態(tài)類"。

var null struct{}

set := make(map[string]struct{})
set["a"] = null

4.4.1 匿名字段

匿名字段不過是一種語法糖,從根本上說,就是一個(gè)與成員類型同名 (不含包名) 的字段。被匿名嵌入的可以是任何類型,當(dāng)然也包括指針。

type User struct {
    name string
}

type Manager struct {
    User
    title string
}
m := Manager{
    User: User{"Tom"}, // 匿名字段的顯式字段名,和類型名相同。
    title: "Administrator",
}

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

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"

外層同名字段會(huì)遮蔽嵌入字段成員,相同層次的同名字段也會(huì)讓編譯器無所適從。解決方法是使用顯式字段名。

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

不能同時(shí)嵌入某一類型和其指針類型,因?yàn)樗鼈兠窒嗤?/p>

type