99re热视频这里只精品,久久久天堂国产精品女人,国产av一区二区三区,久久久精品成人免费看片,99久久精品免费看国产一区二区三区

Go 學(xué)習(xí)筆記第一部分 語(yǔ)言

2018-09-28 18:50 更新

第一部分 語(yǔ)言

1.1 變量

Go 是靜態(tài)類型語(yǔ)言,不能在運(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)

特殊只寫(xiě)變量 "_",用于忽略值占位。

func test() (int, string) {
    return 1, "abc"
}

func main() {
    _, s := test()
    println(s)
}

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

var s string // 全局變量沒(méi)問(wèn)題。

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開(kāi)始按行計(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
)

可通過(guò)自定義類型來(lái)實(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)。

支持八進(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)訪問(wèn)某字節(jié),如 s[i]。
  • 不能用序號(hào)獲取字節(jié)元素指針,&s[i] 非法。
  • 不可變類型,無(wú)法修改字節(jié)數(shù)組。
  • 字節(jié)數(shù)組尾部不包含 NULL。
  • runtime.h

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

使用索引號(hào)訪問(wèn)字符 (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。無(wú)論哪種轉(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,沒(méi)有 NULL 常量。
  • 操作符 "&" 取變量地址,"*" 透過(guò)指針訪問(wèn)目標(biāo)對(duì)象。
  • 不支持指針運(yùn)算,不支持 "->" 運(yùn)算符,直接用 "." 訪問(wè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) // 直接用指針訪問(wèn)目標(biāo)對(duì)象成員,無(wú)須轉(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ì)象,它無(wú)法阻止 "關(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。
  • 方法集相同 (方法名、方法簽名相同,和次序無(wú)關(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)外,它們之間沒(méi)有任何關(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 保留字

語(yǔ)言設(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)。

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

簡(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)算符重載。尤其需要注意,"++"、"--" 是語(yǔ)句而非表達(dá)式。

n := 0
p := &n

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

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

沒(méi)有 "~",取反運(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

很特別的寫(xiě)法:

  • 可省略條件表達(dá)式括號(hào)。
  • 支持初始化語(yǔ)句,可定義代碼塊局部變量。
  • 代碼塊左大括號(hào)必須在條件表達(dá)式尾部。
x := 0

// if x > 10 // Error: missing condition in if statement
// {
// }

if n := "abc"; x > 0 { // 初始化語(yǔ)句未必就是定義變量,比如 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 語(yǔ)法。

s := "abc"

for i, n := 0, len(s); i < n; i++ { // 常見(jiàn)的 for 循環(huán),支持初始化語(yǔ)句。
    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 (;;) {}
}

不要期望編譯器能理解你的想法,在初始化語(yǔ)句中計(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

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

可忽略不想要的返回值,或用 "_" 這個(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]; { // 帶初始化語(yǔ)句
    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ū)分大小寫(xiě),未使用標(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)。

  • 無(wú)需聲明原型。
  • 支持不定長(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ù),必須有明確的終止語(yǔ)句,否則會(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í),必須展開(kāi)。

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)用通過(guò)閉包讀取和修改。

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)致性能問(wèn)題,尤其是在一個(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ò)誤處理

沒(méi)有結(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() // 無(wú)效!
    defer fmt.Println(recover()) // 無(wú)效!
    defer func() {
        func() {
            println("defer inner")
            recover() // 無(wú)效!
        }()
    }()

    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ì)象來(lái)表示函數(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ì)象。通過(guò)判斷錯(cuò)誤對(duì)象實(shí)例來(lái)確定具體錯(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)存總是被初始化過(guò)的。
  • 指針數(shù)組 [n]T,數(shù)組指針 [n]T。

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

a := [3]int{1, 2} // 未初始化元素值為0。
b := [...]int{1, 2, 3, 4} // 通過(guò)初始化值確定數(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ì)造成性能問(wèn)題,通常會(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

需要說(shuō)明,slice 并不是數(shù)組或數(shù)組指針。它通過(guò)內(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ù)量,讀寫(xiě)操作不能超過(guò)該限制。
  • 屬性 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]

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

讀寫(xiě)操作實(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} // 通過(guò)初始化表達(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)度的麻煩。還可用指針直接訪問(wèn)底層數(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

新對(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)說(shuō),就是在 array[slice.high] 寫(xiě)數(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ù)組無(wú)關(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ì)超過(guò) s.cap 限制,也就不會(huì)重新分配。

通常以2倍容量重新分配底層數(shù)組。在大批量添加數(shù)據(jù)時(shí),建議一次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復(fù)制開(kāi)銷?;虺跏蓟銐蜷L(zhǎng)的 len 屬性,改用索引號(hào)進(jìn)行操作。及時(shí)釋放不再使用的 slice 對(duì)象,避免持有過(guò)期數(shù)組,造成 GC 無(wú)法回收。

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éi)有限制。

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)

常見(jiàn)操作:

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 無(wú)效。

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

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

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

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 這種期望透過(guò)原 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ù)制品。透過(guò)指針修改原對(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)存,比如用來(lái)實(shí)現(xiàn) set 數(shù)據(jù)結(jié)構(gòu),或者實(shí)現(xiàn)沒(méi)有 "狀態(tài)" 只有方法的 "靜態(tài)類"。

var null struct{}

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

4.4.1 匿名字段

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

type User struct {
    name string
}

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

可以像普通字段那樣訪問(wèn)匿名字段成員,編譯器從外向內(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ì)讓編譯器無(wú)所適從。解決方法是使用顯式字段名。

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 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)存布局和行為類似繼承。沒(méi)有 class 關(guān)鍵字,沒(méi)有繼承、多態(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
                 // 沒(méi)有繼承,自然也不會(huì)有多態(tài)。
var u User = m.User // 同類型拷貝。

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

可用 unsafe 包相關(guān)函數(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 方法定義

方法總是綁定對(duì)象實(shí)例,并隱式將實(shí)例作為第一實(shí)參 (receiver)。

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

沒(méi)有構(gòu)造和析構(gòu)方法,通常用簡(jiǎn)單工廠模式返回對(duì)象實(shí)例。

type Queue struct {
    elements []interface{}
}

func NewQueue() *Queue { // 創(chuàng)建對(duì)象實(shí)例。
    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)
}

方法不過(guò)是一種特殊的函數(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開(kāi)始,不再支持多級(jí)指針查找方法成員。

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 匿名字段

可以像字段成員那樣訪問(wèn)匿名字段方法,編譯器負(fù)責(zé)查找。

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}

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

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

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

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

5.4 表達(dá)式

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

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

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

兩者都可像普通函數(shù)那樣賦值和傳參,區(qū)別在于 method value 綁定實(shí)例,而 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 會(huì)復(fù)制 receiver。

type User struct {
    id int
    name string
}

func (self User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即復(fù)制 receiver,因?yàn)椴皇侵羔橆愋?,不受后續(xù)修改影響。

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

    mValue()
}

輸出:

{2 Jack}
{1 Tom}

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

FuncVal { method_address, receiver_copy }

可依據(jù)方法集轉(zhuǎn)換 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)。
}                            // 實(shí)際依然是 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 接口定義

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

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

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

func Print(v interface{}) {
    fmt.Printf("%T: %v\n", v, v)
}

func main() {
    Print(1)
    Print("Hello, World!")
}

輸出:

int: 1
string: Hello, World!

匿名接口可用作變量類型,或結(jié)構(gòu)成員。

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í)行機(jī)制

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

runtime.h

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

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

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

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

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}

接口轉(zhuǎn)型返回臨時(shí)對(duì)象,只有使用指針才能修改其狀態(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 時(shí),接口才等于 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 接口轉(zhuǎn)換

利用類型推斷,可判斷接口對(duì)象是否某個(gè)具體的接口或類型。

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

超集接口對(duì)象可轉(zhuǎn)換為子集接口,反之出錯(cuò)。

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

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

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

某些時(shí)候,讓函數(shù)直接 "實(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 在語(yǔ)言層面對(duì)并發(fā)編程提供支持,一種類似協(xié)程,稱作 goroutine 的機(jī)制。

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

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

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

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

默認(rèn)情況下,進(jìn)程啟動(dòng)后僅允許一個(gè)系統(tǒng)線程服務(wù)于 goroutine??墒褂铆h(huán)境變量或標(biāo)準(zhǔn)庫(kù)函數(shù) runtime.GOMAXPROCS 修改,讓調(diào)度器用多個(gè)線程實(shí)現(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 // 程序開(kāi)始到結(jié)束時(shí)間差 (非 CPU 時(shí)間)
user 7.66 // 用戶態(tài)所使用 CPU 時(shí)間片 (多核累加)
sys 0.01 // 內(nèi)核態(tài)所使用 CPU 時(shí)間片

$ GOMAXPROCS=2 time -p ./test

0 9223372030412324865
1 9223372030412324865

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

調(diào)用 runtime.Goexit 將立即終止當(dāng)前 goroutine 執(zhí)行,調(diào)度器確保所有已注冊(cè) 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() // 終止當(dāng)前 goroutine
            println("B") // 不會(huì)執(zhí)行
        }()

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

    wg.Wait()
}

輸出:

B.defer
A.defer

和協(xié)程 yield 作用類似,Gosched 讓出底層線程,將當(dāng)前 goroutine 暫停,放回隊(duì)列等待下次被調(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 模式的具體實(shí)現(xiàn),用于多個(gè) goroutine 通訊。其內(nèi)部實(shí)現(xiàn)了同步,確保并發(fā)安全。

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

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

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

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

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

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

輸出:

1
2
3
send over.
recv over.

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

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

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

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

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

        exit <- true
    }()

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

    <-exit
}

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

var a, b chan int = make(chan int), make(chan int, 3)

除用 range 外,還可用 ok-idiom 模式判斷 channel 是否關(guān)閉。

for {
    if d, ok := <-data; ok {
        fmt.Println(d)
    } else {
        break
    }
}

向 closed channel 發(fā)送數(shù)據(jù)引發(fā) panic 錯(cuò)誤,接收立即返回零值。而 nil channel,無(wú)論收發(fā)都會(huì)被阻塞。

內(nèi)置函數(shù) len 返回未被讀取的緩沖元素?cái)?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 隱式轉(zhuǎn)換為單向隊(duì)列,只收或只發(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 轉(zhuǎn)換為普通 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 選擇

如果需要同時(shí)處理多個(gè) channel,可使用 select 語(yǔ)句。它隨機(jī)選擇一個(gè)可用 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 { // 隨機(jī)選擇可用 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 { // 隨機(jī)選擇可用 channel,發(fā)送數(shù)據(jù)。
        case a <- i:
        case b <- i:
        }
    }

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

輸出:

b 3
a 0
a 1
a 2
b 4

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

7.2.3 模式

用簡(jiǎn)單工廠模式打包并發(fā)任務(wù)和 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 結(jié)束返回。
}

用 channel 實(shí)現(xiàn)信號(hào)量 (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 不會(huì)阻塞,因此可用作退出通知。
                    return
                default: // 執(zhí)行正常任務(wù)。
                    task()
                }
            }
        }(i)
    }

    time.Sleep(time.Second * 5) // 讓測(cè)試 goroutine 運(yùn)行一會(huì)。

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

用 select 實(shí)現(xiàn)超時(shí) (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 是第一類對(duì)象,可傳參 (內(nèi)部實(shí)現(xiàn)為指針) 或者作為結(jié)構(gòu)成員。

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 工作空間

編譯工具對(duì)源碼目錄有嚴(yán)格要求,每個(gè)工作空間 (workspace) 必須由 bin、pkg、src 三個(gè)目錄組成。

可在 GOPATH 環(huán)境變量列表中添加多個(gè)工作空間,但不能和 GOROOT 相同。

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

通常 go get 使用第一個(gè)工作空間保存下載的第三方庫(kù)。

8.2 源文件

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

8.3 包結(jié)構(gòu)

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

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

包中成員以名稱首字母大小寫(xiě)決定訪問(wèn)權(quán)限。

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

8.3.1 導(dǎo)入包

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

import "相對(duì)目錄/包主文件名"

相對(duì)目錄是指從 /pkg/ 開(kāi)始的子目錄,以標(biāo)準(zhǔn)庫(kù)為例:

import "fmt" -> /usr/local/go/pkg/darwin_amd64/fmt.a
import "os/exec" -> /usr/local/go/pkg/darwin_amd64/os/exec.a

在導(dǎo)入時(shí),可指定包成員訪問(wèn)方式。比如對(duì)包重命名,以避免同名沖突。

import "yuhen/test" // 默認(rèn)模式: test.A
import M "yuhen/test" // 包重命名: M.A
import . "yuhen/test" // 簡(jiǎn)便模式: A
import _ "yuhen/test" // 非導(dǎo)入模式: 僅讓該包執(zhí)行初始化函數(shù)。

未使用的導(dǎo)入包,會(huì)被編譯器視為錯(cuò)誤 (不包括 "import _")。

./main.go:4: imported and not used: "fmt"

對(duì)于當(dāng)前目錄下的子包,除使用默認(rèn)完整導(dǎo)入路徑外,還可使用 local 方式。

main.go

import "learn/test" // 正常模式
import "./test" // 本地模式,僅對(duì) go run main.go 有效。

8.3.2 自定義路徑

可通過(guò) meta 設(shè)置為代碼庫(kù)設(shè)置自定義路徑。

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

如此,該庫(kù)就有兩個(gè)有效導(dǎo)入路徑,可能會(huì)導(dǎo)致存儲(chǔ)兩個(gè)本地副本。為此,可以給庫(kù)添加專門的 "import comment"。當(dāng) go get 下載完成后,會(huì)檢查本地存儲(chǔ)路徑和該注釋是否一致。

github.com/qyuhen/test/abc.go

package test // import "test.com/qyuhen/test"

func Hello() {
    println("Hello, Custom import path!")
}

如繼續(xù)用 github 路徑,會(huì)導(dǎo)致 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"

這就強(qiáng)制包用戶使用唯一路徑,也便于日后將包遷移到其他位置。

資源:Go 1.4 Custom Import Path Checking

8.3.3 初始化

初始化函數(shù):

  • 每個(gè)源文件都可以定義一個(gè)或多個(gè)初始化函數(shù)。
  • 編譯器不保證多個(gè)初始化函數(shù)執(zhí)行次序。
  • 初始化函數(shù)在單一線程被調(diào)用,僅執(zhí)行一次。
  • 初始化函數(shù)在包所有全局變量初始化后執(zhí)行。
  • 在所有初始化函數(shù)結(jié)束后才執(zhí)行 main.main。
  • 無(wú)法調(diào)用初始化函數(shù)。
  • 因?yàn)闊o(wú)法保證初始化函數(shù)執(zhí)行順序,因此全局變量應(yīng)該直接用 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,可等待其結(jié)束。

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

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

8.4 文檔

擴(kuò)展工具 godoc 能自動(dòng)提取注釋生成幫助文檔。

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

8.4.1 Package

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

8.4.2 Example

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

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

8.4.3 Bug

非測(cè)試源碼文件中以 BUG(author) 開(kāi)始的注釋,會(huì)在幫助文檔 Bugs 節(jié)點(diǎn)中顯示。

// BUG(yuhen): memory leak.

第 9 章 進(jìn)階

9.1 內(nèi)存布局

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

string

struct

slice

interface

new

make

9.2 指針陷阱

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

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

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

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

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 被當(dāng)做普通指針對(duì)待。

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,同樣能確保對(duì)象不被回收。

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" 等非法指針,所以在使用時(shí)需要特別小心。另外,cgo C.malloc 等函數(shù)所返回指針,與 GC 無(wú)關(guān)。

指針構(gòu)成的 "循環(huán)引用" 加上 runtime.SetFinalizer 會(huì)導(dǎo)致內(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)引用",但無(wú)法確定 Finalizer 依賴次序,也就無(wú)法調(diào)用Finalizer 函數(shù),這會(huì)導(dǎo)致目標(biāo)對(duì)象無(wú)法變成不可達(dá)狀態(tài),其所占用內(nèi)存無(wú)法被回收。

9.3 cgo

通過(guò) 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 代碼是件很麻煩的事,建議單獨(dú)保存到 .c 文件中。這樣可以將其當(dāng)做獨(dú)立的 C 程序進(jìn)行調(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 僅掃描當(dāng)前目錄,如果需要包含其他 C 項(xiàng)目,可在當(dāng)前目錄新建一個(gè) C 文件,然后用 #include 指令將所需的 .h、.c 都包含進(jìn)來(lái),記得在 CFLAGS 中使用 "-I" 參數(shù)指定原路徑。某些時(shí)候,可能還需指定 "-std" 參數(shù)。

9.3.1 Flags

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

/*
    #cgo CFLAGS: -g
    #cgo CFLAGS: -I./lib -D__VER__=1
    #cgo LDFLAGS: -lpthread

    #include "test.h"
*/
import "C"

可設(shè)置 GOOS、GOARCH 編譯條件,空格表示 OR,逗號(hào) AND,感嘆號(hào) NOT。

#cgo windows,386 CFLAGS: -I./lib -D__VER__=1

9.3.2 DataType

數(shù)據(jù)類型對(duì)應(yīng)關(guān)系。

可將 cgo 類型轉(zhuǎn)換為標(biāo)準(zhǔn) 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

字符串轉(zhuǎn)換函數(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) // 轉(zhuǎn)換為數(shù)組指針。
    for i := 0; i < 4; i++ {
        p[i] = i + 100
    }

    fmt.Println(p)
}

輸出:

&[100 101 102 103]

9.3.4 Struct/Enum/Union

對(duì) struct、enum 支持良好,union 會(huì)被轉(zhuǎn)換成字節(jié)數(shù)組。如果沒(méi)使用 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

導(dǎo)出 Go 函數(shù)給 C 調(diào)用,須使用 "//export" 標(biāo)記。建議在獨(dú)立頭文件中聲明函數(shù)原型,避免 "duplicate symbol" 錯(cuò)誤。

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 共享庫(kù)。

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

將共享庫(kù)和頭文件拷貝到 Go 項(xiàng)目目錄。

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 查看動(dòng)態(tài)庫(kù)使用狀態(tài)。 靜態(tài)庫(kù)使用方法類似。

9.4 Reflect

沒(méi)有運(yùn)行期類型對(duì)象,實(shí)例也沒(méi)有附加字段用來(lái)表明身份。只有轉(zhuǎn)換成接口時(shí),才會(huì)在其 itab 內(nèi)部存儲(chǔ)與該類型有關(guān)的信息,Reflect 所有操作都依賴于此。

9.4.1 Type

以 struct 為例,可獲取其全部成員字段信息,包括非導(dǎo)出和匿名字段。

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 // 可進(jìn)一步遞歸。
title string

如果是指針,應(yīng)該先使用 Elem 方法獲取目標(biāo)類型,指針本身是沒(méi)有字段成員的。

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 也會(huì)導(dǎo)致方法集存在差異。

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

可直接用名稱或序號(hào)訪問(wèn)字段,包括用多級(jí)序號(hào)訪問(wèn)嵌入字段成員。

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") // 訪問(wèn)嵌入字段。
    fmt.Println(f.Name)
    f, _ = t.FieldByName("Username") // 直接訪問(wèn)嵌入字段成員,會(huì)自動(dòng)深度查找。
    fmt.Println(f.Name)

    f = t.FieldByIndex([]int{0, 1}) // Admin[0] -> User[1] -> age
    fmt.Println(f.Name)
}

輸出:

title
User
Username
age

字段標(biāo)簽可實(shí)現(xiàn)簡(jiǎn)單元數(shù)據(jù)編程,比如標(biāo)記 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)

可從基本類型獲取所對(duì)應(yīng)復(fù)合類型。

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 }

與之對(duì)應(yīng),方法 Elem 可返回復(fù)合類型的基類型。

func main() {
    t := reflect.TypeOf(make(chan int)).Elem()
    fmt.Println(t)
}

方法 Implements 判斷是否實(shí)現(xiàn)了某個(gè)具體接口,AssignableTo、ConvertibleTo 用于賦值和轉(zhuǎn)換判斷。

type Data struct {
}

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

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

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

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

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

type Data struct {
    b byte
    x int32
}

func main() {
    var d Data

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

    f, _ := t.FieldByName("b")
    fmt.Println(f.Type.FieldAlign()) // 字段對(duì)齊。
}

輸出:

8 4
1

9.4.2 Value

Value 和 Type 使用方法類似,包括使用 Elem 獲取指針目標(biāo)對(duì)象。

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()) // 用轉(zhuǎn)換方法獲取字段值
    fmt.Println(v.FieldByName("age").Int()) // 直接訪問(wèn)嵌入字段成員
    fmt.Println(v.FieldByIndex([]int{0, 1}).Int()) // 用多級(jí)序號(hào)訪問(wèn)嵌入字段成員
}

輸出:

NT

23
23

除具體的 Int、String 等轉(zhuǎn)換方法,還可返回 interface{}。只是非導(dǎo)出字段無(wú)法使用,需用 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

當(dāng)然,轉(zhuǎn)換成具體類型不會(huì)引發(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,其他復(fù)合類型 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 某些方法沒(méi)有遵循 "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

將對(duì)象轉(zhuǎn)換為接口,會(huì)發(fā)生復(fù)制行為。該復(fù)制品只讀,無(wú)法被修改。所以要通過(guò)接口改變目標(biāo)對(duì)象狀態(tài),必須是 pointer-interface。

就算是指針,我們依然沒(méi)法將這個(gè)存儲(chǔ)在 data 的指針指向其他對(duì)象,只能透過(guò)它修改目標(biāo)對(duì)象。因?yàn)槟繕?biāo)對(duì)象并沒(méi)有被復(fù)制,被復(fù)制的只是指針。

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

非導(dǎo)出字段無(wú)法直接修改,可改用指針操作。

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 類型,需要還原成接口才能轉(zhuǎn)型。
    fmt.Println(u, p.Interface().(User))
}

輸出:

false
{Tom 88} {Tom 88}

復(fù)合類型修改示例。

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

動(dòng)態(tài)調(diào)用方法很簡(jiǎn)單,按 In 列表準(zhǔn)備好所需參數(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())
    }
}

非導(dǎo)出方法無(wú)法調(diào)用,甚至無(wú)法透過(guò)指針操作,因?yàn)榻涌陬愋托畔⒅袥](méi)有該方法地址。

9.4.4 Make

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

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

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

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

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

    // 傳入的是函數(shù)變量指針,因?yàn)槲覀円獙⒆兞恐赶?swap 函數(shù)。
    fn := reflect.ValueOf(fptr).Elem()

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

    // 修改函數(shù)指針實(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)

    // 按實(shí)際類型使用。
    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{"", "", ""}

原理并不復(fù)雜。

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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)