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
常量值必須是編譯期可確定的數(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)換。
}
更明確的數(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。
引用類型包括 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é)。
不支持隱式類型轉(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")
}
字符串是不可變值類型,內(nèi)部用指針指向 UTF-8 字節(jié)數(shù)組。
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,漢,字,
支持指針類型 *T,指針的指針 *T,以及包含包名前綴的 .T。
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ì)象被回收。
可將類型分為命名和未命名兩大類。命名類型包括 bool、int、string 等,而 array、slice、map 等和具體元素類型、長(zhǎng)度等有關(guān),屬于未命名類型。
具有相同聲明的未命名類型被視為同一類型。
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
語(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
全部運(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
初始化復(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
很特別的寫(xiě)法:
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"。
支持三種循環(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
類似迭代器操作,返回 (索引, 值) 或 (鍵, 值)。
可忽略不想要的返回值,或用 "_" 這個(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。
分支表達(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")
}
支持在函數(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)
}
不支持 嵌套 (nested)、重載 (overload) 和 默認(rèn)參數(shù) (default parameter)。
使用關(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ò)誤。
變參本質(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...))
}
不能用容器對(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
}
匿名函數(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 ... }
關(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
沒(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。
和以往認(rèn)知的數(shù)組有很大不同。
可用復(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
需要說(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
};
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
所謂 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]
向 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
函數(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)存。
引用類型,哈希表。鍵必須是支持相等運(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]
值類型,賦值和傳參會(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
匿名字段不過(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)
面向?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
方法總是綁定對(duì)象實(shí)例,并隱式將實(shí)例作為第一實(shí)參 (receiver)。
沒(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()
}
可以像字段成員那樣訪問(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}
每個(gè)類型都有與之關(guān)聯(lián)的方法集,這會(huì)影響到接口實(shí)現(xiàn)規(guī)則。
用實(shí)例 value 和 pointer 調(diào)用方法 (含匿名字段) 不受方法集約束,編譯器總是查找全部方法,并自動(dòng)轉(zhuǎn)換 receiver 實(shí)參。
根據(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
}
接口是一個(gè)或多個(gè)方法簽名的集合,任何類型的方法集中只要擁有與之對(duì)應(yīng)的全部方法,就表示它 "實(shí)現(xiàn)" 了該接口,無(wú)須在該類型上顯式添加接口聲明。
所謂對(duì)應(yīng)方法,是指有相同名稱、參數(shù)列表 (不包括參數(shù)名) 以及返回值。當(dāng)然,該類型還可以有其他方法。
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
接口對(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
利用類型推斷,可判斷接口對(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())
}
讓編譯器檢查,以確保某個(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()
}
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
引用類型 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
可以將 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
如果需要同時(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 需要小心,避免形成洪水。
用簡(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)
}
編譯工具對(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ù)。
編碼:源碼文件必須是 UTF-8 格式,否則會(huì)導(dǎo)致編譯器出錯(cuò)。結(jié)束:語(yǔ)句以 ";" 結(jié)束,多數(shù)時(shí)候可以省略。注釋:支持 "//"、"/**/" 兩種注釋方式,不能嵌套。命名:采用 camelCasing 風(fēng)格,不建議使用下劃線。
所有代碼都必須組織在 package 中。
說(shuō)明:os.Args 返回命令行參數(shù),os.Exit 終止進(jìn)程。
要獲取正確的可執(zhí)行文件路徑,可用 filepath.Abs(exec.LookPath(os.Args[0]))。
包中成員以名稱首字母大小寫(xiě)決定訪問(wèn)權(quán)限。
該規(guī)則適用于全局變量、全局常量、類型、結(jié)構(gòu)字段、函數(shù)、方法等。
使用包成員前,必須先用 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 有效。
可通過(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
初始化函數(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è)置。
擴(kuò)展工具 godoc 能自動(dòng)提取注釋生成幫助文檔。
只要 Example 測(cè)試函數(shù)名稱符合以下規(guī)范即可。
說(shuō)明:使用 suffix 作為示例名稱,其首字母必須小寫(xiě)。如果文件中僅有一個(gè) Example 函數(shù),且調(diào)用了該文件中的其他成員,那么示例會(huì)顯示整個(gè)文件內(nèi)容,而不僅僅是測(cè)試函數(shù)自己。
非測(cè)試源碼文件中以 BUG(author) 開(kāi)始的注釋,會(huì)在幫助文檔 Bugs 節(jié)點(diǎn)中顯示。
// BUG(yuhen): memory leak.
了解對(duì)象內(nèi)存布局,有助于理解值傳遞、引用傳遞等概念。
string
struct
slice
interface
new
make
對(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ú)法被回收。
通過(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ù)。
可使用 #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
數(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)
}
字符串轉(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]
對(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]
導(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();
}
在 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ù)使用方法類似。
沒(méi)有運(yùn)行期類型對(duì)象,實(shí)例也沒(méi)有附加字段用來(lái)表明身份。只有轉(zhuǎn)換成接口時(shí),才會(huì)在其 itab 內(nèi)部存儲(chǔ)與該類型有關(guān)的信息,Reflect 所有操作都依賴于此。
以 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
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]
可獲取方法參數(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)有該方法地址。
利用 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ù)雜。
如此,在共享算法的前提下,無(wú)須用 interface{},無(wú)須做類型轉(zhuǎn)換,頗有泛型的效果。
更多建議: