Go 是靜態(tài)類型語言,不能在運行期改變變量類型。使用關鍵字 var 定義變量,自動初始化為零值。如果提供初始化值,可省略變量類型,由編譯器自動推斷。
var x int
var f float32 = 1.6
var s = "abc"
在函數(shù)內(nèi)部,可用更簡略的 ":=" 方式定義變量。
func main() {
x := 123 // 注意檢查,是定義新局部變量,還是修改全局變量。該方式容易造成錯誤。
}
可一次定義多個變量。
var x, y, z int
var s, n = "abc", 123
var (
a int
b float32
)
func main() {
n, s := 0x1234, "Hello, World!"
println(x, s, n)
}
多變量賦值時,先計算所有相關值,然后再從左到右依次賦值。
data, i := [3]int{0, 1, 2}, 0
i, data[i] = 2, 100 // (i = 0) -> (i = 2), (data[0] = 100)
特殊只寫變量 "_",用于忽略值占位。
func test() (int, string) {
return 1, "abc"
}
func main() {
_, s := test()
println(s)
}
編譯器會將未使用的局部變量當做錯誤。
var s string // 全局變量沒問題。
func main() {
i := 0 // Error: i declared and not used。(可使用 "_ = i" 規(guī)避)
}
注意重新賦值與定義新同名變量的區(qū)別。
s := "abc"
println(&s)
s, y := "hello", 20 // 重新賦值: 與前 s 在同一層次的代碼塊中,且有新的變量被定義。
println(&s, y) // 通常函數(shù)多返回值 err 會被重復使用。
{
s, z := 1000, 30 // 定義新同名變量: 不在同一層次代碼塊。
println(&s, z)
}
輸出:
0x2210230f30
0x2210230f30 20
0x2210230f18 30
常量值必須是編譯期可確定的數(shù)字、字符串、布爾值。
const x, y int = 1, 2 // 多常量初始化
const s = "Hello, World!" // 類型推斷
const ( // 常量組
a, b = 10, 100
c bool = false
)
func main() {
const x = "xxx" // 未使用局部常量不會引發(fā)編譯錯誤。
}
不支持 1UL、2LL 這樣的類型后綴。
在常量組中,如不提供類型和初始化值,那么視作與上一常量相同。
const (
s = "abc"
x // x = "abc"
)
常量值還可以是 len、cap、unsafe.Sizeof 等編譯期可確定結果的函數(shù)返回值。
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(b)
)
如果常量類型足以存儲初始化值,那么不會引發(fā)溢出錯誤。
const (
a byte = 100 // int to byte
b int = 1e20 // float64 to int, overflows
)
枚舉
關鍵字 iota 定義常量組中從0開始按行計數(shù)的自增枚舉值。
const (
Sunday = iota // 0
Monday // 1,通常省略后續(xù)行表達式。
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
const (
_ = iota // iota = 0
KB int64 = 1 << (10 * iota) // iota = 1
MB // 與 KB 表達式相同,但 iota = 2
GB
TB
)
在同一常量組中,可以提供多個 iota,它們各自增長。
const (
A, B = iota, iota << 10 // 0, 0 << 10
C, D // 1, 1 << 10
)
如果 iota 自增被打斷,須顯式恢復。
const (
A = iota // 0
B // 1
C = "c" // c
D // c,與上一行相同。
E = iota // 4,顯式恢復。注意計數(shù)包含了 C、D 兩行。
F // 5
)
可通過自定義類型來實現(xiàn)枚舉類型限制。
type Color int
const (
Black Color = iota
Red
Blue
)
func test(c Color) {}
func main() {
c := Black
test(c)
x := 1
test(x) // Error: cannot use x (type int) as type Color in function argument
test(1) // 常量會被編譯器自動轉換。
}
更明確的數(shù)字類型命名,支持 Unicode,支持常用數(shù)據(jù)結構。
支持八進制、十六進制,以及科學記數(shù)法。標準庫 math 定義了各數(shù)字類型取值范圍。
a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
空指針值 nil,而非 C/C++ NULL。
引用類型包括 slice、map 和 channel。它們有復雜的內(nèi)部結構,除了申請內(nèi)存外,還需要初始化相關屬性。
內(nèi)置函數(shù) new 計算類型大小,為其分配零值內(nèi)存,返回指針。而 make 會被編譯器翻譯成具體的創(chuàng)建函數(shù),由其分配內(nèi)存和初始化成員結構,返回對象而非指針。
a := []int{0, 0, 0} // 提供初始化表達式。
a[1] = 10
b := make([]int, 3) // makeslice
b[1] = 10
c := new([]int)
c[1] = 10 // Error: invalid operation: c[1] (index of type *[]int)
有關引用類型具體的內(nèi)存布局,可參考后續(xù)章節(jié)。
不支持隱式類型轉換,即便是從窄向?qū)掁D換也不行。
var b byte = 100
// var n int = b // Error: cannot use b (type byte) as type int in assignment
var n int = int(b) // 顯式轉換
使用括號避免優(yōu)先級錯誤。
*Point(p) // 相當于 *(Point(p))
(*Point)(p)
<-chan int(c) // 相當于 <-(chan int(c))
(<-chan int)(c)
同樣不能將其他類型當 bool 值使用。
a := 100
if a { // Error: non-bool a (type int) used as if condition
println("true")
}
字符串是不可變值類型,內(nèi)部用指針指向 UTF-8 字節(jié)數(shù)組。
runtime.h
struct String
{
byte* str;
intgo len;
};
使用索引號訪問字符 (byte)。
s := "abc"
println(s[0] == '\x61', s[1] == 'b', s[2] == 0x63)
輸出:
true true true
使用 "`" 定義不做轉義處理的原始字符串,支持跨行。
s := `a
b\r\n\x00
c`
println(s)
輸出:
a
b\r\n\x00
c
連接跨行字符串時,"+" 必須在上一行末尾,否則導致編譯錯誤。
s := "Hello, " +
"World!"
s2 := "Hello, "
+ "World!" // Error: invalid operation: + untyped string
支持用兩個索引號返回子串。子串依然指向原字節(jié)數(shù)組,僅修改了指針和長度屬性。
s := "Hello, World!"
s1 := s[:5] // Hello
s2 := s[7:] // World!
s3 := s[1:5] // ello
單引號字符常量表示 Unicode Code Point,支持 \uFFFF、\U7FFFFFFF、\xFF 格式。對應 rune 類型,UCS-4。
func main() {
fmt.Printf("%T\n", 'a')
var c1, c2 rune = '\u6211', '們'
println(c1 == '我', string(c2) == "\xe4\xbb\xac")
}
輸出:
int32 // rune 是 int32 的別名
true true
要修改字符串,可先將其轉換成 []rune 或 []byte,完成后再轉換為 string。無論哪種轉換,都會重新分配內(nèi)存,并復制字節(jié)數(shù)組。
func main() {
s := "abcd"
bs := []byte(s)
bs[1] = 'B'
println(string(bs))
u := "電腦"
us := []rune(u)
us[1] = '話'
println(string(us))
}
輸出:
aBcd
電話
用 for 循環(huán)遍歷字符串時,也有 byte 和 rune 兩種方式。
func main() {
s := "abc漢字"
for i := 0; i < len(s); i++ { // byte
fmt.Printf("%c,", s[i])
}
fmt.Println()
for _, r := range s { // rune
fmt.Printf("%c,", r)
}
}
輸出:
a,b,c,,±,,,,,
a,b,c,漢,字,
支持指針類型 *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) // 直接用指針訪問目標對象成員,無須轉換。
}
輸出:
0x2101ef018, 1234
不能對指針做加減法等運算。
x := 1234
p := &x
p++ // Error: invalid operation: p += 1 (mismatched types *int and int)
可以在 unsafe.Pointer 和任意類型指針間進行轉換。
func main() {
x := 0x12345678
p := unsafe.Pointer(&x) // *int -> Pointer
n := (*[4]byte)(p) // Pointer -> *[4]byte
for i := 0; i < len(n); i++ {
fmt.Printf("%X ", n[i])
}
}
輸出:
78 56 34 12
返回局部變量指針是安全的,編譯器會根據(jù)需要將其分配在 GC Heap 上。
func test() *int {
x := 100
return &x // 在堆上分配 x 內(nèi)存。但在內(nèi)聯(lián)時,也可能直接分配在目標棧。
}
將 Pointer 轉換成 uintptr,可變相實現(xiàn)指針運算。
func main() {
d := struct {
s string
x int
}{"abc", 100}
p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
p += unsafe.Offsetof(d.x) // uintptr + offset
p2 := unsafe.Pointer(p) // uintptr -> Pointer
px := (*int)(p2) // Pointer -> *int
*px = 200 // d.x = 200
fmt.Printf("%#v\n", d)
}
輸出:
struct { s string; x int }{s:"abc", x:200}
注意:GC 把 uintptr 當成普通整數(shù)對象,它無法阻止 "關聯(lián)" 對象被回收。
可將類型分為命名和未命名兩大類。命名類型包括 bool、int、string 等,而 array、slice、map 等和具體元素類型、長度等有關,屬于未命名類型。
具有相同聲明的未命名類型被視為同一類型。
var a struct { x int `a` }
var b struct { x int `ab` }
// cannot use a (type struct { x int "a" }) as type struct { x int "ab" } in assignment
b = a
可用 type 在全局或函數(shù)內(nèi)定義新類型。
func main() {
type bigint int64
var x bigint = 100
println(x)
}
新類型不是原類型的別名,除擁有相同數(shù)據(jù)存儲結構外,它們之間沒有任何關系,不會持有原類型任何信息。除非目標類型是未命名類型,否則必須顯式轉換。
x := 1234
var b bigint = bigint(x) // 必須顯式轉換,除非是常量。
var b2 int64 = int64(b)
var s myslice = []int{1, 2, 3} // 未命名類型,隱式轉換。
var s2 []int = s
語言設計簡練,保留字不多。
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
全部運算符、分隔符,以及其他符號。
運算符結合律全部從左到右。
簡單位運算演示。
0110 & 1011 = 0010 AND 都為1。
0110 | 1011 = 1111 OR 至少一個為1。
0110 ^ 1011 = 1101 XOR 只能一個為1。
0110 &^ 1011 = 0100 AND NOT 清除標志位。
標志位操作。
a := 0
a |= 1 << 2 // 0000100: 在 bit2 設置標志位。
a |= 1 << 6 // 1000100: 在 bit6 設置標志位
a = a &^ (1 << 6) // 0000100: 清除 bit6 標志位。
不支持運算符重載。尤其需要注意,"++"、"--" 是語句而非表達式。
n := 0
p := &n
// b := n++ // syntax error
// if n++ == 1 {} // syntax error
// ++n // syntax error
n++
*p++ // (*p)++
沒有 "~",取反運算也用 "^"。
x := 1
x, ^x // 0001, -0010
初始化復合對象,必須使用類型標簽,且左大括號必須在類型尾部。
// var a struct { x int } = { 100 } // syntax error
// var b []int = { 1, 2, 3 } // syntax error
// c := struct {x int; y string} // syntax error: unexpected semicolon or newline
// {
// }
var a = struct{ x int }{100}
var b = []int{1, 2, 3}
初始化值以 "," 分隔??梢苑侄嘈?,但最后一行必須以 "," 或 "}" 結尾。
a := []int{
1,
2 // Error: need trailing comma before newline in composite literal
}
a := []int{
1,
2, // ok
}
b := []int{
1,
2 } // ok
很特別的寫法:
x := 0
// if x > 10 // Error: missing condition in if statement
// {
// }
if n := "abc"; x > 0 { // 初始化語句未必就是定義變量,比如 println("init") 也是可以的。
println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括號位置。
println(n[1])
} else {
println(n[0])
}
不支持三元操作符 "a > b a : b"。
支持三種循環(huán)方式,包括類 while 語法。
s := "abc"
for i, n := 0, len(s); i < n; i++ { // 常見的 for 循環(huán),支持初始化語句。
println(s[i])
}
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
不要期望編譯器能理解你的想法,在初始化語句中計算出全部結果是個好主意。
func length(s string) int {
println("call length.")
return len(s)
}
func main() {
s := "abcd"
for i, n := 0, length(s); i < n; i++ { // 避免多次調(diào)用 length 函數(shù)。
println(i, s[i])
}
}
輸出:
call length.
0 97
1 98
2 99
3 100
類似迭代器操作,返回 (索引, 值) 或 (鍵, 值)。
可忽略不想要的返回值,或用 "_" 這個特殊變量。
s := "abc"
for i := range s { // 忽略 2nd value,支持 string/array/slice/map。
println(s[i])
}
for _, c := range s { // 忽略 index。
println(c)
}
for range s { // 忽略全部返回值,僅迭代。
...
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m { // 返回 (key, value)。
println(k, v)
}
注意,range 會復制對象。
a := [3]int{0, 1, 2}
for i, v := range a { // index、value 都是從復制品中取出。
if i == 0 { // 在修改前,我們先修改原數(shù)組。
a[1], a[2] = 999, 999
fmt.Println(a) // 確認修改有效,輸出 [0, 999, 999]。
}
a[i] = v + 100 // 使用復制品中取出的 value 修改原數(shù)組。
}
fmt.Println(a) // 輸出 [100, 101, 102]。
建議改用引用類型,其底層數(shù)據(jù)不會被復制。
s := []int{1, 2, 3, 4, 5}
for i, v := range s { // 復制 struct slice { pointer, len, cap }。
if i == 0 {
s = s[:3] // 對 slice 的修改,不會影響 range。
s[2] = 100 // 對底層數(shù)據(jù)的修改。
}
println(i, v)
}
輸出:
0 1
1 2
2 100
3 4
4 5
另外兩種引用類型 map、channel 是指針包裝,而不像 slice 是 struct。
分支表達式可以是任意類型,不限于常量??墒÷?break,默認自動終止。
x := []int{1, 2, 3}
i := 2
switch i {
case x[1]:
println("a")
case 1, 3:
println("b")
default:
println("c")
}
輸出:
a
如需要繼續(xù)下一分支,可使用 fallthrough,但不再判斷條件。
x := 10
switch x {
case 10:
println("a")
fallthrough
case 0:
println("b")
}
輸出:
a
b
省略條件表達式,可當 if...else if...else 使用。
switch {
case x[1] > 0:
println("a")
case x[1] < 0:
println("b")
default:
println("c")
}
switch i := x[2]; { // 帶初始化語句
case i > 0:
println("a")
case i < 0:
println("b")
default:
println("c")
}
支持在函數(shù)內(nèi) goto 跳轉。標簽名區(qū)分大小寫,未使用標簽引發(fā)錯誤。
func main() {
var i int
for {
println(i)
i++
if i > 2 { goto BREAK }
}
BREAK:
println("break")
EXIT: // Error: label EXIT defined and not used
}
配合標簽,break 和 continue 可在多級嵌套循環(huán)中跳出。
func main() {
L1:
for x := 0; x < 3; x++ {
L2:
for y := 0; y < 5; y++ {
if y > 2 { continue L2 }
if x > 1 { break L1 }
print(x, ":", y, " ")
}
println()
}
}
輸出:
0:0 0:1 0:2
1:0 1:1 1:2
附:break 可用于 for、switch、select,而 continue 僅能用于 for 循環(huán)。
x := 100
switch {
case x >= 0:
if x == 0 { break }
println(x)
}
不支持 嵌套 (nested)、重載 (overload) 和 默認參數(shù) (default parameter)。
使用關鍵字 func 定義函數(shù),左大括號依舊不能另起一行。
func test(x, y int, s string) (int, string) { // 類型相同的相鄰參數(shù)可合并。
n := x + y // 多返回值必須用括號。
return n, fmt.Sprintf(s, n)
}
函數(shù)是第一類對象,可作為參數(shù)傳遞。建議將復雜簽名定義為函數(shù)類型,以便于閱讀。
func test(fn func() int) int {
return fn()
}
type FormatFunc func(s string, x, y int) string // 定義函數(shù)類型。
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接將匿名函數(shù)當參數(shù)。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
有返回值的函數(shù),必須有明確的終止語句,否則會引發(fā)編譯錯誤。
變參本質(zhì)上就是 slice。只能有一個,且必須是最后一個。
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
println(test("sum: %d", 1, 2, 3))
}
使用 slice 對象做變參時,必須展開。
func main() {
s := []int{1, 2, 3}
println(test("sum: %d", s...))
}
不能用容器對象接收多返回值。只能用多個變量,或 "_" 忽略。
func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
多返回值可直接作為其他函數(shù)調(diào)用實參。
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
命名返回參數(shù)可看做與形參類似的局部變量,最后由 return 隱式返回。
func add(x, y int) (z int) {
z = x + y
return
}
func main() {
println(add(1, 2))
}
命名返回參數(shù)可被同名局部變量遮蔽,此時需要顯式返回。
func add(x, y int) (z int) {
{ // 不能在一個級別,引發(fā) "z redeclared in this block" 錯誤。
var z = x + y
// return // Error: z is shadowed during return
return z // 必須顯式返回。
}
}
命名返回參數(shù)允許 defer 延遲調(diào)用通過閉包讀取和修改。
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2)) // 輸出: 103
}
顯式 return 返回前,會先修改命名返回參數(shù)。
func add(x, y int) (z int) {
defer func() {
println(z) // 輸出: 203
}()
z = x + y
return z + 200 // 執(zhí)行順序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
println(add(1, 2)) // 輸出: 203
}
匿名函數(shù)可賦值給變量,做為結構字段,或者在 channel 里傳送。
// --- function variable ---
fn := func() { println("Hello, World!") }
fn()
// --- function collection ---
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100))
// --- function as field ---
d := struct {
fn func() string
}{
fn: func() string { return "Hello, World!" },
}
println(d.fn())
// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)())
閉包復制的是原對象指針,這就很容易解釋延遲引用現(xiàn)象。
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
輸出:
x (0x2101ef018) = 100
x (0x2101ef018) = 100
在匯編層面,test 實際返回的是 FuncVal 對象,其中包含了匿名函數(shù)地址、閉包對象指針。當調(diào)用匿名函數(shù)時,只需以某個寄存器傳遞該對象即可。
FuncVal { func_address, closure_var_pointer ... }
關鍵字 defer 用于注冊延遲調(diào)用。這些調(diào)用直到 ret 前才被執(zhí)行,通常用于釋放資源或錯誤處理。
func test() error {
f, err := os.Create("test.txt")
if err != nil { return err }
defer f.Close() // 注冊調(diào)用,而不是注冊函數(shù)。必須提供參數(shù),哪怕為空。
f.WriteString("Hello, World!")
return nil
}
多個 defer 注冊,按 FILO 次序執(zhí)行。哪怕函數(shù)或某個延遲調(diào)用發(fā)生錯誤,這些調(diào)用依舊會被執(zhí)行。
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 異常未被捕獲,逐步往外傳遞,最終終止進程。
}()
defer println("c")
}
func main() {
test(0)
}
輸出:
c
b
a
panic: runtime error: integer divide by zero
延遲調(diào)用參數(shù)在注冊時求值或復制,可用指針或閉包 "延遲" 讀取。
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 閉包引用
}(x) // x 被復制
x += 10
y += 100
println("x =", x, "y =", y)
}
輸出:
x = 20 y = 120
defer: 10 120
濫用 defer 可能會導致性能問題,尤其是在一個 "大循環(huán)" 里。
var lock sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func BenchmarkTest(b *testing.B) {
for i := 0; i < b.N; i++ {
test()
}
}
func BenchmarkTestDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
testdefer()
}
}
輸出:
BenchmarkTest" 50000000 43 ns/op
BenchmarkTestDefer 20000000 128 ns/op
沒有結構化異常,使用 panic 拋出錯誤,recover 捕獲錯誤。
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 將 interface{} 轉型為具體類型。
}
}()
panic("panic error!")
}
由于 panic、recover 參數(shù)類型為 interface{},因此可拋出任何類型對象。
func panic(v interface{})
func recover() interface{}
延遲調(diào)用中引發(fā)的錯誤,可被后續(xù)延遲調(diào)用捕獲,但僅最后一個錯誤可被捕獲。
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
輸出:
defer panic
捕獲函數(shù) recover 只有在延遲調(diào)用內(nèi)直接調(diào)用才會終止錯誤,否則總是返回 nil。任何未捕獲的錯誤都會沿調(diào)用堆棧向外傳遞。
func test() {
defer recover() // 無效!
defer fmt.Println(recover()) // 無效!
defer func() {
func() {
println("defer inner")
recover() // 無效!
}()
}()
panic("test panic")
}
func main() {
test()
}
輸出:
defer inner
<nil>
panic: test panic
使用延遲匿名函數(shù)或下面這樣都是有效的。
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
如果需要保護代碼片段,可將代碼塊重構成匿名函數(shù),如此可確保后續(xù)代碼被執(zhí)行。
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil { z = 0 }
}()
z = x / y
return
}()
println("x / y =", z)
}
除用 panic 引發(fā)中斷性錯誤外,還可返回 error 類型錯誤對象來表示函數(shù)調(diào)用狀態(tài)。
type error interface {
Error() string
}
標準庫 errors.New 和 fmt.Errorf 函數(shù)用于創(chuàng)建實現(xiàn) error 接口的錯誤對象。通過判斷錯誤對象實例來確定具體錯誤類型。
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 { return 0, ErrDivByZero }
return x / y, nil
}
func main() {
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
如何區(qū)別使用 panic 和 error 兩種方式?慣例是:導致關鍵流程出現(xiàn)不可修復性錯誤的使用 panic,其他使用 error。
和以往認知的數(shù)組有很大不同。
可用復合語句初始化。
a := [3]int{1, 2} // 未初始化元素值為0。
b := [...]int{1, 2, 3, 4} // 通過初始化值確定數(shù)組長度。
c := [5]int{2: 100, 4:200} // 使用索引號初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素類型。
{"user2", 20}, // 別忘了最后一行的逗號。
}
支持多維數(shù)組。
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 緯度不能用 "..."。
值拷貝行為會造成性能問題,通常會建議使用 slice,或數(shù)組指針。
func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
}
func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)
test(a)
fmt.Println(a)
}
輸出:
a: 0x2101f9150
x: 0x2101f9170
[0 0]
內(nèi)置函數(shù) len 和 cap 都返回數(shù)組長度 (元素數(shù)量)。
a := [2]int{}
println(len(a), cap(a)) // 2, 2
需要說明,slice 并不是數(shù)組或數(shù)組指針。它通過內(nèi)部指針和相關屬性引用數(shù)組片段,以實現(xiàn)變長方案。
runtime.h
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]
創(chuàng)建表達式使用的是元素索引號,而非數(shù)量。
讀寫操作實際目標是底層數(shù)組,只需注意索引號的差別。
data := [...]int{0, 1, 2, 3, 4, 5}
s := data[2:4]
s[0] += 100
s[1] += 200
fmt.Println(s)
fmt.Println(data)
輸出:
[102 203]
[0 1 102 203 4 5]
可直接創(chuàng)建 slice 對象,自動分配底層數(shù)組。
s1 := []int{0, 1, 2, 3, 8: 100} // 通過初始化表達式構造,可使用索引號。
fmt.Println(s1, len(s1), cap(s1))
s2 := make([]int, 6, 8) // 使用 make 創(chuàng)建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))
s3 := make([]int, 6) // 省略 cap,相當于 cap = len。
fmt.Println(s3, len(s3), cap(s3))
輸出:
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6
使用 make 動態(tài)創(chuàng)建 slice,避免了數(shù)組必須用常量做長度的麻煩。還可用指針直接訪問底層數(shù)組,退化成普通數(shù)組操作。
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 獲取底層數(shù)組元素指針。
*p += 100
fmt.Println(s)
輸出:
[0 1 102 3]
至于 [][]T,是指元素類型為 []T 。
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
可直接修改 struct array/slice 成員。
d := [5]struct {
x int
}{}
s := d[:]
d[1].x = 10
s[2].x = 20
fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])
輸出:
[{0} {10} {20} {0} {0}]
0x20819c180, 0x20819c180
所謂 reslice,是基于已有 slice 創(chuàng)建新 slice 對象,以便在 cap 允許范圍內(nèi)調(diào)整屬性。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error
新對象依舊指向原底層數(shù)組。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s1[2] = 100
s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200
fmt.Println(s)
輸出:
[0 1 2 3 100 5 6 200 8 9]
向 slice 尾部添加數(shù)據(jù),返回新的 slice 對象。
s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)
s2 := append(s, 1)
fmt.Printf("%p\n", &s2)
fmt.Println(s, s2)
輸出:
0x210230000
0x210230040
[] [1]
簡單點說,就是在 array[slice.high] 寫數(shù)據(jù)。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[:3]
s2 := append(s, 100, 200) // 添加多個值。
fmt.Println(data)
fmt.Println(s)
fmt.Println(s2)
輸出:
[0 1 2 100 200 5 6 7 8 9]
[0 1 2]
[0 1 2 100 200]
一旦超出原 slice.cap 限制,就會重新分配底層數(shù)組,即便原數(shù)組并未填滿。
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
s = append(s, 100, 200) // 一次 append 兩個值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底層數(shù)組,與原數(shù)組無關。
fmt.Println(&s[0], &data[0]) // 比對底層數(shù)組起始指針。
輸出:
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0x20819c180 0x20817c0c0
從輸出結果可以看出,append 后的 s 重新分配了底層數(shù)組,并復制數(shù)據(jù)。如果只追加一個值,則不會超過 s.cap 限制,也就不會重新分配。
通常以2倍容量重新分配底層數(shù)組。在大批量添加數(shù)據(jù)時,建議一次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復制開銷?;虺跏蓟銐蜷L的 len 屬性,改用索引號進行操作。及時釋放不再使用的 slice 對象,避免持有過期數(shù)組,造成 GC 無法回收。
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
輸出:
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
函數(shù) copy 在兩個 slice 間復制數(shù)據(jù),復制長度以 len 小的為準。兩個 slice 可指向同一底層數(shù)組,允許元素區(qū)間重疊。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[8:]
s2 := data[:5]
copy(s2, s) // dst:s2, src:s
fmt.Println(s2)
fmt.Println(data)
輸出:
[8 9 2 3 4]
[8 9 2 3 4 5 6 7 8 9]
應及時將所需數(shù)據(jù) copy 到較小的 slice,以便釋放超大號底層數(shù)組內(nèi)存。
引用類型,哈希表。鍵必須是支持相等運算符 (==、!=) 類型,比如 number、string、pointer、array、struct,以及對應的 interface。值可以是任意類型,沒有限制。
m := map[int]struct {
name string
age int
}{
1: {"user1", 10}, // 可省略元素類型。
2: {"user2", 20},
}
println(m[1].name)
預先給 make 函數(shù)一個合理元素數(shù)量參數(shù),有助于提升性能。因為事先申請一大塊內(nèi)存,可避免后續(xù)操作時頻繁擴張。
m := make(map[string]int, 1000)
常見操作:
m := map[string]int{
"a": 1,
}
if v, ok := m["a"]; ok { // 判斷 key 是否存在。
println(v)
}
println(m["c"]) // 對于不存在的 key,直接返回 \0,不會出錯。
m["b"] = 2 // 新增或修改。
delete(m, "c") // 刪除。如果 key 不存在,不會出錯。
println(len(m)) // 獲取鍵值對數(shù)量。cap 無效。
for k, v := range m { // 迭代,可僅返回 key。隨機順序返回,每次都不相同。
println(k, v)
}
不能保證迭代返回次序,通常是隨機結果,具體和版本實現(xiàn)有關。
從 map 中取回的是一個 value 臨時復制品,對其成員的修改是沒有任何意義的。
type user struct{ name string }
m := map[int]user{ // 當 map 因擴張而重新哈希時,各鍵值項存儲位置都會發(fā)生改變。 因此,map
1: {"user1"}, // 被設計成 not addressable。 類似 m[1].name 這種期望透過原 value
} // 指針修改成員的行為自然會被禁止。
m[1].name = "Tom" // Error: cannot assign to m[1].name
正確做法是完整替換 value 或使用指針。
u := m[1]
u.name = "Tom"
m[1] = u // 替換 value。
m2 := map[int]*user{
1: &user{"user1"},
}
m2[1].name = "Jack" // 返回的是指針復制品。透過指針修改原對象是允許的。
可以在迭代時安全刪除鍵值。但如果期間有新增操作,那么就不知道會有什么意外了。
for i := 0; i < 5; i++ {
m := map[int]string{
0: "a", 1: "a", 2: "a", 3: "a", 4: "a",
5: "a", 6: "a", 7: "a", 8: "a", 9: "a",
}
for k := range m {
m[k+k] = "x"
delete(m, k)
}
fmt.Println(m)
}
輸出:
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]
值類型,賦值和傳參會復制全部內(nèi)容??捎?"_" 定義補位字段,支持指向自身類型的指針成員。
type Node struct {
_ int
id int
data *byte
next *Node
}
func main() {
n1 := Node{
id: 1,
data: nil,
}
n2 := Node{
id: 2,
data: nil,
next: &n1,
}
}
順序初始化必須包含全部字段,否則會出錯。
type User struct {
name string
age int
}
u1 := User{"Tom", 20}
u2 := User{"Tom"} // Error: too few values in struct initializer
支持匿名結構,可用作結構成員或定義變量。
type File struct {
name string
size int
attr struct {
perm int
owner int
}
}
f := File{
name: "test.txt",
size: 1025,
// attr: {0755, 1}, // Error: missing type in composite literal
}
f.attr.owner = 1
f.attr.perm = 0755
var attr = struct {
perm int
owner int
}{2, 0755}
f.attr = attr
支持 "=="、"!=" 相等操作符,可用作 map 鍵類型。
type User struct {
id int
name string
}
m := map[User]int{
User{1, "Tom"}: 100,
}
可定義字段標簽,用反射讀取。標簽是類型的組成部分。
var u1 struct { name string "username" }
var u2 struct { name string }
u2 = u1 // Error: cannot use u1 (type struct { name string "username" }) as
// type struct { name string } in assignment
空結構 "節(jié)省" 內(nèi)存,比如用來實現(xiàn) set 數(shù)據(jù)結構,或者實現(xiàn)沒有 "狀態(tài)" 只有方法的 "靜態(tài)類"。
var null struct{}
set := make(map[string]struct{})
set["a"] = null
匿名字段不過是一種語法糖,從根本上說,就是一個與成員類型同名 (不含包名) 的字段。被匿名嵌入的可以是任何類型,當然也包括指針。
type User struct {
name string
}
type Manager struct {
User
title string
}
m := Manager{
User: User{"Tom"}, // 匿名字段的顯式字段名,和類型名相同。
title: "Administrator",
}
可以像普通字段那樣訪問匿名字段成員,編譯器從外向內(nèi)逐級查找所有層次的匿名字段,直到發(fā)現(xiàn)目標或出錯。
type Resource struct {
id int
}
type User struct {
Resource
name string
}
type Manager struct {
User
title string
}
var m Manager
m.id = 1
m.name = "Jack"
m.title = "Administrator"
外層同名字段會遮蔽嵌入字段成員,相同層次的同名字段也會讓編譯器無所適從。解決方法是使用顯式字段名。
type Resource struct {
id int
name string
}
type Classify struct {
id int
}
type User struct {
Resource // Resource.id 與 Classify.id 處于同一層次。
Classify
name string // 遮蔽 Resource.name。
}
u := User{
Resource{1, "people"},
Classify{100},
"Jack",
}
println(u.name) // User.name: Jack
println(u.Resource.name) // people
// println(u.id) // Error: ambiguous selector u.id
println(u.Classify.id) // 100
不能同時嵌入某一類型和其指針類型,因為它們名字相同。
type Resource struct {
id int
}
type User struct {
*Resource
// Resource // Error: duplicate field Resource
name string
}
u := User{
&Resource{1},
"Administrator",
}
println(u.id)
println(u.Resource.id)
面向?qū)ο笕筇卣骼?,Go 僅支持封裝,盡管匿名字段的內(nèi)存布局和行為類似繼承。沒有 class 關鍵字,沒有繼承、多態(tài)等等。
type User struct {
id int
name string
}
type Manager struct {
User
title string
}
m := Manager{User{1, "Tom"}, "Administrator"}
// var u User = m // Error: cannot use m (type Manager) as type User in assignment
// 沒有繼承,自然也不會有多態(tài)。
var u User = m.User // 同類型拷貝。
內(nèi)存布局和 C struct 相同,沒有任何附加的 object 信息。
可用 unsafe 包相關函數(shù)輸出內(nèi)存地址信息。
m : 0x2102271b0, size: 40, align: 8
m.id : 0x2102271b0, offset: 0
m.name : 0x2102271b8, offset: 8
m.title: 0x2102271c8, offset: 24
方法總是綁定對象實例,并隱式將實例作為第一實參 (receiver)。
沒有構造和析構方法,通常用簡單工廠模式返回對象實例。
type Queue struct {
elements []interface{}
}
func NewQueue() *Queue { // 創(chuàng)建對象實例。
return &Queue{make([]interface{}, 10)}
}
func (*Queue) Push(e interface{}) error { // 省略 receiver 參數(shù)名。
panic("not implemented")
}
// func (Queue) Push(e int) error { // Error: method redeclared: Queue.Push
// panic("not implemented")
// }
func (self *Queue) length() int { // receiver 參數(shù)名可以是 self、this 或其他。
return len(self.elements)
}
方法不過是一種特殊的函數(shù),只需將其還原,就知道 receiver T 和 *T 的差別。
type Data struct{
x int
}
func (self Data) ValueTest() { // func ValueTest(self Data);
fmt.Printf("Value: %p\n", &self)
}
func (self *Data) PointerTest() { // func PointerTest(self *Data);
fmt.Printf("Pointer: %p\n", self)
}
func main() {
d := Data{}
p := &d
fmt.Printf("Data: %p\n", p)
d.ValueTest() // ValueTest(d)
d.PointerTest() // PointerTest(&d)
p.ValueTest() // ValueTest(*p)
p.PointerTest() // PointerTest(p)
}
輸出:
Data : 0x2101ef018
Value : 0x2101ef028
Pointer: 0x2101ef018
Value : 0x2101ef030
Pointer: 0x2101ef018
從1.4開始,不再支持多級指針查找方法成員。
type X struct{}
func (*X) test() {
println("X.test")
}
func main() {
p := &X{}
p.test()
// Error: calling method with receiver &p (type **X) requires explicit dereference
// (&p).test()
}
可以像字段成員那樣訪問匿名字段方法,編譯器負責查找。
type User struct {
id int
name string
}
type Manager struct {
User
}
func (self *User) ToString() string { // receiver = &(Manager.User)
return fmt.Sprintf("User: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}}
fmt.Printf("Manager: %p\n", &m)
fmt.Println(m.ToString())
}
輸出:
Manager: 0x2102281b0
User : 0x2102281b0, &{1 Tom}
通過匿名字段,可獲得和繼承類似的復用能力。依據(jù)編譯器查找次序,只需在外層定義同名方法,就可以實現(xiàn) "override"。
type User struct {
id int
name string
}
type Manager struct {
User
title string
}
func (self *User) ToString() string {
return fmt.Sprintf("User: %p, %v", self, self)
}
func (self *Manager) ToString() string {
return fmt.Sprintf("Manager: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}, "Administrator"}
fmt.Println(m.ToString())
fmt.Println(m.User.ToString())
}
輸出:
Manager: 0x2102271b0, &{{1 Tom} Administrator}
User : 0x2102271b0, &{1 Tom}
每個類型都有與之關聯(lián)的方法集,這會影響到接口實現(xiàn)規(guī)則。
用實例 value 和 pointer 調(diào)用方法 (含匿名字段) 不受方法集約束,編譯器總是查找全部方法,并自動轉換 receiver 實參。
根據(jù)調(diào)用者不同,方法分為兩種表現(xiàn)形式:
instance.method(args...) ---> <type>.func(instance, args...)
前者稱為 method value,后者 method expression。
兩者都可像普通函數(shù)那樣賦值和傳參,區(qū)別在于 method value 綁定實例,而 method expression 則須顯式傳參。
type User struct {
id int
name string
}
func (self *User) Test() {
fmt.Printf("%p, %v\n", self, self)
}
func main() {
u := User{1, "Tom"}
u.Test()
mValue := u.Test
mValue() // 隱式傳遞 receiver
mExpression := (*User).Test
mExpression(&u) // 顯式傳遞 receiver
}
輸出:
0x210230000, &{1 Tom}
0x210230000, &{1 Tom}
0x210230000, &{1 Tom}
需要注意,method value 會復制 receiver。
type User struct {
id int
name string
}
func (self User) Test() {
fmt.Println(self)
}
func main() {
u := User{1, "Tom"}
mValue := u.Test // 立即復制 receiver,因為不是指針類型,不受后續(xù)修改影響。
u.id, u.name = 2, "Jack"
u.Test()
mValue()
}
輸出:
{2 Jack}
{1 Tom}
在匯編層面,method value 和閉包的實現(xiàn)方式相同,實際返回 FuncVal 類型對象。
FuncVal { method_address, receiver_copy }
可依據(jù)方法集轉換 method expression,注意 receiver 類型的差異。
type User struct {
id int
name string
}
func (self *User) TestPointer() {
fmt.Printf("TestPointer: %p, %v\n", self, self)
}
func (self User) TestValue() {
fmt.Printf("TestValue: %p, %v\n", &self, self)
}
func main() {
u := User{1, "Tom"}
fmt.Printf("User: %p, %v\n", &u, u)
mv := User.TestValue
mv(u)
mp := (*User).TestPointer
mp(&u)
mp2 := (*User).TestValue // *User 方法集包含 TestValue。
mp2(&u) // 簽名變?yōu)?func TestValue(self *User)。
} // 實際依然是 receiver value copy。
輸出:
User : 0x210231000, {1 Tom}
TestValue : 0x210231060, {1 Tom}
TestPointer: 0x210231000, &{1 Tom}
TestValue : 0x2102310c0, {1 Tom}
將方法 "還原" 成函數(shù),就容易理解下面的代碼了。
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
var p *Data = nil
p.TestPointer()
(*Data)(nil).TestPointer() // method value
(*Data).TestPointer(nil) // method expression
// p.TestValue() // invalid memory address or nil pointer dereference
// (Data)(nil).TestValue() // cannot convert nil to type Data
// Data.TestValue(nil) // cannot use nil as type Data in function argument
}
接口是一個或多個方法簽名的集合,任何類型的方法集中只要擁有與之對應的全部方法,就表示它 "實現(xiàn)" 了該接口,無須在該類型上顯式添加接口聲明。
所謂對應方法,是指有相同名稱、參數(shù)列表 (不包括參數(shù)名) 以及返回值。當然,該類型還可以有其他方法。
type Stringer interface {
String() string
}
type Printer interface {
Stringer // 接口嵌入。
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var t Printer = &User{1, "Tom"} // *User 方法集包含 String、Print。
t.Print()
}
輸出:
user 1, Tom
空接口 interface{} 沒有任何方法簽名,也就意味著任何類型都實現(xiàn)了空接口。其作用類似面向?qū)ο笳Z言中的根對象 object。
func Print(v interface{}) {
fmt.Printf("%T: %v\n", v, v)
}
func main() {
Print(1)
Print("Hello, World!")
}
輸出:
int: 1
string: Hello, World!
匿名接口可用作變量類型,或結構成員。
type Tester struct {
s interface {
String() string
}
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
t := Tester{&User{1, "Tom"}}
fmt.Println(t.s.String())
}
輸出:
user 1, Tom
接口對象由接口表 (interface table) 指針和數(shù)據(jù)指針組成。
runtime.h
struct Iface
{
Itab* tab;
void* data;
};
struct Itab
{
InterfaceType* inter;
Type* type;
void (*fun[])(void);
};
接口表存儲元數(shù)據(jù)信息,包括接口類型、動態(tài)類型,以及實現(xiàn)接口的方法指針。無論是反射還是通過接口調(diào)用方法,都會用到這些信息。
數(shù)據(jù)指針持有的是目標對象的只讀復制品,復制完整對象或指針。
type User struct {
id int
name string
}
func main() {
u := User{1, "Tom"}
var i interface{} = u
u.id = 2
u.name = "Jack"
fmt.Printf("%v\n", u)
fmt.Printf("%v\n", i.(User))
}
輸出:
{2 Jack}
{1 Tom}
接口轉型返回臨時對象,只有使用指針才能修改其狀態(tài)。
type User struct {
id int
name string
}
func main() {
u := User{1, "Tom"}
var vi, pi interface{} = u, &u
// vi.(User).name = "Jack" // Error: cannot assign to vi.(User).name
pi.(*User).name = "Jack"
fmt.Printf("%v\n", vi.(User))
fmt.Printf("%v\n", pi.(*User))
}
輸出:
{1 Tom}
&{1 Jack}
只有 tab 和 data 都為 nil 時,接口才等于 nil。
var a interface{} = nil // tab = nil, data = nil
var b interface{} = (*int)(nil) // tab 包含 *int 類型信息, data = nil
type iface struct {
itab, data uintptr
}
ia := *(*iface)(unsafe.Pointer(&a))
ib := *(*iface)(unsafe.Pointer(&b))
fmt.Println(a == nil, ia)
fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
輸出:
true {0 0}
false {505728 0} true
利用類型推斷,可判斷接口對象是否某個具體的接口或類型。
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
var o interface{} = &User{1, "Tom"}
if i, ok := o.(fmt.Stringer); ok { // ok-idiom
fmt.Println(i)
}
u := o.(*User)
// u := o.(User) // panic: interface is *main.User, not main.User
fmt.Println(u)
}
還可用 switch 做批量類型判斷,不支持 fallthrough。
func main() {
var o interface{} = &User{1, "Tom"}
switch v := o.(type) {
case nil: // o == nil
fmt.Println("nil")
case fmt.Stringer: // interface
fmt.Println(v)
case func() string: // func
fmt.Println(v())
case *User: // *struct
fmt.Printf("%d, %s\n", v.id, v.name)
default:
fmt.Println("unknown")
}
}
超集接口對象可轉換為子集接口,反之出錯。
type Stringer interface {
String() string
}
type Printer interface {
String() string
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %v", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var o Printer = &User{1, "Tom"}
var s Stringer = o
fmt.Println(s.String())
}
讓編譯器檢查,以確保某個類型實現(xiàn)接口。
var _ fmt.Stringer = (*Data)(nil)
某些時候,讓函數(shù)直接 "實現(xiàn)" 接口能省不少事。
type Tester interface {
Do()
}
type FuncDo func()
func (self FuncDo) Do() { self() }
func main() {
var t Tester = FuncDo(func() { println("Hello, World!") })
t.Do()
}
Go 在語言層面對并發(fā)編程提供支持,一種類似協(xié)程,稱作 goroutine 的機制。
只需在函數(shù)調(diào)用語句前添加 go 關鍵字,就可創(chuàng)建并發(fā)執(zhí)行單元。開發(fā)人員無需了解任何執(zhí)行細節(jié),調(diào)度器會自動將其安排到合適的系統(tǒng)線程上執(zhí)行。goroutine 是一種非常輕量級的實現(xiàn),可在單個進程里執(zhí)行成千上萬的并發(fā)任務。
事實上,入口函數(shù) main 就以 goroutine 運行。另有與之配套的 channel 類型,用以實現(xiàn) "以通訊來共享內(nèi)存" 的 CSP 模式。相關實現(xiàn)細節(jié)可參考本書第二部分的源碼剖析。
go func() {
println("Hello, World!")
}()
調(diào)度器不能保證多個 goroutine 執(zhí)行次序,且進程退出時不會等待它們結束。
默認情況下,進程啟動后僅允許一個系統(tǒng)線程服務于 goroutine??墒褂铆h(huán)境變量或標準庫函數(shù) runtime.GOMAXPROCS 修改,讓調(diào)度器用多個線程實現(xiàn)多核并行,而不僅僅是并發(fā)。
func sum(id int) {
var x int64
for i := 0; i < math.MaxUint32; i++ {
x += int64(i)
}
println(id, x)
}
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
for i := 0; i < 2; i++ {
go func(id int) {
defer wg.Done()
sum(id)
}(i)
}
wg.Wait()
}
輸出:
$ go build -o test
$ time -p ./test
0 9223372030412324865
1 9223372030412324865
real 7.70 // 程序開始到結束時間差 (非 CPU 時間)
user 7.66 // 用戶態(tài)所使用 CPU 時間片 (多核累加)
sys 0.01 // 內(nèi)核態(tài)所使用 CPU 時間片
$ GOMAXPROCS=2 time -p ./test
0 9223372030412324865
1 9223372030412324865
real 4.18
user 7.61 // 雖然總時間差不多,但由 2 個核并行,real 時間自然少了許多。
sys 0.02
調(diào)用 runtime.Goexit 將立即終止當前 goroutine 執(zhí)行,調(diào)度器確保所有已注冊 defer 延遲調(diào)用被執(zhí)行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
defer wg.Done()
defer println("A.defer")
func() {
defer println("B.defer")
runtime.Goexit() // 終止當前 goroutine
println("B") // 不會執(zhí)行
}()
println("A") // 不會執(zhí)行
}()
wg.Wait()
}
輸出:
B.defer
A.defer
和協(xié)程 yield 作用類似,Gosched 讓出底層線程,將當前 goroutine 暫停,放回隊列等待下次被調(diào)度執(zhí)行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 6; i++ {
println(i)
if i == 3 { runtime.Gosched() }
}
}()
go func() {
defer wg.Done()
println("Hello, World!")
}()
wg.Wait()
}
輸出:
$ go run main.go
0
1
2
3
Hello, World!
4
5
引用類型 channel 是 CSP 模式的具體實現(xiàn),用于多個 goroutine 通訊。其內(nèi)部實現(xiàn)了同步,確保并發(fā)安全。
默認為同步模式,需要發(fā)送和接收配對。否則會被阻塞,直到另一方準備好后被喚醒。
func main() {
data := make(chan int) // 數(shù)據(jù)交換隊列
exit := make(chan bool) // 退出通知
go func() {
for d := range data { // 從隊列迭代接收數(shù)據(jù),直到 close 。
fmt.Println(d)
}
fmt.Println("recv over.")
exit <- true // 發(fā)出退出通知。
}()
data <- 1 // 發(fā)送數(shù)據(jù)。
data <- 2
data <- 3
close(data) // 關閉隊列。
fmt.Println("send over.")
<-exit // 等待退出通知。
}
輸出:
1
2
3
send over.
recv over.
異步方式通過判斷緩沖區(qū)來決定是否阻塞。如果緩沖區(qū)已滿,發(fā)送被阻塞;緩沖區(qū)為空,接收被阻塞。
通常情況下,異步 channel 可減少排隊阻塞,具備更高的效率。但應該考慮使用指針規(guī)避大對象拷貝,將多個元素打包,減小緩沖區(qū)大小等。
func main() {
data := make(chan int, 3) // 緩沖區(qū)可以存儲 3 個元素
exit := make(chan bool)
data <- 1 // 在緩沖區(qū)未滿前,不會阻塞。
data <- 2
data <- 3
go func() {
for d := range data { // 在緩沖區(qū)未空前,不會阻塞。
fmt.Println(d)
}
exit <- true
}()
data <- 4 // 如果緩沖區(qū)已滿,阻塞。
data <- 5
close(data)
<-exit
}
緩沖區(qū)是內(nèi)部屬性,并非類型構成要素。
var a, b chan int = make(chan int), make(chan int, 3)
除用 range 外,還可用 ok-idiom 模式判斷 channel 是否關閉。
for {
if d, ok := <-data; ok {
fmt.Println(d)
} else {
break
}
}
向 closed channel 發(fā)送數(shù)據(jù)引發(fā) panic 錯誤,接收立即返回零值。而 nil channel,無論收發(fā)都會被阻塞。
內(nèi)置函數(shù) len 返回未被讀取的緩沖元素數(shù)量,cap 返回緩沖區(qū)大小。
d1 := make(chan int)
d2 := make(chan int, 3)
d2 <- 1
fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3
可以將 channel 隱式轉換為單向隊列,只收或只發(fā)。
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
send <- 1
// <-send // Error: receive from send-only type chan<- int
<-recv
// recv <- 2 // Error: send to receive-only type <-chan int
不能將單向 channel 轉換為普通 channel。
d := (chan int)(send) // Error: cannot convert type chan<- int to type chan int
d := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int
如果需要同時處理多個 channel,可使用 select 語句。它隨機選擇一個可用 channel 做收發(fā)操作,或執(zhí)行 default case。
func main() {
a, b := make(chan int, 3), make(chan int)
go func() {
v, ok, s := 0, false, ""
for {
select { // 隨機選擇可用 channel,接收數(shù)據(jù)。
case v, ok = <-a: s = "a"
case v, ok = <-b: s = "b"
}
if ok {
fmt.Println(s, v)
} else {
os.Exit(0)
}
}
}()
for i := 0; i < 5; i++ {
select { // 隨機選擇可用 channel,發(fā)送數(shù)據(jù)。
case a <- i:
case b <- i:
}
}
close(a)
select {} // 沒有可用 channel,阻塞 main goroutine。
}
輸出:
b 3
a 0
a 1
a 2
b 4
在循環(huán)中使用 select default case 需要小心,避免形成洪水。
用簡單工廠模式打包并發(fā)任務和 channel。
func NewTest() chan int {
c := make(chan int)
rand.Seed(time.Now().UnixNano())
go func() {
time.Sleep(time.Second)
c <- rand.Int()
}()
return c
}
func main() {
t := NewTest()
println(<-t) // 等待 goroutine 結束返回。
}
用 channel 實現(xiàn)信號量 (semaphore)。
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
sem := make(chan int, 1)
for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done()
sem <- 1 // 向 sem 發(fā)送數(shù)據(jù),阻塞或者成功。
for x := 0; x < 3; x++ {
fmt.Println(id, x)
}
<-sem // 接收數(shù)據(jù),使得其他阻塞 goroutine 可以發(fā)送數(shù)據(jù)。
}(i)
}
wg.Wait()
}
輸出:
$ GOMAXPROCS=2 go run main.go
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2
用 closed channel 發(fā)出退出通知。
func main() {
var wg sync.WaitGroup
quit := make(chan bool)
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
task := func() {
println(id, time.Now().Nanosecond())
time.Sleep(time.Second)
}
for {
select {
case <-quit: // closed channel 不會阻塞,因此可用作退出通知。
return
default: // 執(zhí)行正常任務。
task()
}
}
}(i)
}
time.Sleep(time.Second * 5) // 讓測試 goroutine 運行一會。
close(quit) // 發(fā)出退出通知。
wg.Wait()
}
用 select 實現(xiàn)超時 (timeout)。
func main() {
w := make(chan bool)
c := make(chan int, 2)
go func() {
select {
case v := <-c: fmt.Println(v)
case <-time.After(time.Second * 3): fmt.Println("timeout.")
}
w <- true
}()
// c <- 1 // 注釋掉,引發(fā) timeout。
<-w
}
channel 是第一類對象,可傳參 (內(nèi)部實現(xiàn)為指針) 或者作為結構成員。
type Request struct {
data []int
ret chan int
}
func NewRequest(data ...int) *Request {
return &Request{ data, make(chan int, 1) }
}
func Process(req *Request) {
x := 0
for _, i := range req.data {
x += i
}
req.ret <- x
}
func main() {
req := NewRequest(10, 20, 30)
Process(req)
fmt.Println(<-req.ret)
}
編譯工具對源碼目錄有嚴格要求,每個工作空間 (workspace) 必須由 bin、pkg、src 三個目錄組成。
可在 GOPATH 環(huán)境變量列表中添加多個工作空間,但不能和 GOROOT 相同。
export GOPATH=$HOME/projects/golib:$HOME/projects/go
通常 go get 使用第一個工作空間保存下載的第三方庫。
編碼:源碼文件必須是 UTF-8 格式,否則會導致編譯器出錯。結束:語句以 ";" 結束,多數(shù)時候可以省略。注釋:支持 "//"、"/**/" 兩種注釋方式,不能嵌套。命名:采用 camelCasing 風格,不建議使用下劃線。
所有代碼都必須組織在 package 中。
說明:os.Args 返回命令行參數(shù),os.Exit 終止進程。
要獲取正確的可執(zhí)行文件路徑,可用 filepath.Abs(exec.LookPath(os.Args[0]))。
包中成員以名稱首字母大小寫決定訪問權限。
該規(guī)則適用于全局變量、全局常量、類型、結構字段、函數(shù)、方法等。
使用包成員前,必須先用 import 關鍵字導入,但不能形成導入循環(huán)。
import "相對目錄/包主文件名"
相對目錄是指從 /pkg/ 開始的子目錄,以標準庫為例:
import "fmt" -> /usr/local/go/pkg/darwin_amd64/fmt.a
import "os/exec" -> /usr/local/go/pkg/darwin_amd64/os/exec.a
在導入時,可指定包成員訪問方式。比如對包重命名,以避免同名沖突。
import "yuhen/test" // 默認模式: test.A
import M "yuhen/test" // 包重命名: M.A
import . "yuhen/test" // 簡便模式: A
import _ "yuhen/test" // 非導入模式: 僅讓該包執(zhí)行初始化函數(shù)。
未使用的導入包,會被編譯器視為錯誤 (不包括 "import _")。
./main.go:4: imported and not used: "fmt"
對于當前目錄下的子包,除使用默認完整導入路徑外,還可使用 local 方式。
main.go
import "learn/test" // 正常模式
import "./test" // 本地模式,僅對 go run main.go 有效。
可通過 meta 設置為代碼庫設置自定義路徑。
server.go
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `<meta name="go-import"
content="test.com/qyuhen/test git https://github.com/qyuhen/test">`)
}
func main() {
http.HandleFunc("/qyuhen/test", handler)
http.ListenAndServe(":80", nil)
}
該示例使用自定義域名 test.com 重定向到 github。
$ go get -v test.com/qyuhen/test
Fetching https://test.com/qyuhen/testgo-get=1
https fetch failed.
Fetching http://test.com/qyuhen/testgo-get=1
Parsing meta tags from http://test.com/qyuhen/testgo-get=1 (status code 200)
get "test.com/qyuhen/test": found meta tag http://test.com/qyuhen/testgo-get=1
test.com/qyuhen/test (download)
test.com/qyuhen/test
如此,該庫就有兩個有效導入路徑,可能會導致存儲兩個本地副本。為此,可以給庫添加專門的 "import comment"。當 go get 下載完成后,會檢查本地存儲路徑和該注釋是否一致。
github.com/qyuhen/test/abc.go
package test // import "test.com/qyuhen/test"
func Hello() {
println("Hello, Custom import path!")
}
如繼續(xù)用 github 路徑,會導致 go build 失敗。
$ go get -v github.com/qyuhen/test
github.com/qyuhen/test (download)
package github.com/qyuhen/test
" imports github.com/qyuhen/test
" imports github.com/qyuhen/test: expects import "test.com/qyuhen/test"
這就強制包用戶使用唯一路徑,也便于日后將包遷移到其他位置。
資源:Go 1.4 Custom Import Path Checking
初始化函數(shù):
因為無法保證初始化函數(shù)執(zhí)行順序,因此全局變量應該直接用 var 初始化。
var now = time.Now()
func init() {
fmt.Printf("now: %v\n", now)
}
func init() {
fmt.Printf("since: %v\n", time.Now().Sub(now))
}
可在初始化函數(shù)中使用 goroutine,可等待其結束。
var now = time.Now()
func main() {
fmt.Println("main:", int(time.Now().Sub(now).Seconds()))
}
func init() {
fmt.Println("init:", int(time.Now().Sub(now).Seconds()))
w := make(chan bool)
go func() {
time.Sleep(time.Second * 3)
w <- true
}()
<-w
}
輸出:
init: 0
main: 3
不應該濫用初始化函數(shù),僅適合完成當前文件中的相關環(huán)境設置。
擴展工具 godoc 能自動提取注釋生成幫助文檔。
只要 Example 測試函數(shù)名稱符合以下規(guī)范即可。
說明:使用 suffix 作為示例名稱,其首字母必須小寫。如果文件中僅有一個 Example 函數(shù),且調(diào)用了該文件中的其他成員,那么示例會顯示整個文件內(nèi)容,而不僅僅是測試函數(shù)自己。
非測試源碼文件中以 BUG(author) 開始的注釋,會在幫助文檔 Bugs 節(jié)點中顯示。
// BUG(yuhen): memory leak.
了解對象內(nèi)存布局,有助于理解值傳遞、引用傳遞等概念。
string
struct
slice
interface
new
make
對象內(nèi)存分配會受編譯參數(shù)影響。舉個例子,當函數(shù)返回對象指針時,必然在堆上分配。
可如果該函數(shù)被內(nèi)聯(lián),那么這個指針就不會跨棧幀使用,就有可能直接在棧上分配,以實現(xiàn)代碼優(yōu)化目的。因此,是否阻止內(nèi)聯(lián)對指針輸出結果有很大影響。
允許指針指向?qū)ο蟪蓡T,并確保該對象是可達狀態(tài)。
除正常指針外,指針還有 unsafe.Pointer 和 uintptr 兩種形態(tài)。其中 uintptr 被 GC 當做普通整數(shù)對象,它不能阻止所 "引用" 對象被回收。
type data struct {
x [1024 * 100]byte
}
func test() uintptr {
p := &data{}
return uintptr(unsafe.Pointer(p))
}
func main() {
const N = 10000
cache := new([N]uintptr)
for i := 0; i < N; i++ {
cache[i] = test()
time.Sleep(time.Millisecond)
}
}
輸出:
$ go build -o test && GODEBUG="gctrace=1" ./test
gc607(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3070-3025) objects
gc611(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3090-3045) objects
gc613(1): 0+0+0 ms, 0 -> 0 MB 50 -> 45 (3100-3055) objects
合法的 unsafe.Pointer 被當做普通指針對待。
func test() unsafe.Pointer {
p := &data{}
return unsafe.Pointer(p)
}
func main() {
const N = 10000
cache := new([N]unsafe.Pointer)
for i := 0; i < N; i++ {
cache[i] = test()
time.Sleep(time.Millisecond)
}
}
輸出:
$ go build -o test && GODEBUG="gctrace=1" ./test
gc12(1): 0+0+0 ms, 199 -> 199 MB 2088 -> 2088 (2095-7) objects
gc13(1): 0+0+0 ms, 399 -> 399 MB 4136 -> 4136 (4143-7) objects
gc14(1): 0+0+0 ms, 799 -> 799 MB 8232 -> 8232 (8239-7) objects
指向?qū)ο蟪蓡T的 unsafe.Pointer,同樣能確保對象不被回收。
type data struct {
x [1024 * 100]byte
y int
}
func test() unsafe.Pointer {
d := data{}
return unsafe.Pointer(&d.y)
}
func main() {
const N = 10000
cache := new([N]unsafe.Pointer)
for i := 0; i < N; i++ {
cache[i] = test()
time.Sleep(time.Millisecond)
}
}
輸出:
$ go build -o test && GODEBUG="gctrace=1" ./test
gc12(1): 0+0+0 ms, 207 -> 207 MB 2088 -> 2088 (2095-7) objects
gc13(1): 1+0+0 ms, 415 -> 415 MB 4136 -> 4136 (4143-7) objects
gc14(1): 3+1+0 ms, 831 -> 831 MB 8232 -> 8232 (8239-7) objects
由于可以用 unsafe.Pointer、uintptr 創(chuàng)建 "dangling pointer" 等非法指針,所以在使用時需要特別小心。另外,cgo C.malloc 等函數(shù)所返回指針,與 GC 無關。
指針構成的 "循環(huán)引用" 加上 runtime.SetFinalizer 會導致內(nèi)存泄露。
type Data struct {
d [1024 * 100]byte
o *Data
}
func test() {
var a, b Data
a.o = &b
b.o = &a
runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
}
func main() {
for {
test()
time.Sleep(time.Millisecond)
}
}
輸出:
$ go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test
gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects
垃圾回收器能正確處理 "指針循環(huán)引用",但無法確定 Finalizer 依賴次序,也就無法調(diào)用Finalizer 函數(shù),這會導致目標對象無法變成不可達狀態(tài),其所占用內(nèi)存無法被回收。
通過 cgo,可在 Go 和 C/C++ 代碼間相互調(diào)用。受 CGO_ENABLED 參數(shù)限制。
package main
/*
#include <stdio.h>
#include <stdlib.h>
void hello() {
printf("Hello, World!\n");
}
*/
import "C"
func main() {
C.hello()
}
調(diào)試 cgo 代碼是件很麻煩的事,建議單獨保存到 .c 文件中。這樣可以將其當做獨立的 C 程序進行調(diào)試。
test.h
#ifndef __TEST_H__
#define __TEST_H__
void hello();
#endif
test.c
#include <stdio.h>
#include "test.h"
void hello() {
printf("Hello, World!\n");
}
#ifdef __TEST__ // 避免和 Go bootstrap main 沖突。
int main(int argc, char *argv[]) {
hello();
return 0;
}
#endif
main.go
package main
/*
#include "test.h"
*/
import "C"
func main() {
C.hello()
}
編譯和調(diào)試 C,只需在命令行提供宏定義即可。
$ gcc -g -D__TEST__ -o test test.c
由于 cgo 僅掃描當前目錄,如果需要包含其他 C 項目,可在當前目錄新建一個 C 文件,然后用 #include 指令將所需的 .h、.c 都包含進來,記得在 CFLAGS 中使用 "-I" 參數(shù)指定原路徑。某些時候,可能還需指定 "-std" 參數(shù)。
可使用 #cgo 命令定義 CFLAGS、LDFLAGS 等參數(shù),自動合并多個設置。
/*
#cgo CFLAGS: -g
#cgo CFLAGS: -I./lib -D__VER__=1
#cgo LDFLAGS: -lpthread
#include "test.h"
*/
import "C"
可設置 GOOS、GOARCH 編譯條件,空格表示 OR,逗號 AND,感嘆號 NOT。
#cgo windows,386 CFLAGS: -I./lib -D__VER__=1
數(shù)據(jù)類型對應關系。
可將 cgo 類型轉換為標準 Go 類型。
/*
int add(int x, int y) {
return x + y;
}
*/
import "C"
func main() {
var x C.int = C.add(1, 2)
var y int = int(x)
fmt.Println(x, y)
}
字符串轉換函數(shù)。
/*
#include <stdio.h>
#include <stdlib.h>
void test(char *s) {
printf("%s\n", s);
}
char* cstr() {
return "abcde";
}
*/
import "C"
func main() {
s := "Hello, World!"
cs := C.CString(s) // 該函數(shù)在 C heap 分配內(nèi)存,需要調(diào)用 free 釋放。
defer C.free(unsafe.Pointer(cs)) // #include <stdlib.h>
C.test(cs)
cs = C.cstr()
fmt.Println(C.GoString(cs))
fmt.Println(C.GoStringN(cs, 2))
fmt.Println(C.GoBytes(unsafe.Pointer(cs), 2))
}
輸出:
Hello, World!
abcde
ab
[97 98]
用 C.malloc/free 分配 C heap 內(nèi)存。
/*
#include <stdlib.h>
*/
import "C"
func main() {
m := unsafe.Pointer(C.malloc(4 * 8))
defer C.free(m) // 注釋釋放內(nèi)存。
p := (*[4]int)(m) // 轉換為數(shù)組指針。
for i := 0; i < 4; i++ {
p[i] = i + 100
}
fmt.Println(p)
}
輸出:
&[100 101 102 103]
對 struct、enum 支持良好,union 會被轉換成字節(jié)數(shù)組。如果沒使用 typedef 定義,那么必須添加 struct、enum、union_ 前綴。
struct
/*
#include <stdlib.h>
struct Data {
int x;
};
typedef struct {
int x;
} DataType;
struct Data* testData() {
return malloc(sizeof(struct Data));
}
DataType* testDataType() {
return malloc(sizeof(DataType));
}
*/
import "C"
func main() {
var d *C.struct_Data = C.testData()
defer C.free(unsafe.Pointer(d))
var dt *C.DataType = C.testDataType()
defer C.free(unsafe.Pointer(dt))
d.x = 100
dt.x = 200
fmt.Printf("%#v\n", d)
fmt.Printf("%#v\n", dt)
}
輸出:
&main._Ctype_struct_Data{x:100}
&main._Ctype_DataType{x:200}
enum
/*
enum Color { BLACK = 10, RED, BLUE };
typedef enum { INSERT = 3, DELETE } Mode;
*/
import "C"
func main() {
var c C.enum_Color = C.RED
var x uint32 = c
fmt.Println(c, x)
var m C.Mode = C.INSERT
fmt.Println(m)
}
union
/*
#include <stdlib.h>
union Data {
char x;
int y;
};
union Data* test() {
union Data* p = malloc(sizeof(union Data));
p->x = 100;
return p;
}
*/
import "C"
func main() {
var d *C.union_Data = C.test()
defer C.free(unsafe.Pointer(d))
fmt.Println(d)
}
輸出:
&[100 0 0 0]
導出 Go 函數(shù)給 C 調(diào)用,須使用 "//export" 標記。建議在獨立頭文件中聲明函數(shù)原型,避免 "duplicate symbol" 錯誤。
main.go
package main
import "fmt"
/*
#include "test.h"
*/
import "C"
//export hello
func hello() {
fmt.Println("Hello, World!\n")
}
func main() {
C.test()
}
test.h
#ifndef __TEST_H__
#define __TEST_H__
extern void hello();
void test();
#endif
test.c
#include <stdio.h>
#include "test.h"
void test() {
hello();
}
在 cgo 中使用 C 共享庫。
test.h
#ifndef __TEST_HEAD__
#define __TEST_HEAD__
int sum(int x, int y);
#endif
test.c
#include <stdio.h>
#include <stdlib.h>
#include "test.h"
int sum(int x, int y)
{
return x + y + 100;
}
編譯成 .so 或 .dylib。
$ gcc -c -fPIC -o test.o test.c
$ gcc -dynamiclib -o libtest.dylib test.o
將共享庫和頭文件拷貝到 Go 項目目錄。
main.go
package main
/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -L. -ltest
#include "test.h"
*/
import "C"
func main() {
println(C.sum(10, 20))
}
輸出:
$ go build -o test && ./test
130
編譯成功后可用 ldd 或 otool 查看動態(tài)庫使用狀態(tài)。 靜態(tài)庫使用方法類似。
沒有運行期類型對象,實例也沒有附加字段用來表明身份。只有轉換成接口時,才會在其 itab 內(nèi)部存儲與該類型有關的信息,Reflect 所有操作都依賴于此。
以 struct 為例,可獲取其全部成員字段信息,包括非導出和匿名字段。
type User struct {
Username string
}
type Admin struct {
User
title string
}
func main() {
var u Admin
t := reflect.TypeOf(u)
for i, n := 0, t.NumField(); i < n; i++ {
f := t.Field(i)
fmt.Println(f.Name, f.Type)
}
}
輸出:
User main.User // 可進一步遞歸。
title string
如果是指針,應該先使用 Elem 方法獲取目標類型,指針本身是沒有字段成員的。
func main() {
u := new(Admin)
t := reflect.TypeOf(u)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
for i, n := 0, t.NumField(); i < n; i++ {
f := t.Field(i)
fmt.Println(f.Name, f.Type)
}
}
同樣,value-interface 和 pointer-interface 也會導致方法集存在差異。
type User struct {
}
type Admin struct {
User
}
func (*User) ToString() {}
func (Admin) test() {}
func main() {
var u Admin
methods := func(t reflect.Type) {
for i, n := 0, t.NumMethod(); i < n; i++ {
m := t.Method(i)
fmt.Println(m.Name)
}
}
fmt.Println("--- value interface ---")
methods(reflect.TypeOf(u))
fmt.Println("--- pointer interface ---")
methods(reflect.TypeOf(&u))
}
輸出:
--- value interface ---
test
--- pointer interface ---
ToString
test
可直接用名稱或序號訪問字段,包括用多級序號訪問嵌入字段成員。
type User struct {
Username string
age int
}
type Admin struct {
User
title string
}
func main() {
var u Admin
t := reflect.TypeOf(u)
f, _ := t.FieldByName("title")
fmt.Println(f.Name)
f, _ = t.FieldByName("User") // 訪問嵌入字段。
fmt.Println(f.Name)
f, _ = t.FieldByName("Username") // 直接訪問嵌入字段成員,會自動深度查找。
fmt.Println(f.Name)
f = t.FieldByIndex([]int{0, 1}) // Admin[0] -> User[1] -> age
fmt.Println(f.Name)
}
輸出:
title
User
Username
age
字段標簽可實現(xiàn)簡單元數(shù)據(jù)編程,比如標記 ORM Model 屬性。
type User struct {
Name string `field:"username" type:"nvarchar(20)"`
Age int `field:"age" type:"tinyint"`
}
func main() {
var u User
t := reflect.TypeOf(u)
f, _ := t.FieldByName("Name")
fmt.Println(f.Tag)
fmt.Println(f.Tag.Get("field"))
fmt.Println(f.Tag.Get("type"))
}
輸出:
field:"username" type:"nvarchar(20)"
username
nvarchar(20)
可從基本類型獲取所對應復合類型。
var (
Int = reflect.TypeOf(0)
String = reflect.TypeOf("")
)
func main() {
c := reflect.ChanOf(reflect.SendDir, String)
fmt.Println(c)
m := reflect.MapOf(String, Int)
fmt.Println(m)
s := reflect.SliceOf(Int)
fmt.Println(s)
t := struct{ Name string }{}
p := reflect.PtrTo(reflect.TypeOf(t))
fmt.Println(p)
}
輸出:
chan<- string
map[string]int
[]int
*struct { Name string }
與之對應,方法 Elem 可返回復合類型的基類型。
func main() {
t := reflect.TypeOf(make(chan int)).Elem()
fmt.Println(t)
}
方法 Implements 判斷是否實現(xiàn)了某個具體接口,AssignableTo、ConvertibleTo 用于賦值和轉換判斷。
type Data struct {
}
func (*Data) String() string {
return ""
}
func main() {
var d *Data
t := reflect.TypeOf(d)
// 沒法直接獲取接口類型,好在接口本身是個 struct,創(chuàng)建
// 一個空指針對象,這樣傳遞給 TypeOf 轉換成 interface{}
// 時就有類型信息了。。
it := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
// 為啥不是 t.Implements(fmt.Stringer),完全可以由編譯器生成。
fmt.Println(t.Implements(it))
}
某些時候,獲取對齊信息對于內(nèi)存自動分析是很有用的。
type Data struct {
b byte
x int32
}
func main() {
var d Data
t := reflect.TypeOf(d)
fmt.Println(t.Size(), t.Align()) // sizeof,以及最寬字段的對齊模數(shù)。
f, _ := t.FieldByName("b")
fmt.Println(f.Type.FieldAlign()) // 字段對齊。
}
輸出:
8 4
1
Value 和 Type 使用方法類似,包括使用 Elem 獲取指針目標對象。
type User struct {
Username string
age int
}
type Admin struct {
User
title string
}
func main() {
u := &Admin{User{"Jack", 23}, "NT"}
v := reflect.ValueOf(u).Elem()
fmt.Println(v.FieldByName("title").String()) // 用轉換方法獲取字段值
fmt.Println(v.FieldByName("age").Int()) // 直接訪問嵌入字段成員
fmt.Println(v.FieldByIndex([]int{0, 1}).Int()) // 用多級序號訪問嵌入字段成員
}
輸出:
NT
23
23
除具體的 Int、String 等轉換方法,還可返回 interface{}。只是非導出字段無法使用,需用 CanInterface 判斷一下。
type User struct {
Username string
age int
}
func main() {
u := User{"Jack", 23}
v := reflect.ValueOf(u)
fmt.Println(v.FieldByName("Username").Interface())
fmt.Println(v.FieldByName("age").Interface())
}
輸出:
Jack
panic: reflect.Value.Interface: cannot return value obtained from unexported field or
method
當然,轉換成具體類型不會引發(fā) panic。
func main() {
u := User{"Jack", 23}
v := reflect.ValueOf(u)
f := v.FieldByName("age")
if f.CanInterface() {
fmt.Println(f.Interface())
} else {
fmt.Println(f.Int())
}
}
除 struct,其他復合類型 array、slice、map 取值示例。
func main() {
v := reflect.ValueOf([]int{1, 2, 3})
for i, n := 0, v.Len(); i < n; i++ {
fmt.Println(v.Index(i).Int())
}
fmt.Println("---------------------------")
v = reflect.ValueOf(map[string]int{"a": 1, "b": 2})
for _, k := range v.MapKeys() {
fmt.Println(k.String(), v.MapIndex(k).Int())
}
}
輸出:
1
2
3
---------------------------
a 1
b 2
需要注意,Value 某些方法沒有遵循 "comma ok" 模式,而是返回 ZeroValue,因此需要用 IsValid 判斷一下是否可用。
func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}
type User struct {
Username string
age int
}
func main() {
u := User{}
v := reflect.ValueOf(u)
f := v.FieldByName("a")
fmt.Println(f.Kind(), f.IsValid())
}
輸出:
invalid false
另外,接口是否為 nil,需要 tab 和 data 都為空??墒褂?IsNil 方法判斷 data 值。
func main() {
var p *int
var x interface{} = p
fmt.Println(x == nil)
v := reflect.ValueOf(p)
fmt.Println(v.Kind(), v.IsNil())
}
輸出:
false
ptr true
將對象轉換為接口,會發(fā)生復制行為。該復制品只讀,無法被修改。所以要通過接口改變目標對象狀態(tài),必須是 pointer-interface。
就算是指針,我們依然沒法將這個存儲在 data 的指針指向其他對象,只能透過它修改目標對象。因為目標對象并沒有被復制,被復制的只是指針。
type User struct {
Username string
age int
}
func main() {
u := User{"Jack", 23}
v := reflect.ValueOf(u)
p := reflect.ValueOf(&u)
fmt.Println(v.CanSet(), v.FieldByName("Username").CanSet())
fmt.Println(p.CanSet(), p.Elem().FieldByName("Username").CanSet())
}
輸出:
false false
false true
非導出字段無法直接修改,可改用指針操作。
type User struct {
Username string
age int
}
func main() {
u := User{"Jack", 23}
p := reflect.ValueOf(&u).Elem()
p.FieldByName("Username").SetString("Tom")
f := p.FieldByName("age")
fmt.Println(f.CanSet())
// 判斷是否能獲取地址。
if f.CanAddr() {
age := (*int)(unsafe.Pointer(f.UnsafeAddr()))
// age := (*int)(unsafe.Pointer(f.Addr().Pointer())) // 等同
*age = 88
}
// 注意 p 是 Value 類型,需要還原成接口才能轉型。
fmt.Println(u, p.Interface().(User))
}
輸出:
false
{Tom 88} {Tom 88}
復合類型修改示例。
func main() {
s := make([]int, 0, 10)
v := reflect.ValueOf(&s).Elem()
v.SetLen(2)
v.Index(0).SetInt(100)
v.Index(1).SetInt(200)
fmt.Println(v.Interface(), s)
v2 := reflect.Append(v, reflect.ValueOf(300))
v2 = reflect.AppendSlice(v2, reflect.ValueOf([]int{400, 500}))
fmt.Println(v2.Interface())
fmt.Println("----------------------")
m := map[string]int{"a": 1}
v = reflect.ValueOf(&m).Elem()
v.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(100)) // update
v.SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf(200)) // add
fmt.Println(v.Interface(), m)
}
輸出:
[100 200] [100 200]
[100 200 300 400 500]
----------------------
map[a:100 b:200] map[a:100 b:200]
可獲取方法參數(shù)、返回值類型等信息。
type Data struct {
}
func (*Data) Test(x, y int) (int, int) {
return x + 100, y + 100
}
func (*Data) Sum(s string, x ...int) string {
c := 0
for _, n := range x {
c += n
}
return fmt.Sprintf(s, c)
}
func info(m reflect.Method) {
t := m.Type
fmt.Println(m.Name)
for i, n := 0, t.NumIn(); i < n; i++ {
fmt.Printf(" in[%d] %v\n", i, t.In(i))
}
for i, n := 0, t.NumOut(); i < n; i++ {
fmt.Printf(" out[%d] %v\n", i, t.Out(i))
}
}
func main() {
d := new(Data)
t := reflect.TypeOf(d)
test, _ := t.MethodByName("Test")
info(test)
sum, _ := t.MethodByName("Sum")
info(sum)
}
輸出:
Test
in[0] *main.Data // receiver
in[1] int
in[2] int
out[0] int
out[1] int
Sum
in[0] *main.Data
in[1] string
in[2] []int
out[0] string
動態(tài)調(diào)用方法很簡單,按 In 列表準備好所需參數(shù)即可 (不包括 receiver)。
func main() {
d := new(Data)
v := reflect.ValueOf(d)
exec := func(name string, in []reflect.Value) {
m := v.MethodByName(name)
out := m.Call(in)
for _, v := range out {
fmt.Println(v.Interface())
}
}
exec("Test", []reflect.Value{
reflect.ValueOf(1),
reflect.ValueOf(2),
})
fmt.Println("-----------------------")
exec("Sum", []reflect.Value{
reflect.ValueOf("result = %d"),
reflect.ValueOf(1),
reflect.ValueOf(2),
})
}
輸出:
101
102
-----------------------
result = 3
如改用 CallSlice,只需將變參打包成 slice 即可。
func main() {
d := new(Data)
v := reflect.ValueOf(d)
m := v.MethodByName("Sum")
in := []reflect.Value{
reflect.ValueOf("result = %d"),
reflect.ValueOf([]int{1, 2}), // 將變參打包成 slice。
}
out := m.CallSlice(in)
for _, v := range out {
fmt.Println(v.Interface())
}
}
非導出方法無法調(diào)用,甚至無法透過指針操作,因為接口類型信息中沒有該方法地址。
利用 Make、New 等函數(shù),可實現(xiàn)近似泛型操作。
var (
Int = reflect.TypeOf(0)
String = reflect.TypeOf("")
)
func Make(T reflect.Type, fptr interface{}) {
// 實際創(chuàng)建 slice 的包裝函數(shù)。
swap := func(in []reflect.Value) []reflect.Value {
// --- 省略算法內(nèi)容 --- //
// 返回和類型匹配的 slice 對象。
return []reflect.Value{
reflect.MakeSlice(
reflect.SliceOf(T), // slice type
int(in[0].Int()), // len
int(in[1].Int()) // cap
),
}
}
// 傳入的是函數(shù)變量指針,因為我們要將變量指向 swap 函數(shù)。
fn := reflect.ValueOf(fptr).Elem()
// 獲取函數(shù)指針類型,生成所需 swap function value。
v := reflect.MakeFunc(fn.Type(), swap)
// 修改函數(shù)指針實際指向,也就是 swap。
fn.Set(v)
}
func main() {
var makeints func(int, int) []int
var makestrings func(int, int) []string
// 用相同算法,生成不同類型創(chuàng)建函數(shù)。
Make(Int, &makeints)
Make(String, &makestrings)
// 按實際類型使用。
x := makeints(5, 10)
fmt.Printf("%#v\n", x)
s := makestrings(3, 10)
fmt.Printf("%#v\n", s)
}
輸出:
[]int{0, 0, 0, 0, 0}
[]string{"", "", ""}
原理并不復雜。
如此,在共享算法的前提下,無須用 interface{},無須做類型轉換,頗有泛型的效果。
更多建議: