1.go版本的hello world
package main
import "fmt"
func main() {
// 終端輸出hello world
fmt.Println("Hello world!")
}
和C語言相似,go語言的基本組成有:
*包聲明,編寫源文件時,必須在非注釋的第一行指明這個文件屬于哪個包,如package main。
*引入包,其實(shí)就是告訴Go 編譯器這個程序需要使用的包,如import "fmt"其實(shí)就是引入了fmt包。
*函數(shù),和c語言相同,即是一個可以實(shí)現(xiàn)某一個功能的函數(shù)體,每一個可執(zhí)行程序中必須擁有一個main函數(shù)。
*變量,Go 語言變量名由字母、數(shù)字、下劃線組成,其中首個字符不能為數(shù)字。
*語句/表達(dá)式,在 Go 程序中,一行代表一個語句結(jié)束。每個語句不需要像 C 家族中的其它語言一樣以分號 ; 結(jié)尾,因?yàn)檫@些工作都將由 Go 編譯器自動完成。
注釋,和c語言中的注釋方式相同,可以在任何地方使用以 // 開頭的單行注釋。以 / 開頭,并以 */ 結(jié)尾來進(jìn)行多行注釋,且不可以嵌套使用,多行注釋一般用于包的文檔描述或注釋成塊的代碼片段。
需要注意的是:標(biāo)識符是用來命名變量、類型等程序?qū)嶓w。一個標(biāo)識符實(shí)際上就是一個或是多個字母和數(shù)字、下劃線_組成的序列,但是第一個字符必須是字母或下劃線而不能是數(shù)字。
2.數(shù)據(jù)類型
在 Go 編程語言中,數(shù)據(jù)類型用于聲明函數(shù)和變量。
數(shù)據(jù)類型的出現(xiàn)是為了把數(shù)據(jù)分成所需內(nèi)存大小不同的數(shù)據(jù),編程的時候需要用大數(shù)據(jù)的時候才需要申請大內(nèi)存,就可以充分利用內(nèi)存。具體分類如下:

2.0 定義變量
聲明變量的一般形式是使用 var 關(guān)鍵字,具體格式為:var identifier typename。如下的代碼中我們定義了一個類型為int的變量。
package main
import "fmt"
func main() {
var a int = 27
fmt.Println(a);
}
2.0.1 如果變量沒有初始化
在go語言中定義了一個變量,指定變量類型,如果沒有初始化,則變量默認(rèn)為零值。零值就是變量沒有做初始化時系統(tǒng)默認(rèn)設(shè)置的值。

2.0.2 如果變量沒有指定類型
在go語言中如果沒有指定變量類型,可以通過變量的初始值來判斷變量類型。如下代碼
package main
import "fmt"
func main() {
var d = true
fmt.Println(d)
}
2.0.3 :=符號
當(dāng)我們定義一個變量后又使用該符號初始化變量,就會產(chǎn)生編譯錯誤,因?yàn)樵摲柶鋵?shí)是一個聲明語句。
使用格式:typename := value
也就是說intVal := 1相等于:
var intVal int
intVal =1
2.0.4 多變量聲明
可以同時聲明多個類型相同的變量(非全局變量),如下圖所示:
var x, y int
var c, d int = 1, 2
g, h := 123, "hello"
關(guān)于全局變量的聲明如下:
var ( vname1 v_type1 vname2 v_type2 )
具體舉例如下:
var (
a int
b bool
)
2.0.5 匿名變量
匿名變量的特點(diǎn)是一個下畫線_,這本身就是一個特殊的標(biāo)識符,被稱為空白標(biāo)識符。它可以像其他標(biāo)識符那樣用于變量的聲明或賦值(任何類型都可以賦值給它),但任何賦給這個標(biāo)識符的值都將被拋棄,因此這些值不能在后續(xù)的代碼中使用,也不可以使用這個標(biāo)識符作為變量對其它變量進(jìn)行賦值或運(yùn)算。
使用匿名變量時,只需要在變量聲明的地方使用下畫線替換即可。
func GetData() (int, int) {
return 10, 20
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}
需要注意的是匿名變量不占用內(nèi)存空間,不會分配內(nèi)存。匿名變量與匿名變量之間也不會因?yàn)槎啻温暶鞫鵁o法使用。
2.0.6 變量作用域
作用域指的是已聲明的標(biāo)識符所表示的常量、類型、函數(shù)或者包在源代碼中的作用范圍,在此我們主要看一下go中變量的作用域,根據(jù)變量定義位置的不同,可以分為一下三個類型:
1.函數(shù)內(nèi)定義的變量為局部變量,這種局部變量的作用域只在函數(shù)體內(nèi),函數(shù)的參數(shù)和返回值變量都屬于局部變量。這種變量在存在于函數(shù)被調(diào)用時,銷毀于函數(shù)調(diào)用結(jié)束后。
2.函數(shù)外定義的變量為全局變量,全局變量只需要在一個源文件中定義,就可以在所有源文件中使用,甚至可以使用import引入外部包來使用。全局變量聲明必須以 var 關(guān)鍵字開頭,如果想要在外部包中使用全局變量的首字母必須大寫。
3.函數(shù)定義中的變量成為形式參數(shù),定義函數(shù)時函數(shù)名后面括號中的變量叫做形式參數(shù)(簡稱形參)。形式參數(shù)只在函數(shù)調(diào)用時才會生效,函數(shù)調(diào)用結(jié)束后就會被銷毀,在函數(shù)未被調(diào)用時,函數(shù)的形參并不占用實(shí)際的存儲單元,也沒有實(shí)際值。形式參數(shù)會作為函數(shù)的局部變量來使用。
2.1 基本類型

以上就是go語言基本的數(shù)據(jù)類型,有了數(shù)據(jù)類型,我們就可以使用這些類型來定義變量,Go 語言變量名由字母、數(shù)字、下劃線組成,其中首個字符不能為數(shù)字。
2.2 指針
與C相同,Go語言讓程序員決定何時使用指針。變量其實(shí)是一種使用方便的占位符,用于引用計算機(jī)內(nèi)存地址。Go 語言中的的取地址符是&,放到一個變量前使用就會返回相應(yīng)變量的內(nèi)存地址。
指針變量其實(shí)就是用于存放某一個對象的內(nèi)存地址。
2.2.1 指針聲明和初始化
和基礎(chǔ)類型數(shù)據(jù)相同,在使用指針變量之前我們首先需要申明指針,聲明格式如下:var var_name var-type,其中的var-type 為指針類型,var_name 為指針變量名, 號用于指定變量是作為一個指針。
代碼舉例如下:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮點(diǎn)型 */
指針的初始化就是取出相對應(yīng)的變量地址對指針進(jìn)行賦值,具體如下:
var a int= 20 /* 聲明實(shí)際變量 */
var ip *int /* 聲明指針變量 */
ip = &a /* 指針變量的存儲地址 */
2.2.2 空指針
當(dāng)一個指針被定義后沒有分配到任何變量時,它的值為 nil,也稱為空指針。它概念上和其它語言的null、NULL一樣,都指代零值或空值。
2.3 數(shù)組
和c語言相同,Go語言也提供了數(shù)組類型的數(shù)據(jù)結(jié)構(gòu),數(shù)組是具有相同唯一類型的一組已編號且長度固定的數(shù)據(jù)項序列,這種類型可以是任意的原始類型例如整型、字符串或者自定義類型。
2.3.1 聲明數(shù)組
Go 語言數(shù)組聲明需要指定元素類型及元素個數(shù),語法格式如下:
var variable_name [SIZE] variable_type
以上就可以定一個一維數(shù)組,我們舉例代碼如下:
var balance [10] float32
2.3.2 初始化數(shù)組
數(shù)組的初始化方式有不止一種方式,我們列舉如下:
1.直接進(jìn)行初始化:var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
2.通過字面量在聲明數(shù)組的同時快速初始化數(shù)組:balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
3.數(shù)組長度不確定,編譯器通過元素個數(shù)自行推斷數(shù)組長度,在[ ]中填入...,舉例如下:var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}和balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
4.數(shù)組長度確定,指定下標(biāo)進(jìn)行部分初始化:balanced := [5]float32(1:2.0, 3:7.0)
注意:
初始化數(shù)組中 {} 中的元素個數(shù)不能大于 [] 中的數(shù)字。
如果忽略 [] 中的數(shù)字不設(shè)置數(shù)組大小,Go 語言會根據(jù)元素的個數(shù)來設(shè)置數(shù)組的大小。
2.3.3 go中的數(shù)組名意義
在c語言中我們知道數(shù)組名在本質(zhì)上是數(shù)組中第一個元素的地址,而在go語言中,數(shù)組名僅僅表示整個數(shù)組,是一個完整的值,一個數(shù)組變量即是表示整個數(shù)組。
所以在go中一個數(shù)組變量被賦值或者被傳遞的時候?qū)嶋H上就會復(fù)制整個數(shù)組。如果數(shù)組比較大的話,這種復(fù)制往往會占有很大的開銷。所以為了避免這種開銷,往往需要傳遞一個指向數(shù)組的指針,這個數(shù)組指針并不是數(shù)組。關(guān)于數(shù)組指針具體在指針的部分深入的了解。
2.3.4 數(shù)組指針
通過數(shù)組和指針的知識我們就可以定義一個數(shù)組指針,代碼如下:
var a = [...]int{1, 2, 3} // a 是一個數(shù)組
var b = &a // b 是指向數(shù)組的指針
數(shù)組指針除了可以防止數(shù)組作為參數(shù)傳遞的時候浪費(fèi)空間,還可以利用其和for range來遍歷數(shù)組,具體代碼如下:
for i, v := range b { // 通過數(shù)組指針迭代數(shù)組的元素
fmt.Println(i, v)
}
具體關(guān)于go語言的循環(huán)語句我們在后文中再進(jìn)行詳細(xì)介紹。
2.4 結(jié)構(gòu)體
通過上述數(shù)組的學(xué)習(xí),我們就可以直接定義多個同類型的變量,但這往往也是一種限制,只能存儲同一種類型的數(shù)據(jù),而我們在結(jié)構(gòu)體中就可以定義多個不同的數(shù)據(jù)類型。
2.4.1 聲明結(jié)構(gòu)體
在聲明結(jié)構(gòu)體之前我們首先需要定義一個結(jié)構(gòu)體類型,這需要使用type和struct,type用于設(shè)定結(jié)構(gòu)體的名稱,struct用于定義一個新的數(shù)據(jù)類型。具體結(jié)構(gòu)如下:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
定義好了結(jié)構(gòu)體類型,我們就可以使用該結(jié)構(gòu)體聲明這樣一個結(jié)構(gòu)體變量,語法如下:
variable_name := structure_variable_type {value1, value2...valuen}
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
2.4.2 訪問結(jié)構(gòu)體成員
如果要訪問結(jié)構(gòu)體成員,需要使用點(diǎn)號 . 操作符,格式為:結(jié)構(gòu)體變量名.成員名。舉例代碼如下:
package main
import "fmt"
type Books struct {
title string
author string
}
func main() {
var book1 Books
Book1.title = "Go 語言入門"
Book1.author = "mars.hao"
}
2.4.3 結(jié)構(gòu)體指針
關(guān)于結(jié)構(gòu)體指針的定義和申明同樣可以套用前文中講到的指針的相關(guān)定義,從而使用一個指針變量存放一個結(jié)構(gòu)體變量的地址。
定義一個結(jié)構(gòu)體變量的語法:var struct_pointer *Books。
這種指針變量的初始化和上文指針部分的初始化方式相同struct_pointer = &Book1,但是和c語言中有所不同,使用結(jié)構(gòu)體指針訪問結(jié)構(gòu)體成員仍然使用.操作符。格式如下:struct_pointer.title
2.5 字符串
一個字符串是一個不可改變的字節(jié)序列,字符串通常是用來包含人類可讀的文本數(shù)據(jù)。和數(shù)組不同的是,字符串的元素不可修改,是一個只讀的字節(jié)數(shù)組。每個字符串的長度雖然也是固定的,但是字符串的長度并不是字符串類型的一部分。
2.5.1 字符串定義和初始化
Go語言字符串的底層結(jié)構(gòu)在reflect.StringHeader中定義,具體如下:
type StringHeader struct {
Data uintptr
Len int
}
也就是說字符串結(jié)構(gòu)由兩個信息組成:第一個是字符串指向的底層字節(jié)數(shù)組,第二個是字符串的字節(jié)的長度。
字符串其實(shí)是一個結(jié)構(gòu)體,因此字符串的賦值操作也就是reflect.StringHeader結(jié)構(gòu)體的復(fù)制過程,并不會涉及底層字節(jié)數(shù)組的復(fù)制,所以我們也可以將字符串?dāng)?shù)組看作一個結(jié)構(gòu)體數(shù)組。
字符串和數(shù)組類似,內(nèi)置的len函數(shù)返回字符串的長度。
2.5.2 字符串UTF8編碼
根據(jù)Go語言規(guī)范,Go語言的源文件都是采用UTF8編碼。因此,Go源文件中出現(xiàn)的字符串面值常量一般也是UTF8編碼的(對于轉(zhuǎn)義字符,則沒有這個限制)。提到Go字符串時,我們一般都會假設(shè)字符串對應(yīng)的是一個合法的UTF8編碼的字符序列。
Go語言的字符串中可以存放任意的二進(jìn)制字節(jié)序列,而且即使是UTF8字符序列也可能會遇到壞的編碼。如果遇到一個錯誤的UTF8編碼輸入,將生成一個特別的Unicode字符‘\uFFFD’,這個字符在不同的軟件中的顯示效果可能不太一樣,在印刷中這個符號通常是一個黑色六角形或鉆石形狀,里面包含一個白色的問號‘?’。
下面的字符串中,我們故意損壞了第一字符的第二和第三字節(jié),因此第一字符將會打印為“?”,第二和第三字節(jié)則被忽略;后面的“abc”依然可以正常解碼打?。ㄥe誤編碼不會向后擴(kuò)散是UTF8編碼的優(yōu)秀特性之一)。代碼如下:
fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") // ?界abc
不過在for range迭代這個含有損壞的UTF8字符串時,第一字符的第二和第三字節(jié)依然會被單獨(dú)迭代到,不過此時迭代的值是損壞后的0:
// 0 65533 // \uFFFD, 對應(yīng) ?
// 1 0 // 空字符
// 2 0 // 空字符
// 3 30028 // 界
// 6 97 // a
// 7 98 // b
// 8 99 // c
2.5.3 字符串的強(qiáng)制類型轉(zhuǎn)換
在上文中我們知道源代碼往往會采用UTF8編碼,如果不想解碼UTF8字符串,想直接遍歷原始的字節(jié)碼:
1.可以將字符串強(qiáng)制轉(zhuǎn)為[]byte字節(jié)序列后再行遍歷(這里的轉(zhuǎn)換一般不會產(chǎn)生運(yùn)行時開銷):
2.采用傳統(tǒng)的下標(biāo)方式遍歷字符串的字節(jié)數(shù)組
除此以外,字符串相關(guān)的強(qiáng)制類型轉(zhuǎn)換主要涉及到[]byte和[]rune兩種類型。每個轉(zhuǎn)換都可能隱含重新分配內(nèi)存的代價,最壞的情況下它們的運(yùn)算時間復(fù)雜度都是O(n)。
不過字符串和[]rune的轉(zhuǎn)換要更為特殊一些,因?yàn)橐话氵@種強(qiáng)制類型轉(zhuǎn)換要求兩個類型的底層內(nèi)存結(jié)構(gòu)要盡量一致,顯然它們底層對應(yīng)的[]byte和[]int32類型是完全不同的內(nèi)部布局,因此這種轉(zhuǎn)換可能隱含重新分配內(nèi)存的操作。
2.6 slice
簡單地說,切片就是一種簡化版的動態(tài)數(shù)組。因?yàn)閯討B(tài)數(shù)組的長度不固定,切片的長度自然也就不能是類型的組成部分了。數(shù)組雖然有適用它們的地方,但是數(shù)組的類型和操作都不夠靈活,而切片則使用得相當(dāng)廣泛。
切片高效操作的要點(diǎn)是要降低內(nèi)存分配的次數(shù),盡量保證append操作(在后續(xù)的插入和刪除操作中都涉及到這個函數(shù))不會超出cap的容量,降低觸發(fā)內(nèi)存分配的次數(shù)和每次分配內(nèi)存大小。
2.6.1 slice定義
我們先看看切片的結(jié)構(gòu)定義,reflect.SliceHeader:
type SliceHeader struct {
Data uintptr // 指向底層的的數(shù)組指針
Len int // 切片長度
Cap int // 切片最大長度
}
和數(shù)組一樣,內(nèi)置的len函數(shù)返回切片中有效元素的長度,內(nèi)置的cap函數(shù)返回切片容量大小,容量必須大于或等于切片的長度。
切片可以和nil進(jìn)行比較,只有當(dāng)切片底層數(shù)據(jù)指針為空時切片本身為nil,這時候切片的長度和容量信息將是無效的。如果有切片的底層數(shù)據(jù)指針為空,但是長度和容量不為0的情況,那么說明切片本身已經(jīng)被損壞了
只要是切片的底層數(shù)據(jù)指針、長度和容量沒有發(fā)生變化的話,對切片的遍歷、元素的讀取和修改都和數(shù)組是一樣的。在對切片本身賦值或參數(shù)傳遞時,和數(shù)組指針的操作方式類似,只是復(fù)制切片頭信息(reflect.SliceHeader),并不會復(fù)制底層的數(shù)據(jù)。對于類型,和數(shù)組的最大不同是,切片的類型和長度信息無關(guān),只要是相同類型元素構(gòu)成的切片均對應(yīng)相同的切片類型。
當(dāng)我們想定義聲明一個切片時可以如下:
在對切片本身賦值或參數(shù)傳遞時,和數(shù)組指針的操作方式類似,只是復(fù)制切片頭信息·(reflect.SliceHeader),并不會復(fù)制底層的數(shù)據(jù)。對于類型,和數(shù)組的最大不同是,切片的類型和長度信息無關(guān),只要是相同類型元素構(gòu)成的切片均對應(yīng)相同的切片類型。
2.6.2 添加元素
append() :內(nèi)置的泛型函數(shù),可以向切片中增加元素。
1.在切片尾部追加N個元素
var a []int
a = append(a, 1) // 追加1個元素
a = append(a, 1, 2, 3) // 追加多個元素, 手寫解包方式
a = append(a, []int{1,2,3}...) // 追加一個切片, 切片需要解包
注意:尾部添加在容量不足的條件下需要重新分配內(nèi)存,可能導(dǎo)致巨大的內(nèi)存分配和復(fù)制數(shù)據(jù)代價。即使容量足夠,依然需要用append函數(shù)的返回值來更新切片本身,因?yàn)樾虑衅拈L度已經(jīng)發(fā)生了變化。
2.在切片開頭位置添加元素
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在開頭位置添加1個元素
a = append([]int{-3,-2,-1}, a...) // 在開頭添加1個切片
3.append鏈?zhǔn)讲僮?/p>
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個位置插入切片
每個添加操作中的第二個append調(diào)用都會創(chuàng)建一個臨時切片,并將a[i:]的內(nèi)容復(fù)制到新創(chuàng)建的切片中,然后將臨時創(chuàng)建的切片再追加到a[:i]。
4.append和copy組合
a = append(a, 0) // 切片擴(kuò)展1個空間
copy(a[i+1:], a[i:]) // a[i:]向后移動1個位置
a[i] = x // 設(shè)置新添加的元素
第三個操作中會創(chuàng)建一個臨時對象,我們可以借用copy函數(shù)避免這個操作,這種方式操作語句雖然冗長了一點(diǎn),但是相比前面的方法,可以減少中間創(chuàng)建的臨時切片。
2.6.3 刪除元素
根據(jù)要刪除元素的位置有三種情況:
1.從開頭位置刪除;
直接移動數(shù)據(jù)指針,代碼如下:
a = []int{1, 2, 3, ...}
a = a[1:] // 刪除開頭1個元素
a = a[N:] // 刪除開頭N個元素
將后面的數(shù)據(jù)向開頭移動,使用append原地完成(所謂原地完成是指在原有的切片數(shù)據(jù)對應(yīng)的內(nèi)存區(qū)間內(nèi)完成,不會導(dǎo)致內(nèi)存空間結(jié)構(gòu)的變化)
a = []int{1, 2, 3, ...}
a = append(a[:0], a[1:]...) // 刪除開頭1個元素
a = append(a[:0], a[N:]...) // 刪除開頭N個元素
使用copy將后續(xù)數(shù)據(jù)向前移動,代碼如下:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 刪除開頭1個元素
a = a[:copy(a, a[N:])] // 刪除開頭N個元素
2.從中間位置刪除;
對于刪除中間的元素,需要對剩余的元素進(jìn)行一次整體挪動,同樣可以用append或copy原地完成:
append刪除操作如下:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1], ...)
a = append(a[:i], a[i+N:], ...)
copy刪除操作如下:
a = []int{1, 2, 3}
a = a[:copy(a[:i], a[i+1:])] // 刪除中間1個元素
a = a[:copy(a[:i], a[i+N:])] // 刪除中間N個元素
3.從尾部刪除。
代碼如下所示:
a = []int{1, 2, 3, ...}
a = a[:len(a)-1] // 刪除尾部1個元素
a = a[:len(a)-N] // 刪除尾部N個元素
刪除切片尾部的元素是最快的
2.7 函數(shù)
為完成某一功能的程序指令(語句)的集合,稱為函數(shù)。
2.7.1 函數(shù)分類
在Go語言中,函數(shù)是第一類對象,我們可以將函數(shù)保持到變量中。函數(shù)主要有具名和匿名之分,包級函數(shù)一般都是具名函數(shù),具名函數(shù)是匿名函數(shù)的一種特例,當(dāng)匿名函數(shù)引用了外部作用域中的變量時就成了閉包函數(shù),閉包函數(shù)是函數(shù)式編程語言的核心。
舉例代碼如下:
1.具名函數(shù):就和c語言中的普通函數(shù)意義相同,具有函數(shù)名、返回值以及函數(shù)參數(shù)的函數(shù)。
func Add(a, b int) int {
return a+b
}
2.匿名函數(shù):指不需要定義函數(shù)名的一種函數(shù)實(shí)現(xiàn)方式,它由一個不帶函數(shù)名的函數(shù)聲明和函數(shù)體組成。
var Add = func(a, b int) int {
return a+b
}
解釋幾個名詞如下:
1.閉包函數(shù):返回為函數(shù)對象,不僅僅是一個函數(shù)對象,在該函數(shù)外還包裹了一層作用域,這使得,該函數(shù)無論在何處調(diào)用,優(yōu)先使用自己外層包裹的作用域。
2.一級對象:支持閉包的多數(shù)語言都將函數(shù)作為第一級對象,就是說函數(shù)可以存儲到變量中作為參數(shù)傳遞給其他函數(shù),最重要的是能夠被函數(shù)動態(tài)創(chuàng)建和返回。
3.包:go的每一個文件都是屬于一個包的,也就是說go是以包的形式來管理文件和項目目錄結(jié)構(gòu)的。
2.7.2 函數(shù)聲明和定義
Go 語言函數(shù)定義格式如下:
func fuction_name([parameter list])[return types]{
函數(shù)體
}

2.7.3 函數(shù)傳參
Go語言中的函數(shù)可以有多個參數(shù)和多個返回值,參數(shù)和返回值都是以傳值的方式和被調(diào)用者交換數(shù)據(jù)。在語法上,函數(shù)還支持可變數(shù)量的參數(shù),可變數(shù)量的參數(shù)必須是最后出現(xiàn)的參數(shù),可變數(shù)量的參數(shù)其實(shí)是一個切片類型的參數(shù)。
當(dāng)可變參數(shù)是一個空接口類型時,調(diào)用者是否解包可變參數(shù)會導(dǎo)致不同的結(jié)果,我們解釋一下解包的含義,代碼如下:
func main(){
var a = []int{1, 2, 3}
Print(a...) // 解包
Print(a) // 未解包
}
func Print(a ...int{}) {
fmt.Println(a...)
}
以上當(dāng)傳入?yún)?shù)為a...時即是對切片a進(jìn)行了解包,此時其實(shí)相當(dāng)于直接調(diào)用Print(1,2,3)。當(dāng)傳入?yún)?shù)直接為 a時等價于直接調(diào)用Print([]int{}{1,2,3})
2.7.4 函數(shù)返回值
不僅函數(shù)的參數(shù)可以有名字,也可以給函數(shù)的返回值命名。
舉例代碼如下:
func Find(m map[int]int, key int)(value int, ok bool) {
value,ok = m[key]
return
}
如果返回值命名了,可以通過名字來修改返回值,也可以通過defer語句在return語句之后修改返回值,舉例代碼如下:
func mian() {
for i := 0 ; i<3; i++ {
defer func() { println(i) }
}
}
// 該函數(shù)最終的輸出為:
// 3
// 3
// 3
以上代碼中如果沒有defer其實(shí)返回值就是0,1,2,但defer語句會在函數(shù)return之后才會執(zhí)行,也就是或只有以上函數(shù)在執(zhí)行結(jié)束return之后才會執(zhí)行defer語句,而該函數(shù)return時的i值將會達(dá)到3,所以最終的defer語句執(zhí)行printlin的輸出都是3。
defer語句延遲執(zhí)行的其實(shí)是一個匿名函數(shù),因?yàn)檫@個匿名函數(shù)捕獲了外部函數(shù)的局部變量v,這種函數(shù)我們一般叫閉包。閉包對捕獲的外部變量并不是傳值方式訪問,而是以引用的方式訪問。
這種方式往往會帶來一些問題,修復(fù)方法為在每一輪迭代中都為defer函數(shù)提供一個獨(dú)有的變量,修改代碼如下:
func main() {
for i := 0; i < 3; i++ {
i := i // 定義一個循環(huán)體內(nèi)局部變量i
defer func(){ println(i) } ()
}
}
func main() {
for i := 0; i < 3; i++ {
// 通過函數(shù)傳入i
// defer 語句會馬上對調(diào)用參數(shù)求值
// 不再捕獲,而是直接傳值
defer func(i int){ println(i) } (i)
}
}
2.7.5 遞歸調(diào)用
Go語言中,函數(shù)還可以直接或間接地調(diào)用自己,也就是支持遞歸調(diào)用。Go語言函數(shù)的遞歸調(diào)用深度邏輯上沒有限制,函數(shù)調(diào)用的棧是不會出現(xiàn)溢出錯誤的,因?yàn)镚o語言運(yùn)行時會根據(jù)需要動態(tài)地調(diào)整函數(shù)棧的大小。這部分的知識將會涉及goroutint和動態(tài)棧的相關(guān)知識,我們將會在之后的博文中向大家解釋。
它的語法和c很相似,格式如下:
func recursion() {
recursion() /* 函數(shù)調(diào)用自身 */
}
func main() {
recursion()
}
2.8 方法
方法一般是面向?qū)ο缶幊?OOP)的一個特性,在C++語言中方法對應(yīng)一個類對象的成員函數(shù),是關(guān)聯(lián)到具體對象上的虛表中的。但是Go語言的方法卻是關(guān)聯(lián)到類型的,這樣可以在編譯階段完成方法的靜態(tài)綁定。一個面向?qū)ο蟮某绦驎梅椒▉肀磉_(dá)其屬性對應(yīng)的操作,這樣使用這個對象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。
實(shí)現(xiàn)C語言中的一組函數(shù)如下:
// 文件對象
type File struct {
fd int
}
// 打開文件
func OpenFile(name string) (f *File, err error) {
// ...
}
// 關(guān)閉文件
func CloseFile(f *File) error {
// ...
}
// 讀文件數(shù)據(jù)
func ReadFile(f *File, offset int64, data []byte) int {
// ...
}
以上的三個函數(shù)都是普通的函數(shù),需要占用包級空間中的名字資源。不過CloseFile和ReadFile函數(shù)只是針對File類型對象的操作,這時候我們更希望這類函數(shù)和操作對象的類型緊密綁定在一起。
所以在go語言中我們修改如下:
// 關(guān)閉文件
func (f *File) CloseFile() error {
// ...
}
// 讀文件數(shù)據(jù)
func (f *File) ReadFile(offset int64, data []byte) int {
// ...
}
將CloseFile和ReadFile函數(shù)的第一個參數(shù)移動到函數(shù)名的開頭,這兩個函數(shù)就成了File類型獨(dú)有的方法了(而不是File對象方法)
從代碼角度看雖然只是一個小的改動,但是從編程哲學(xué)角度來看,Go語言已經(jīng)是進(jìn)入面向?qū)ο笳Z言的行列了。我們可以給任何自定義類型添加一個或多個方法。每種類型對應(yīng)的方法必須和類型的定義在同一個包中,因此是無法給int這類內(nèi)置類型添加方法的(因?yàn)榉椒ǖ亩x和類型的定義不在一個包中)。對于給定的類型,每個方法的名字必須是唯一的,同時方法和函數(shù)一樣也不支持重載。
2.9 接口
2.9.1 什么是接口
Go 語言提供了另外一種數(shù)據(jù)類型即接口,它把所有的具有共性的方法定義在一起,任何其他類型只要實(shí)現(xiàn)了這些方法就是實(shí)現(xiàn)了這個接口。
Go的接口類型是對其它類型行為的抽象和概括;因?yàn)榻涌陬愋筒粫吞囟ǖ膶?shí)現(xiàn)細(xì)節(jié)綁定在一起,通過這種抽象的方式我們可以讓對象更加靈活和更具有適應(yīng)能力。很多面向?qū)ο蟮恼Z言都有相似的接口概念,但Go語言中接口類型的獨(dú)特之處在于它是滿足隱式實(shí)現(xiàn)的鴨子類型。
所謂鴨子類型說的是:只要走起路來像鴨子、叫起來也像鴨子,那么就可以把它當(dāng)作鴨子。Go語言中的面向?qū)ο缶褪侨绱?,如果一個對象只要看起來像是某種接口類型的實(shí)現(xiàn),那么它就可以作為該接口類型使用。
就比如說在c語言中,使用printf在終端輸出的時候只能輸出有限類型的幾個變量,而在go中可以使用fmt.Printf,實(shí)際上是fmt.Fprintf向任意自定義的輸出流對象打印,甚至可以打印到網(wǎng)絡(luò)甚至是壓縮文件,同時打印的數(shù)據(jù)不限于語言內(nèi)置的基礎(chǔ)類型,任意隱士滿足fmt.Stringer接口的對象都可以打印,不滿足fmt.Stringer接口的依然可以通過反射的技術(shù)打印。
2.9.2 結(jié)構(gòu)體類型
interface實(shí)際上就是一個結(jié)構(gòu)體,包含兩個成員。其中一個成員是指向具體數(shù)據(jù)的指針,另一個成員中包含了類型信息。空接口和帶方法的接口略有不同,下面分別是空接口的數(shù)據(jù)結(jié)構(gòu):
struct Eface
{
Type* type;
void* data;
};
其中的Type指的是:
struct Type
{
uintptr size;
uint32 hash;
uint8 _unused;
uint8 align;
uint8 fieldAlign;
uint8 kind;
Alg *alg;
void *gc;
String *string;
UncommonType *x;
Type *ptrto;
};
和帶方法的接口使用的數(shù)據(jù)結(jié)構(gòu):
struct Iface
{
Itab* tab;
void* data;
};
其中的Iface指的是:
struct Itab
{
InterfaceType* inter;
Type* type;
Itab* link;
int32 bad;
int32 unused;
void (*fun[])(void); // 方法表
};
3.9.3 具體類型向接口類型賦值
將一個具體類型數(shù)據(jù)賦值給interface這樣的抽象類型,需要進(jìn)行類型轉(zhuǎn)換。這個轉(zhuǎn)換過程中涉及哪些操作呢?
如果轉(zhuǎn)換為空接口,返回一個Eface,將Eface中的data指針指向原型數(shù)據(jù),type指針會指向數(shù)據(jù)的Type結(jié)構(gòu)體。
如果將其轉(zhuǎn)化為帶方法的interface,需要進(jìn)行一次檢測,該類型必須實(shí)現(xiàn)interface中聲明的所有方法才可以進(jìn)行轉(zhuǎn)換,這個檢測將會在編譯過程中進(jìn)行。檢測過程具體實(shí)現(xiàn)式通過比較具體類型的方法表和接口類型的方法表來進(jìn)行的。
1.具體類型方法表:Type的UncommonType中有一個方法表,某個具體類型實(shí)現(xiàn)的所有方法都會被收集到這張表中。
2.接口類型方法表:Iface的Itab的InterfaceType中也有一張方法表,這張方法表中是接口所聲明的方法。Iface中的Itab的func域也是一張方法表,這張表中的每一項就是一個函數(shù)指針,也就是只有實(shí)現(xiàn)沒有聲明。
這兩處方法表都是排序過的,只需要一遍順序掃描進(jìn)行比較,應(yīng)該可以知道Type中否實(shí)現(xiàn)了接口中聲明的所有方法。最后還會將Type方法表中的函數(shù)指針,拷貝到Itab的fun字段中。Iface中的Itab的func域也是一張方法表,這張表中的每一項就是一個函數(shù)指針,也就是只有實(shí)現(xiàn)沒有聲明。
2.9.4 獲取接口類型數(shù)據(jù)的具體類型信息
接口類型轉(zhuǎn)換為具體類型(也就是反射,reflect),也涉及到了類型轉(zhuǎn)換。reflect包中的TypeOf和ValueOf函數(shù)來得到接口變量的Type和Value。
2.10 channel
2.10.1 相關(guān)結(jié)構(gòu)體定義
go中的channel是可以被存儲在變量中,可以作為參數(shù)傳遞給函數(shù),也可以作為函數(shù)返回值返回,我們先來看一下channel的結(jié)構(gòu)體定義:
struct Hchan
{
uintgo qcount; // 隊列q中的總數(shù)據(jù)數(shù)量
uintgo dataqsize; // 環(huán)形隊列q的數(shù)據(jù)大小
uint16 elemsize; // 當(dāng)前隊列的使用量
bool closed;
uint8 elemalign;
Alg* elemalg; // interface for element type
uintgo sendx; // 發(fā)送index
uintgo recvx; // 接收index
WaitQ recvq; // 因recv而阻塞的等待隊列
WaitQ sendq; // 因send而阻塞的等待隊列
Lock;
};
Hchan結(jié)構(gòu)體中的核心部分是存放channel數(shù)據(jù)的環(huán)形隊列,相關(guān)數(shù)據(jù)的作用已經(jīng)在其后做出了備注。在該結(jié)構(gòu)體中沒有存放數(shù)據(jù)的域,如果是帶緩沖區(qū)的chan,則緩沖區(qū)數(shù)據(jù)實(shí)際上是緊接著Hchan結(jié)構(gòu)體中分配的。
另一個重要部分就是recvq和sendq兩個鏈表,一個是因讀這個通道而導(dǎo)致阻塞的goroutine,另一個是因?yàn)閷戇@個通道而阻塞的goroutine。如果一個goroutine阻塞于channel了,那么它就被掛在recvq或sendq中。WaitQ是鏈表的定義,包含一個頭結(jié)點(diǎn)和一個尾結(jié)點(diǎn),該鏈表中中存放的成員是一個sudoG結(jié)構(gòu)體變量,具體定義如下:
struct SudoG
{
G* g; // g and selgen constitute
uint32 selgen; // a weak pointer to g
SudoG* link;
int64 releasetime;
byte* elem; // data element
};
該結(jié)構(gòu)體中最主要的是g和elem。elem用于存儲goroutine的數(shù)據(jù)。讀通道時,數(shù)據(jù)會從Hchan的隊列中拷貝到SudoG的elem域。寫通道時,數(shù)據(jù)則是由SudoG的elem域拷貝到Hchan的隊列中。
Hchan結(jié)構(gòu)如下:

2.10.2 阻塞式讀寫channel操作
寫操作代碼如下,其中的c就是channel,v指的是數(shù)據(jù):
c <- v
事實(shí)上基本的阻塞模式寫channel操作在底層運(yùn)行時庫中對應(yīng)的是一個runtime.chansend函數(shù)。具體如下:
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
其中的ep指的是變量v的地址,這里的傳值約定是調(diào)用者負(fù)責(zé)分配好ep的空間,僅需要簡單的取變量地址就好了,pres是在select中的通道操作中使用的。

阻塞模式讀操作的核心函數(shù)有兩種包裝如下:
1.chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
以及
2.chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected)
這兩種的區(qū)別主要在于返回值是否會返回一個bool類型值,該值只是用于判斷channel是否能讀取出數(shù)據(jù)。
讀寫操作的以上阻塞的過程類似,故而不再做出說明,我們補(bǔ)充三個細(xì)節(jié):
1.以上我們都強(qiáng)調(diào)是阻塞式的讀寫操作,其實(shí)相對應(yīng)的也有非阻塞的讀寫操作,使用過select-case來進(jìn)行調(diào)用的。
2.空通道,指的是將一個channel賦值為nil,或者調(diào)用后不適用make進(jìn)行初始化。讀寫空通道是永遠(yuǎn)阻塞的。
3.關(guān)閉的通道,永遠(yuǎn)不會阻塞,會返回一個通道數(shù)據(jù)類型的零值。首先將closed置為1,第二步收集讀等待隊列recvq的所有sg,每個sg的elem都設(shè)為類型零值,第三步收集寫等待隊列sendq的所有sg,每個sg的elem都設(shè)為nil,最后喚醒所有收集的sg。
2.10.3 非阻塞式讀寫channel操作
如上文所說,非阻塞式其實(shí)就是使用select-case來實(shí)現(xiàn),在編譯時將會被編譯為if-else。
如:
select {
case v = <-c:
...foo
default:
...bar
}
就會被編譯為:
if selectnbrecv(&v, c) {
...foo
} else {
...bar
}
至于其中的selectnbrecv相關(guān)的函數(shù)簡單地調(diào)runtime.chanrecv函數(shù),設(shè)置了一個參數(shù),告訴runtime.chanrecv函數(shù),當(dāng)不能完成操作時不要阻塞,而是返回失敗。
但是select中的case的執(zhí)行順序是隨機(jī)的,而不像switch中的case那樣一條一條的順序執(zhí)行。讓每一個select都對應(yīng)一個Select結(jié)構(gòu)體。在Select數(shù)據(jù)結(jié)構(gòu)中有個Scase數(shù)組,記錄下了每一個case,而Scase中包含了Hchan。然后pollorder數(shù)組將元素隨機(jī)排列,這樣就可以將Scase亂序了。
2.11 map
map表的底層原理是哈希表,其結(jié)構(gòu)體定義如下:
type Map struct {
Key *Type // Key type
Elem *Type // Val (elem) type
Bucket *Type // 哈希桶
Hmap *Type // 底層使用的哈希表元信息
Hiter *Type // 用于遍歷哈希表的迭代器
}
其中的Hmap 的具體化數(shù)據(jù)結(jié)構(gòu)如下:
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // map目前的元素數(shù)目
flags uint8 // map狀態(tài)(正在被遍歷/正在被寫入)
B uint8 // 哈希桶數(shù)目以2為底的對數(shù)(哈希桶的數(shù)目都是 2 的整數(shù)次冪,用位運(yùn)算來計算取余運(yùn)算的值, 即 N mod M = N & (M-1)))
noverflow uint16 //溢出桶的數(shù)目, 這個數(shù)值不是恒定精確的, 當(dāng)其 B>=16 時為近似值
hash0 uint32 // 隨機(jī)哈希種子
buckets unsafe.Pointer // 指向當(dāng)前哈希桶的指針
oldbuckets unsafe.Pointer // 擴(kuò)容時指向舊桶的指針
nevacuate uintptr // 桶進(jìn)行調(diào)整時指示的搬遷進(jìn)度
extra *mapextra // 表征溢出桶的變量
}
以上hmap基本都是涉及到了哈希桶和溢出桶,我們首先看一下它的數(shù)據(jù)結(jié)構(gòu),如下:
type bmap struct {
topbits [8]uint8 // 鍵哈希值的高8位
keys [8]keytype // 哈希桶中所有鍵
elems [8]elemtype // 哈希桶中所有值
//pad uintptr(新的 go 版本已經(jīng)移除了該字段, 我未具體了解此處的 change detail, 之前設(shè)置該字段是為了在 nacl/amd64p32 上的內(nèi)存對齊)
overflow uintptr
}
我們會發(fā)現(xiàn)哈希桶bmap一般指定其能保存8個鍵值對,如果多于8個鍵值對,就會申請新的buckets,并將其于之前的buckets鏈接在一起。
其中的聯(lián)系如圖所示:

在具體插入時,首先會根據(jù)key值采用相應(yīng)的hash算法計算對應(yīng)的哈希值,將哈希值的低8位作為Hmap結(jié)構(gòu)體中buckets數(shù)組的索引,找到key值所對應(yīng)的bucket,將哈希值的高8位催出在bucket的tophash中。
特點(diǎn)如下:
1.map是無序的(原因?yàn)闊o序?qū)懭胍约皵U(kuò)容導(dǎo)致的元素順序發(fā)生變化),每次打印出來的map都會不一樣,它不能通過index獲取,而必須通過key獲取
2.map的長度是不固定的,也就是和slice一樣,也是一種引用類型
內(nèi)置的len函數(shù)同樣適用于map,返回map擁有的key的數(shù)量
3.map的key可以是所有可比較的類型,如布爾型、整數(shù)型、浮點(diǎn)型、復(fù)雜型、字符串型……也可以鍵。
如下方式即可進(jìn)行初始化:
var a map[keytype]valuetype

除此以外還可以使用make進(jìn)行初始化,代碼如下:
map_variable = make(map[key_data_type]value_data_type)
我們還可以使用初始值進(jìn)行初始化,如下:
var m map[string]int = map[string]int{"hunter":12,"tony":10}
2.11.1 插入數(shù)據(jù)
map的數(shù)據(jù)插入代碼如下:
map_variable["mars"] = 27
插入過程如下:
1.根據(jù)key值計算出哈希值
2.取哈希值低位和hmap.B取模確定bucket位置
3.查找該key是否已經(jīng)存在,如果存在則直接更新值
4.如果沒有找到key,則將這一對key-value插入
2.11.2 刪除數(shù)據(jù)
delete(map, key) 函數(shù)用于刪除集合的元素, 參數(shù)為 map 和其對應(yīng)的 key。刪除函數(shù)不返回任何值。相關(guān)代碼如下:
countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}
/* 刪除元素 */
delete(countryCapitalMap,"France");
2.11.3 查找數(shù)據(jù)
通過key獲取map中對應(yīng)的value值。語法為:map[key] .但是當(dāng)key如果不存在的時候,我們會得到該value值類型的默認(rèn)值,比如string類型得到空字符串,int類型得到0。但是程序不會報錯。
所以我們可以使用ok-idiom獲取值,如下:value, ok := map[key] ,其中的value是返回值,ok是一個bool值,可知道key/value是否存在。
在map表中的查找過程如下:
1.查找或者操作map時,首先key經(jīng)過hash函數(shù)生成hash值
2.通過哈希值的低8位來判斷當(dāng)前數(shù)據(jù)屬于哪個桶
3.找到桶之后,通過哈希值的高八位與bucket存儲的高位哈希值循環(huán)比對
4.如果相同就比較剛才找到的底層數(shù)組的key值,如果key相同,取出value
5.如果高八位hash值在此bucket沒有,或者有,但是key不相同,就去鏈表中下一個溢出bucket中查找,直到查找到鏈表的末尾
6.如果查找不到,也不會返回空值,而是返回相應(yīng)類型的0值。
2.11.4 擴(kuò)容
哈希表就是以空間換時間,訪問速度是直接跟填充因子相關(guān)的,所以當(dāng)哈希表太滿之后就需要進(jìn)行擴(kuò)容。
如果擴(kuò)容前的哈希表大小為2B擴(kuò)容之后的大小為2(B+1),每次擴(kuò)容都變?yōu)樵瓉泶笮〉膬杀叮1泶笮∈冀K為2的指數(shù)倍,則有(hash mod 2B)等價于(hash & (2B-1))。這樣可以簡化運(yùn)算,避免了取余操作。
觸發(fā)擴(kuò)容的條件?
1.負(fù)載因子(負(fù)載因子 = 鍵數(shù)量/bucket數(shù)量) > 6.5時,也即平均每個bucket存儲的鍵值對達(dá)到6.5個。
2.溢出桶(overflow)數(shù)量 > 2^15時,也即overflow數(shù)量超過32768時。
什么是增量擴(kuò)容呢?
如果負(fù)載因子>6.5時,進(jìn)行增量擴(kuò)容。這時會新建一個桶(bucket),新的bucket長度是原來的2倍,然后舊桶數(shù)據(jù)搬遷到新桶。每個舊桶的鍵值對都會分流到兩個新桶中
主要是縮短map容器的響應(yīng)時間。假如我們直接將map用作某個響應(yīng)實(shí)時性要求非常高的web應(yīng)用存儲,如果不采用增量擴(kuò)容,當(dāng)map里面存儲的元素很多之后,擴(kuò)容時系統(tǒng)就會卡往,導(dǎo)致較長一段時間內(nèi)無法響應(yīng)請求。不過增量擴(kuò)容本質(zhì)上還是將總的擴(kuò)容時間分?jǐn)偟搅嗣恳淮喂2僮魃厦妗?/p>
什么是等量擴(kuò)容?它的觸發(fā)條件是什么?進(jìn)行等量擴(kuò)容后的優(yōu)勢是什么?
等量擴(kuò)容,就是創(chuàng)建和舊桶數(shù)目一樣多的新桶,然后把原來的鍵值對遷移到新桶中,重新做一遍類似增量擴(kuò)容的搬遷動作。
觸發(fā)條件:負(fù)載因子沒超標(biāo),溢出桶較多。這個較多的評判標(biāo)準(zhǔn)為:
1.如果常規(guī)桶數(shù)目不大于2^15,那么使用的溢出桶數(shù)目超過常規(guī)桶就算是多了;
2.如果常規(guī)桶數(shù)目大于215,那么使用溢出桶數(shù)目一旦超過215就算多了。
這樣做的目的是把松散的鍵值對重新排列一次,能夠存儲的更加緊湊,進(jìn)而減少溢出桶的使用,以使bucket的使用率更高,進(jìn)而保證更快的存取。
- 常用語句及關(guān)鍵字
接下來我們了解一下關(guān)于go語言語句的基本內(nèi)容。
4.1 條件語句
和c語言類似,相關(guān)的條件語句如下表所示

if語句
語法如下:
if 布爾表達(dá)式 {
/* 在布爾表達(dá)式為 true 時執(zhí)行 */
}
if-else語句
if 布爾表達(dá)式 {
/* 在布爾表達(dá)式為 true 時執(zhí)行 */
} else {
/* 在布爾表達(dá)式為 false 時執(zhí)行 */
}
switch語句
其中的變量v可以是任何類型,val1和val2可以是同類型的任意值,類型不局限為常量或者整數(shù),或者最終結(jié)果為相同類型的表達(dá)式。
switch v {
case val1:
...
case val2:
...
default:
...
}
select語句
select 是 Go 中的一個控制結(jié)構(gòu),類似于用于通信的 switch 語句。每個 case 必須是一個通信操作,要么是發(fā)送要么是接收。它將會隨機(jī)執(zhí)行一個可運(yùn)行的 case。如果沒有 case 可運(yùn)行,它將阻塞,直到有 case 可運(yùn)行。一個默認(rèn)的子句應(yīng)該總是可運(yùn)行的。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定義任意數(shù)量的 case */
default : /* 可選 */
statement(s);
}
注意:
每個case必須都是一個通信
所有channel表達(dá)式都會被求值,所有被發(fā)送的表達(dá)式都會被求值
如果任意某一個通信都可以,它就執(zhí)行,其他就忽略
如果有多個case都可以運(yùn)行,select就會隨機(jī)挑選一個來執(zhí)行。
如果沒有一個case可以被運(yùn)行:如果有default子句,就執(zhí)行default子句,select將被阻塞,直到某個通信可以運(yùn)行,從而避免饑餓問題。
3.2 循環(huán)語句
3.2.1 循環(huán)處理語句
go中時使用for實(shí)現(xiàn)循環(huán)的,共有三種形式:

除此以外,for循環(huán)還可以直接使用range對slice、map、數(shù)組以及字符串等進(jìn)行迭代循環(huán),格式如下:
for key, value := range oldmap {
newmap[key] = value
}
3.2.1 循環(huán)控制語句

1.break
break主要用于循環(huán)語句跳出循環(huán),和c語言中的使用方式是相同的。且在多重循環(huán)的時候還可以使用label標(biāo)出想要break的循環(huán)。
實(shí)例代碼如下:
a := 0
for a<5 {
fmt.Printf("%d\n", a)
a++
if a==2 {
break;
}
}
/* output
0
1
2
*/
2.continue
Go 語言的 continue 語句 有點(diǎn)像 break 語句。但是 continue 不是跳出循環(huán),而是跳過當(dāng)前循環(huán)執(zhí)行下一次循環(huán)語句。在多重循環(huán)中,可以用標(biāo)號 label 標(biāo)出想 continue 的循環(huán)。
實(shí)例代碼如下:
// 不使用標(biāo)記
fmt.Println("---- continue ---- ")
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue
}
}
/* output
i: 1
i2: 11
i2: 12
i2: 13
i: 2
i2: 11
i2: 12
i2: 13
i: 3
i2: 11
i2: 12
i2: 13
*/
// 使用標(biāo)記
fmt.Println("---- continue label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}
/* output
i: 1
i2: 11
i: 2
i2: 11
i: 3
i2: 11
*/
3.goto
goto語句主要是無條件轉(zhuǎn)移到過程中指定的行。goto語句通常和條件語句配合使用,可用來實(shí)現(xiàn)條件轉(zhuǎn)移、構(gòu)成循環(huán)以及跳出循環(huán)體等功能。但是并不主張使用goto語句,以免造成程序流程混亂。
示例代碼如下:
var a int = 0
LOOP: for a<5 {
if a == 2 {
a = a+1
goto LOOP
}
fmt.Printf("%d\n", a)
a++
}
/*
output:
0
1
2
3
4
*/
以上代碼中的LOOP就是一個標(biāo)簽,當(dāng)運(yùn)行到goto語句的時候,此時執(zhí)行流就會跳轉(zhuǎn)到LOOP標(biāo)志的哪一行上。
3.3 關(guān)鍵字
我們這一部分直接列表供大家了解go中的關(guān)鍵字如下:
