Go 是靜態(tài)類型語言,不能在運(yùn)行期改變變量類型。使用關(guān)鍵字 var 定義變量,自動(dòng)初始化為零值。如果提供初始化值,可省略變量類型,由編譯器自動(dòng)推斷。
var x int
var f float32 = 1.6
var s = "abc"
在函數(shù)內(nèi)部,可用更簡(jiǎn)略的 ":=" 方式定義變量。
func main() {
x := 123 // 注意檢查,是定義新局部變量,還是修改全局變量。該方式容易造成錯(cuò)誤。
}
可一次定義多個(gè)變量。
var x, y, z int
var s, n = "abc", 123
var (
a int
b float32
)
func main() {
n, s := 0x1234, "Hello, World!"
println(x, s, n)
}
多變量賦值時(shí),先計(jì)算所有相關(guān)值,然后再?gòu)淖蟮接乙来钨x值。
data, i := [3]int{0, 1, 2}, 0
i, data[i] = 2, 100 // (i = 0) -> (i = 2), (data[0] = 100)
特殊只寫變量 "_",用于忽略值占位。
func test() (int, string) {
return 1, "abc"
}
func main() {
_, s := test()
println(s)
}
編譯器會(huì)將未使用的局部變量當(dāng)做錯(cuò)誤。
var s string // 全局變量沒問題。
func main() {
i := 0 // Error: i declared and not used。(可使用 "_ = i" 規(guī)避)
}
注意重新賦值與定義新同名變量的區(qū)別。
s := "abc"
println(&s)
s, y := "hello", 20 // 重新賦值: 與前 s 在同一層次的代碼塊中,且有新的變量被定義。
println(&s, y) // 通常函數(shù)多返回值 err 會(huì)被重復(fù)使用。
{
s, z := 1000, 30 // 定義新同名變量: 不在同一層次代碼塊。
println(&s, z)
}
輸出:
0x2210230f30
0x2210230f30 20
0x2210230f18 30
常量值必須是編譯期可確定的數(shù)字、字符串、布爾值。
const x, y int = 1, 2 // 多常量初始化
const s = "Hello, World!" // 類型推斷
const ( // 常量組
a, b = 10, 100
c bool = false
)
func main() {
const x = "xxx" // 未使用局部常量不會(huì)引發(fā)編譯錯(cuò)誤。
}
不支持 1UL、2LL 這樣的類型后綴。
在常量組中,如不提供類型和初始化值,那么視作與上一常量相同。
const (
s = "abc"
x // x = "abc"
)
常量值還可以是 len、cap、unsafe.Sizeof 等編譯期可確定結(jié)果的函數(shù)返回值。
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(b)
)
如果常量類型足以存儲(chǔ)初始化值,那么不會(huì)引發(fā)溢出錯(cuò)誤。
const (
a byte = 100 // int to byte
b int = 1e20 // float64 to int, overflows
)
枚舉
關(guān)鍵字 iota 定義常量組中從0開始按行計(jì)數(shù)的自增枚舉值。
const (
Sunday = iota // 0
Monday // 1,通常省略后續(xù)行表達(dá)式。
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
const (
_ = iota // iota = 0
KB int64 = 1 << (10 * iota) // iota = 1
MB // 與 KB 表達(dá)式相同,但 iota = 2
GB
TB
)
在同一常量組中,可以提供多個(gè) iota,它們各自增長(zhǎng)。
const (
A, B = iota, iota << 10 // 0, 0 << 10
C, D // 1, 1 << 10
)
如果 iota 自增被打斷,須顯式恢復(fù)。
const (
A = iota // 0
B // 1
C = "c" // c
D // c,與上一行相同。
E = iota // 4,顯式恢復(fù)。注意計(jì)數(shù)包含了 C、D 兩行。
F // 5
)
可通過自定義類型來實(shí)現(xiàn)枚舉類型限制。
type Color int
const (
Black Color = iota
Red
Blue
)
func test(c Color) {}
func main() {
c := Black
test(c)
x := 1
test(x) // Error: cannot use x (type int) as type Color in function argument
test(1) // 常量會(huì)被編譯器自動(dòng)轉(zhuǎn)換。
}
更明確的數(shù)字類型命名,支持 Unicode,支持常用數(shù)據(jù)結(jié)構(gòu)。
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/1.png" alt="" />
支持八進(jìn)制、十六進(jìn)制,以及科學(xué)記數(shù)法。標(biāo)準(zhǔn)庫(kù) math 定義了各數(shù)字類型取值范圍。
a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
空指針值 nil,而非 C/C++ NULL。
引用類型包括 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)訪問字符 (byte)。
s := "abc"
println(s[0] == '\x61', s[1] == 'b', s[2] == 0x63)
輸出:
true true true
使用 "`" 定義不做轉(zhuǎn)義處理的原始字符串,支持跨行。
s := `a
b\r\n\x00
c`
println(s)
輸出:
a
b\r\n\x00
c
連接跨行字符串時(shí),"+" 必須在上一行末尾,否則導(dǎo)致編譯錯(cuò)誤。
s := "Hello, " +
"World!"
s2 := "Hello, "
+ "World!" // Error: invalid operation: + untyped string
支持用兩個(gè)索引號(hào)返回子串。子串依然指向原字節(jié)數(shù)組,僅修改了指針和長(zhǎng)度屬性。
s := "Hello, World!"
s1 := s[:5] // Hello
s2 := s[7:] // World!
s3 := s[1:5] // ello
單引號(hào)字符常量表示 Unicode Code Point,支持 \uFFFF、\U7FFFFFFF、\xFF 格式。對(duì)應(yīng) rune 類型,UCS-4。
func main() {
fmt.Printf("%T\n", 'a')
var c1, c2 rune = '\u6211', '們'
println(c1 == '我', string(c2) == "\xe4\xbb\xac")
}
輸出:
int32 // rune 是 int32 的別名
true true
要修改字符串,可先將其轉(zhuǎn)換成 []rune 或 []byte,完成后再轉(zhuǎn)換為 string。無論哪種轉(zhuǎn)換,都會(huì)重新分配內(nèi)存,并復(fù)制字節(jié)數(shù)組。
func main() {
s := "abcd"
bs := []byte(s)
bs[1] = 'B'
println(string(bs))
u := "電腦"
us := []rune(u)
us[1] = '話'
println(string(us))
}
輸出:
aBcd
電話
用 for 循環(huán)遍歷字符串時(shí),也有 byte 和 rune 兩種方式。
func main() {
s := "abc漢字"
for i := 0; i < len(s); i++ { // byte
fmt.Printf("%c,", s[i])
}
fmt.Println()
for _, r := range s { // rune
fmt.Printf("%c,", r)
}
}
輸出:
a,b,c,,±,,,,,
a,b,c,漢,字,
支持指針類型 *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) // 直接用指針訪問目標(biāo)對(duì)象成員,無須轉(zhuǎn)換。
}
輸出:
0x2101ef018, 1234
不能對(duì)指針做加減法等運(yùn)算。
x := 1234
p := &x
p++ // Error: invalid operation: p += 1 (mismatched types *int and int)
可以在 unsafe.Pointer 和任意類型指針間進(jìn)行轉(zhuǎn)換。
func main() {
x := 0x12345678
p := unsafe.Pointer(&x) // *int -> Pointer
n := (*[4]byte)(p) // Pointer -> *[4]byte
for i := 0; i < len(n); i++ {
fmt.Printf("%X ", n[i])
}
}
輸出:
78 56 34 12
返回局部變量指針是安全的,編譯器會(huì)根據(jù)需要將其分配在 GC Heap 上。
func test() *int {
x := 100
return &x // 在堆上分配 x 內(nèi)存。但在內(nèi)聯(lián)時(shí),也可能直接分配在目標(biāo)棧。
}
將 Pointer 轉(zhuǎn)換成 uintptr,可變相實(shí)現(xiàn)指針運(yùn)算。
func main() {
d := struct {
s string
x int
}{"abc", 100}
p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
p += unsafe.Offsetof(d.x) // uintptr + offset
p2 := unsafe.Pointer(p) // uintptr -> Pointer
px := (*int)(p2) // Pointer -> *int
*px = 200 // d.x = 200
fmt.Printf("%#v\n", d)
}
輸出:
struct { s string; x int }{s:"abc", x:200}
注意:GC 把 uintptr 當(dāng)成普通整數(shù)對(duì)象,它無法阻止 "關(guān)聯(lián)" 對(duì)象被回收。
可將類型分為命名和未命名兩大類。命名類型包括 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)外,它們之間沒有任何關(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
語言設(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)。
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/2.png" alt="" />
運(yùn)算符結(jié)合律全部從左到右。
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/3.png" alt="" />
簡(jiǎn)單位運(yùn)算演示。
0110 & 1011 = 0010 AND 都為1。
0110 | 1011 = 1111 OR 至少一個(gè)為1。
0110 ^ 1011 = 1101 XOR 只能一個(gè)為1。
0110 &^ 1011 = 0100 AND NOT 清除標(biāo)志位。
標(biāo)志位操作。
a := 0
a |= 1 << 2 // 0000100: 在 bit2 設(shè)置標(biāo)志位。
a |= 1 << 6 // 1000100: 在 bit6 設(shè)置標(biāo)志位
a = a &^ (1 << 6) // 0000100: 清除 bit6 標(biāo)志位。
不支持運(yùn)算符重載。尤其需要注意,"++"、"--" 是語句而非表達(dá)式。
n := 0
p := &n
// b := n++ // syntax error
// if n++ == 1 {} // syntax error
// ++n // syntax error
n++
*p++ // (*p)++
沒有 "~",取反運(yùn)算也用 "^"。
x := 1
x, ^x // 0001, -0010
初始化復(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
很特別的寫法:
x := 0
// if x > 10 // Error: missing condition in if statement
// {
// }
if n := "abc"; x > 0 { // 初始化語句未必就是定義變量,比如 println("init") 也是可以的。
println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括號(hào)位置。
println(n[1])
} else {
println(n[0])
}
不支持三元操作符 "a > b a : b"。
支持三種循環(huán)方式,包括類 while 語法。
s := "abc"
for i, n := 0, len(s); i < n; i++ { // 常見的 for 循環(huán),支持初始化語句。
println(s[i])
}
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
不要期望編譯器能理解你的想法,在初始化語句中計(jì)算出全部結(jié)果是個(gè)好主意。
func length(s string) int {
println("call length.")
return len(s)
}
func main() {
s := "abcd"
for i, n := 0, length(s); i < n; i++ { // 避免多次調(diào)用 length 函數(shù)。
println(i, s[i])
}
}
輸出:
call length.
0 97
1 98
2 99
3 100
類似迭代器操作,返回 (索引, 值) 或 (鍵, 值)。
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/4.png" alt="" />
可忽略不想要的返回值,或用 "_" 這個(gè)特殊變量。
s := "abc"
for i := range s { // 忽略 2nd value,支持 string/array/slice/map。
println(s[i])
}
for _, c := range s { // 忽略 index。
println(c)
}
for range s { // 忽略全部返回值,僅迭代。
...
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m { // 返回 (key, value)。
println(k, v)
}
注意,range 會(huì)復(fù)制對(duì)象。
a := [3]int{0, 1, 2}
for i, v := range a { // index、value 都是從復(fù)制品中取出。
if i == 0 { // 在修改前,我們先修改原數(shù)組。
a[1], a[2] = 999, 999
fmt.Println(a) // 確認(rèn)修改有效,輸出 [0, 999, 999]。
}
a[i] = v + 100 // 使用復(fù)制品中取出的 value 修改原數(shù)組。
}
fmt.Println(a) // 輸出 [100, 101, 102]。
建議改用引用類型,其底層數(shù)據(jù)不會(huì)被復(fù)制。
s := []int{1, 2, 3, 4, 5}
for i, v := range s { // 復(fù)制 struct slice { pointer, len, cap }。
if i == 0 {
s = s[:3] // 對(duì) slice 的修改,不會(huì)影響 range。
s[2] = 100 // 對(duì)底層數(shù)據(jù)的修改。
}
println(i, v)
}
輸出:
0 1
1 2
2 100
3 4
4 5
另外兩種引用類型 map、channel 是指針包裝,而不像 slice 是 struct。
分支表達(dá)式可以是任意類型,不限于常量??墒÷?break,默認(rèn)自動(dòng)終止。
x := []int{1, 2, 3}
i := 2
switch i {
case x[1]:
println("a")
case 1, 3:
println("b")
default:
println("c")
}
輸出:
a
如需要繼續(xù)下一分支,可使用 fallthrough,但不再判斷條件。
x := 10
switch x {
case 10:
println("a")
fallthrough
case 0:
println("b")
}
輸出:
a
b
省略條件表達(dá)式,可當(dāng) if...else if...else 使用。
switch {
case x[1] > 0:
println("a")
case x[1] < 0:
println("b")
default:
println("c")
}
switch i := x[2]; { // 帶初始化語句
case i > 0:
println("a")
case i < 0:
println("b")
default:
println("c")
}
支持在函數(shù)內(nèi) goto 跳轉(zhuǎn)。標(biāo)簽名區(qū)分大小寫,未使用標(biāo)簽引發(fā)錯(cuò)誤。
func main() {
var i int
for {
println(i)
i++
if i > 2 { goto BREAK }
}
BREAK:
println("break")
EXIT: // Error: label EXIT defined and not used
}
配合標(biāo)簽,break 和 continue 可在多級(jí)嵌套循環(huán)中跳出。
func main() {
L1:
for x := 0; x < 3; x++ {
L2:
for y := 0; y < 5; y++ {
if y > 2 { continue L2 }
if x > 1 { break L1 }
print(x, ":", y, " ")
}
println()
}
}
輸出:
0:0 0:1 0:2
1:0 1:1 1:2
附:break 可用于 for、switch、select,而 continue 僅能用于 for 循環(huán)。
x := 100
switch {
case x >= 0:
if x == 0 { break }
println(x)
}
不支持 嵌套 (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ù),必須有明確的終止語句,否則會(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í),必須展開。
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)用通過閉包讀取和修改。
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)致性能問題,尤其是在一個(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
沒有結(jié)構(gòu)化異常,使用 panic 拋出錯(cuò)誤,recover 捕獲錯(cuò)誤。
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 將 interface{} 轉(zhuǎn)型為具體類型。
}
}()
panic("panic error!")
}
由于 panic、recover 參數(shù)類型為 interface{},因此可拋出任何類型對(duì)象。
func panic(v interface{})
func recover() interface{}
延遲調(diào)用中引發(fā)的錯(cuò)誤,可被后續(xù)延遲調(diào)用捕獲,但僅最后一個(gè)錯(cuò)誤可被捕獲。
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
輸出:
defer panic
捕獲函數(shù) recover 只有在延遲調(diào)用內(nèi)直接調(diào)用才會(huì)終止錯(cuò)誤,否則總是返回 nil。任何未捕獲的錯(cuò)誤都會(huì)沿調(diào)用堆棧向外傳遞。
func test() {
defer recover() // 無效!
defer fmt.Println(recover()) // 無效!
defer func() {
func() {
println("defer inner")
recover() // 無效!
}()
}()
panic("test panic")
}
func main() {
test()
}
輸出:
defer inner
<nil>
panic: test panic
使用延遲匿名函數(shù)或下面這樣都是有效的。
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
如果需要保護(hù)代碼片段,可將代碼塊重構(gòu)成匿名函數(shù),如此可確保后續(xù)代碼被執(zhí)行。
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil { z = 0 }
}()
z = x / y
return
}()
println("x / y =", z)
}
除用 panic 引發(fā)中斷性錯(cuò)誤外,還可返回 error 類型錯(cuò)誤對(duì)象來表示函數(shù)調(diào)用狀態(tài)。
type error interface {
Error() string
}
標(biāo)準(zhǔn)庫(kù) errors.New 和 fmt.Errorf 函數(shù)用于創(chuàng)建實(shí)現(xiàn) error 接口的錯(cuò)誤對(duì)象。通過判斷錯(cuò)誤對(duì)象實(shí)例來確定具體錯(cuò)誤類型。
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 { return 0, ErrDivByZero }
return x / y, nil
}
func main() {
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
如何區(qū)別使用 panic 和 error 兩種方式?慣例是:導(dǎo)致關(guān)鍵流程出現(xiàn)不可修復(fù)性錯(cuò)誤的使用 panic,其他使用 error。
和以往認(rèn)知的數(shù)組有很大不同。
可用復(fù)合語句初始化。
a := [3]int{1, 2} // 未初始化元素值為0。
b := [...]int{1, 2, 3, 4} // 通過初始化值確定數(shù)組長(zhǎng)度。
c := [5]int{2: 100, 4:200} // 使用索引號(hào)初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素類型。
{"user2", 20}, // 別忘了最后一行的逗號(hào)。
}
支持多維數(shù)組。
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 緯度不能用 "..."。
值拷貝行為會(huì)造成性能問題,通常會(huì)建議使用 slice,或數(shù)組指針。
func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
}
func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)
test(a)
fmt.Println(a)
}
輸出:
a: 0x2101f9150
x: 0x2101f9170
[0 0]
內(nèi)置函數(shù) len 和 cap 都返回?cái)?shù)組長(zhǎng)度 (元素?cái)?shù)量)。
a := [2]int{}
println(len(a), cap(a)) // 2, 2
需要說明,slice 并不是數(shù)組或數(shù)組指針。它通過內(nèi)部指針和相關(guān)屬性引用數(shù)組片段,以實(shí)現(xiàn)變長(zhǎng)方案。
runtime.h
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/6.png" alt="" />
創(chuàng)建表達(dá)式使用的是元素索引號(hào),而非數(shù)量。
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/5.png" alt="" />
讀寫操作實(shí)際目標(biāo)是底層數(shù)組,只需注意索引號(hào)的差別。
data := [...]int{0, 1, 2, 3, 4, 5}
s := data[2:4]
s[0] += 100
s[1] += 200
fmt.Println(s)
fmt.Println(data)
輸出:
[102 203]
[0 1 102 203 4 5]
可直接創(chuàng)建 slice 對(duì)象,自動(dòng)分配底層數(shù)組。
s1 := []int{0, 1, 2, 3, 8: 100} // 通過初始化表達(dá)式構(gòu)造,可使用索引號(hào)。
fmt.Println(s1, len(s1), cap(s1))
s2 := make([]int, 6, 8) // 使用 make 創(chuàng)建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))
s3 := make([]int, 6) // 省略 cap,相當(dāng)于 cap = len。
fmt.Println(s3, len(s3), cap(s3))
輸出:
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6
使用 make 動(dòng)態(tài)創(chuàng)建 slice,避免了數(shù)組必須用常量做長(zhǎng)度的麻煩。還可用指針直接訪問底層數(shù)組,退化成普通數(shù)組操作。
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 獲取底層數(shù)組元素指針。
*p += 100
fmt.Println(s)
輸出:
[0 1 102 3]
至于 [][]T,是指元素類型為 []T 。
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
可直接修改 struct array/slice 成員。
d := [5]struct {
x int
}{}
s := d[:]
d[1].x = 10
s[2].x = 20
fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])
輸出:
[{0} {10} {20} {0} {0}]
0x20819c180, 0x20819c180
所謂 reslice,是基于已有 slice 創(chuàng)建新 slice 對(duì)象,以便在 cap 允許范圍內(nèi)調(diào)整屬性。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error
http://wiki.jikexueyuan.com/project/the-go-study-notes-fourth-edition/images/7.png" alt="" />
新對(duì)象依舊指向原底層數(shù)組。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s1[2] = 100
s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200
fmt.Println(s)
輸出:
[0 1 2 3 100 5 6 200 8 9]
向 slice 尾部添加數(shù)據(jù),返回新的 slice 對(duì)象。
s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)
s2 := append(s, 1)
fmt.Printf("%p\n", &s2)
fmt.Println(s, s2)
輸出:
0x210230000
0x210230040
[] [1]
簡(jiǎn)單點(diǎn)說,就是在 array[slice.high] 寫數(shù)據(jù)。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[:3]
s2 := append(s, 100, 200) // 添加多個(gè)值。
fmt.Println(data)
fmt.Println(s)
fmt.Println(s2)
輸出:
[0 1 2 100 200 5 6 7 8 9]
[0 1 2]
[0 1 2 100 200]
一旦超出原 slice.cap 限制,就會(huì)重新分配底層數(shù)組,即便原數(shù)組并未填滿。
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
s = append(s, 100, 200) // 一次 append 兩個(gè)值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底層數(shù)組,與原數(shù)組無關(guān)。
fmt.Println(&s[0], &data[0]) // 比對(duì)底層數(shù)組起始指針。
輸出:
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0x20819c180 0x20817c0c0
從輸出結(jié)果可以看出,append 后的 s 重新分配了底層數(shù)組,并復(fù)制數(shù)據(jù)。如果只追加一個(gè)值,則不會(huì)超過 s.cap 限制,也就不會(huì)重新分配。
通常以2倍容量重新分配底層數(shù)組。在大批量添加數(shù)據(jù)時(shí),建議一次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復(fù)制開銷。或初始化足夠長(zhǎng)的 len 屬性,改用索引號(hào)進(jìn)行操作。及時(shí)釋放不再使用的 slice 對(duì)象,避免持有過期數(shù)組,造成 GC 無法回收。
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
輸出:
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
函數(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 := map[int]struct {
name string
age int
}{
1: {"user1", 10}, // 可省略元素類型。
2: {"user2", 20},
}
println(m[1].name)
預(yù)先給 make 函數(shù)一個(gè)合理元素?cái)?shù)量參數(shù),有助于提升性能。因?yàn)槭孪壬暾?qǐng)一大塊內(nèi)存,可避免后續(xù)操作時(shí)頻繁擴(kuò)張。
m := make(map[string]int, 1000)
常見操作:
m := map[string]int{
"a": 1,
}
if v, ok := m["a"]; ok { // 判斷 key 是否存在。
println(v)
}
println(m["c"]) // 對(duì)于不存在的 key,直接返回 \0,不會(huì)出錯(cuò)。
m["b"] = 2 // 新增或修改。
delete(m, "c") // 刪除。如果 key 不存在,不會(huì)出錯(cuò)。
println(len(m)) // 獲取鍵值對(duì)數(shù)量。cap 無效。
for k, v := range m { // 迭代,可僅返回 key。隨機(jī)順序返回,每次都不相同。
println(k, v)
}
不能保證迭代返回次序,通常是隨機(jī)結(jié)果,具體和版本實(shí)現(xiàn)有關(guān)。
從 map 中取回的是一個(gè) value 臨時(shí)復(fù)制品,對(duì)其成員的修改是沒有任何意義的。
type user struct{ name string }
m := map[int]user{ // 當(dāng) map 因擴(kuò)張而重新哈希時(shí),各鍵值項(xiàng)存儲(chǔ)位置都會(huì)發(fā)生改變。 因此,map
1: {"user1"}, // 被設(shè)計(jì)成 not addressable。 類似 m[1].name 這種期望透過原 value
} // 指針修改成員的行為自然會(huì)被禁止。
m[1].name = "Tom" // Error: cannot assign to m[1].name
正確做法是完整替換 value 或使用指針。
u := m[1]
u.name = "Tom"
m[1] = u // 替換 value。
m2 := map[int]*user{
1: &user{"user1"},
}
m2[1].name = "Jack" // 返回的是指針復(fù)制品。透過指針修改原對(duì)象是允許的。
可以在迭代時(shí)安全刪除鍵值。但如果期間有新增操作,那么就不知道會(huì)有什么意外了。
for i := 0; i < 5; i++ {
m := map[int]string{
0: "a", 1: "a", 2: "a", 3: "a", 4: "a",
5: "a", 6: "a", 7: "a", 8: "a", 9: "a",
}
for k := range m {
m[k+k] = "x"
delete(m, k)
}
fmt.Println(m)
}
輸出:
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]
值類型,賦值和傳參會(huì)復(fù)制全部?jī)?nèi)容??捎?"_" 定義補(bǔ)位字段,支持指向自身類型的指針成員。
type Node struct {
_ int
id int
data *byte
next *Node
}
func main() {
n1 := Node{
id: 1,
data: nil,
}
n2 := Node{
id: 2,
data: nil,
next: &n1,
}
}
順序初始化必須包含全部字段,否則會(huì)出錯(cuò)。
type User struct {
name string
age int
}
u1 := User{"Tom", 20}
u2 := User{"Tom"} // Error: too few values in struct initializer
支持匿名結(jié)構(gòu),可用作結(jié)構(gòu)成員或定義變量。
type File struct {
name string
size int
attr struct {
perm int
owner int
}
}
f := File{
name: "test.txt",
size: 1025,
// attr: {0755, 1}, // Error: missing type in composite literal
}
f.attr.owner = 1
f.attr.perm = 0755
var attr = struct {
perm int
owner int
}{2, 0755}
f.attr = attr
支持 "=="、"!=" 相等操作符,可用作 map 鍵類型。
type User struct {
id int
name string
}
m := map[User]int{
User{1, "Tom"}: 100,
}
可定義字段標(biāo)簽,用反射讀取。標(biāo)簽是類型的組成部分。
var u1 struct { name string "username" }
var u2 struct { name string }
u2 = u1 // Error: cannot use u1 (type struct { name string "username" }) as
// type struct { name string } in assignment
空結(jié)構(gòu) "節(jié)省" 內(nèi)存,比如用來實(shí)現(xiàn) set 數(shù)據(jù)結(jié)構(gòu),或者實(shí)現(xiàn)沒有 "狀態(tài)" 只有方法的 "靜態(tài)類"。
var null struct{}
set := make(map[string]struct{})
set["a"] = null
匿名字段不過是一種語法糖,從根本上說,就是一個(gè)與成員類型同名 (不含包名) 的字段。被匿名嵌入的可以是任何類型,當(dāng)然也包括指針。
type User struct {
name string
}
type Manager struct {
User
title string
}
m := Manager{
User: User{"Tom"}, // 匿名字段的顯式字段名,和類型名相同。
title: "Administrator",
}
可以像普通字段那樣訪問匿名字段成員,編譯器從外向內(nèi)逐級(jí)查找所有層次的匿名字段,直到發(fā)現(xiàn)目標(biāo)或出錯(cuò)。
type Resource struct {
id int
}
type User struct {
Resource
name string
}
type Manager struct {
User
title string
}
var m Manager
m.id = 1
m.name = "Jack"
m.title = "Administrator"
外層同名字段會(huì)遮蔽嵌入字段成員,相同層次的同名字段也會(huì)讓編譯器無所適從。解決方法是使用顯式字段名。
type Resource struct {
id int
name string
}
type Classify struct {
id int
}
type User struct {
Resource // Resource.id 與 Classify.id 處于同一層次。
Classify
name string // 遮蔽 Resource.name。
}
u := User{
Resource{1, "people"},
Classify{100},
"Jack",
}
println(u.name) // User.name: Jack
println(u.Resource.name) // people
// println(u.id) // Error: ambiguous selector u.id
println(u.Classify.id) // 100
不能同時(shí)嵌入某一類型和其指針類型,因?yàn)樗鼈兠窒嗤?/p>
type