本章主要分成三個部分:第一部分包括基本語法和數據結構;第二部分討論方法和接口;第三部分介紹并發機制。 包、變量和函數 先看一個例子Packages.go:
package main import ( "fmt" "math/rand" ) func add(x int, y int) int { return x + y } func main() { fmt.Println("My favorite number is", rand.Intn(10)) fmt.Println(add(42, 13)) }
包:每個 Go 程序都是由包(package)組成的,程序運行的入口是包 main
。
導入:這個代碼用圓括號組合了導入,這是“打包”導入語句。同樣可以編寫多個導入語句,例如:
import "fmt" import "math"
這個程序使用并導入了包 "fmt" 和 "math/rand"
。包名與導入路徑的最后一個目錄一致。例如,如果你導入了"math/rand"
,那么你就可以在程序里面直接寫rand.Intn(10),但如果你導入的是"math"
,那么你就得寫math.rand.Intn(10)。
函數:函數可以沒有參數或接受多個參數。在這個例子中,add
接受兩個 int 類型的參數,注意類型在變量名之后。有沒有覺得很奇怪呢,下面就詳細解釋一下go這樣做的原因。 看一個c的例子:
int (*fp)(int (*ff)(int x, int y), int b)
我相信學過c的同學為了看懂這個都下了不少功夫吧,有沒有感覺到痛苦或者說痛苦之后的那一點點優越感。 什么?你還能看懂?那么再來一個:
int (*(*fp)(int (*)(int, int), int))(int, int);// 10s內看懂的同學給我回復下這個定義是啥,我請你吃飯。
用go語言的話這兩個例子的定義如下:
f func(func(int,int) int, int) int f func(func(int,int) int, int) func(int, int) int
是不是清晰多了,原來f返回的是一個函數啊。。。
函數參數: 當兩個或多個連續的函數命名參數是同一類型,則除了最后一個類型之外,其他都可以省略,比如func add(x int, y int) int可以寫為func add(x, y int) int 函數返回值:
函數可以返回任意數量的返回值,比如func swap(x, y string) (string, string)。 并且,函數的返回值還可以被命名,并且像變量那樣使用,比如
func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return }
沒有參數的 return 語句返回結果的當前值。 變量: var 語句定義了一個變量的列表;跟函數的參數列表一樣,類型在后面,可以定義在包或函數級別。比如:
package main import "fmt" var c, python, java bool func main() { var i int fmt.Println(i, c, python, java) }
變量的初始化: 變量定義可以包含初始值,每個變量對應一個。如果初始化是使用表達式,則可以省略類型;變量從初始值中獲得類型。比如:var i, j int = 1, 2或者var c, python, java = true, false, "no!" 短聲明變量:在函數中,:=
簡潔賦值語句在明確類型的地方,可以用于替代 var 定義。 函數外的每個語句都必須以關鍵字開始(var
、func
、等等),:=
結構不能使用在函數外。比如:
package main import "fmt" func main() { var i, j int = 1, 2 k := 3 c, python, java := true, false, "no!" fmt.Println(i, j, k, c, python, java) }
Go的基本類型:這里先簡單列一下,后面再具體講
bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 的別名 rune // int32 的別名// 代表一個Unicode碼 float32 float64 complex64 complex128
零值:變量在定義時沒有明確的初始化時會賦值為零值。數值類型為 0
,布爾類型為 false
,字符串為 ""
。 類型轉換:Go 的在不同類型之間的項目賦值時需要顯式轉換。表達式 T(v) 將值 v 轉換為類型 T
。比如:
var i int = 42 var f float64 = float64(i) var u uint = uint(f)
或者,更加簡單的形式:
i := 42 f := float64(i) u := uint(f)
類型推導:在定義一個變量但不指定其類型時(使用沒有類型的 var 或 := 語句), 變量的類型由右值推導得出。 var i int j := i // j 也是一個 int 但是當右邊包含了未指名類型的數字常量時,新的變量就可能是 int 、 float64 或complex128
。 這取決于常量的精度:
i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128
常量:常量的定義與變量類似,只不過使用 const 關鍵字,常量可以是字符、字符串、布爾或數字類型的值,常量不能使用 := 語法定義。 流程控制語句 for循環:Go 只有一種循環結構for循環。基本的 for 循環除了沒有了 ( )
之外(,看起來跟 C 或者 Java 中做的一樣,而 { }
是必須的。比如:
package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) }
死循環:如果省略了循環條件,循環就不會結束,因此可以用更簡潔地形式表達死循環。
package main func main() { for { } }
If語句:if 語句除了沒有了 ( )
之外,看起來跟 C 或者 Java 中的一樣,而 { }
是必須的。
func sqrt(x float64) string { if x < 0 { return sqrt(-x) + "i" } return fmt.Sprint(math.Sqrt(x)) }
可以在if條件之前執行一個簡單的語句,由這個語句定義的變量的作用域僅在 if 范圍之內,比如:
func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } return lim }
在 if 的便捷語句定義的變量同樣可以在任何對應的 else 塊中使用,比如:
func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } else { fmt.Printf("%g >= %g\n", v, lim) } // 這里開始就不能使用 v 了 return lim }
Switch語句:switch 的條件從上到下的執行,當匹配成功的時候停止,除非以 fallthrough 語句結束,否則分支會自動終止。比如:
switch i { case 0: case f(): }
當 i==0 時不會調用 f
。沒有條件的 switch 同 switch true
一樣,這一構造使得可以用更清晰的形式來編寫長的 if-then-else 鏈。
func main() { t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon.") default: fmt.Println("Good evening.") } }
Defer語句:defer 語句會延遲函數的執行直到上層函數返回,延遲調用的參數會立刻生成,但是在上層函數返回前函數都不會被調用。比如打印hello world:
func main() { defer fmt.Println("world") fmt.Println("hello") }
延遲的函數調用被壓入一個棧中。當函數返回時, 會按照后進先出的順序調用被延遲的函數調用。
func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done") }
輸出
counting done 9 8 7 6 5 4 3 2 1 0
這個功能用來做清理工作非常清晰且強大,可以用結構化編程的思路處理面向對象編程中類似析構函數的功能。舉一個實際點的例子:
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() return io.Copy(dst, src) }
復雜類型 指針:Go 具有指針。 指針保存了變量的內存地址,類型 T 是指向類型 T 的值的指針。其零值是 nil
,& 符號會生成一個指向其作用對象的指針, 符號表示指針指向的底層的值。
var p *int i := 42 p = &i fmt.Println(*p) // 通過指針 p 讀取 i *p = 21 // 通過指針 p 設置 i
與 C 不同,Go 沒有指針運算。 結構體:一個結構體(struct
)就是一個字段的集合,跟C類似,結構體字段使用點號來訪問,結構體字段可以通過結構體指針來訪問。
type Vertex struct { X int Y int } func main() { fmt.Println(Vertex{1, 2}) v.X = 4 fmt.Println(v.X) p := &v p.X = 1e9 }
結構體文法:結構體文法表示通過結構體字段的值作為列表來新分配一個結構體。使用 Name: 語法可以僅列出部分字段。(字段名的順序無關。)特殊的前綴 & 返回一個指向結構體的指針。
type Vertex struct { X, Y int } var ( v1 = Vertex{1, 2} // 類型為 Vertex v2 = Vertex{X: 1} // Y:0 被省略 v3 = Vertex{} // X:0 和 Y:0 p = &Vertex{1, 2} // 類型為 *Vertex )
數組:類型 [n]T 是一個有 n 個類型為 T 的值的數組,數組的長度是其類型的一部分,因此數組不能改變大小。 Slice:一個 slice 會指向一個序列的值,并且包含了長度信息,[]T 是一個元素類型為 T 的 slice。slice 可以重新切片,創建一個新的 slice 值指向相同的數組。s[lo:hi] 表示從 lo 到 hi-1 的 slice 元素,含兩端,因此s[lo:lo] 是空的。下標從0開始。
package main import "fmt" func main() { p := []int{2, 3, 5, 7, 11, 13} fmt.Println("p ==", p) fmt.Println("p[1:4] ==", p[1:4]) // 省略下標代表從 0 開始 fmt.Println("p[:3] ==", p[:3]) // 省略上標代表到 len(s) 結束 fmt.Println("p[4:] ==", p[4:]) }
構造slice:slice 由函數 make 創建。這會分配一個零長度的數組并且返回一個 slice 指向這個數組,比如:
a := make([]int, 5) // len(a)=5
為了指定容量,可傳遞第三個參數到 make
:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5 b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4
slice 的零值是 nil
,一個 nil 的 slice 的長度和容量是 0。 向slice添加元素: func append(s []T, vs ...T) []T append 的第一個參數 s 是一個類型為 T 的數組,append 的結果是一個包含原 slice 所有元素加上新添加的元素的 slice,如果 s 的底層數組太小,而不能容納所有值時,會分配一個更大的數組,返回的 slice 會指向這個新分配的數組。 range: for 循環的 range 格式可以對 slice 或者 map 進行迭代循環。
package main import "fmt" var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} func main() { for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } }
可以通過賦值給 _ 來忽略序號和值,如果只需要索引值,去掉“, value”的部分即可。
func main() { pow := make([]int, 10) for i := range pow { pow[i] = 1 << uint(i) } for _, value := range pow { fmt.Printf("%d\n", value) } }
map:map 在使用之前必須用 make 而不是 new 來創建;值為 nil 的 map 是空的,并且不能賦值。
type Vertex struct { Lat, Long float64 } var m map[string]Vertex func main() { m = make(map[string]Vertex) m["Bell Labs"] = Vertex{ 40.68433, -74.39967, } fmt.Println(m["Bell Labs"]) }
map 的文法跟結構體文法相似,不過必須有鍵名。
var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, }, }
如果頂級的類型只有類型名的話,可以在文法的元素中省略鍵名。
var m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408}, }
修改 map: 在 map m 中插入或修改一個元素:m[key] = elem 獲得元素:elem = m[key] 刪除元素:delete(m, key) 通過雙賦值檢測某個鍵存在:elem, ok = m[key] 如果 key 在 m 中,ok
為 true 。否則, ok 為 false
,并且 elem 是 map 的元素類型的零值,同樣的,當從 map 中讀取某個不存在的鍵時,結果是 map 的元素類型的零值。
函數值:函數也是值。
package main import ( "fmt" "math" ) func main() { hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(3, 4)) }
函數的閉包:這個意思簡單點就是以函數作為返回值。 Go 函數可以是閉包的。閉包是一個函數值,它來自函數體的外部的變量引用。 函數可以對這個引用值進行訪問和賦值;換句話說這個函數被“綁定”在這個變量上。
package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } }