前言
學習Go半年之後,我決定重新開始閱讀《The Go Programing Language》,對書中涉及重點進行全面講解,這是Go語言知識查漏補缺系列的文章第三篇。
我也開源了一個Go語言的學習倉庫,有需要的同學可以關注,其中将整理往期精彩文章、以及Go相關電子書等資料。
倉庫位址:github.com/BaiZe1998/g…
而本文的内容就是針對《The Go Programing Language》第四、五章的整理,預計會用一個多月的時間完成這份筆記的更新。
差別于連篇累牍,我希望這份筆記是詳略得當的,可能更适合一些對Go有着一些使用經驗,但是由于是轉語言或者速食主義者,對Go的許多知識點并未了解深刻(與我一般),筆記中雖然會帶有一些個人的色彩,但是Go語言的重點我将悉數講解。
再啰嗦一句:筆記中講述一個知識點的時候有時并非完全講透,或是淺嘗辄止,或是抛出疑問卻未曾解答。希望你可以接受這種風格,而有些知識點後續涉及到後續章節,目前未過分剖析,也會在後面進行更深入的講解。
最後,如果遇到錯誤,或者你認為值得改進的地方,也很歡迎你評論或者聯系我進行更正,又或者你也可以直接在倉庫中提issue或者pr,或許這也是小小的一次“開源”。
四、複合類型
4.1 數組
長度不可變,如果兩個數組類型是相同的則可以進行比較,且隻有完全相等才會為true
a := [...]int{1, 2} // 數組的長度由内容長度确定
b := [2]int{1, 2}
c := [3]int{1, 2}
4.2 切片
切片由三部分組成:指針、長度(len)、容量(cap)
切片可以通過數組建立
// 建立月份數組
months := [...]string{1:"January", 省略部分内容, 12: "December"}
基于月份數組建立切片,且不同切片底層可能共享一片數組空間

fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // 如果未超過summer的cap,則會擴充slice的len
fmt.Println(endlessSummer) // "[June July August September October]"
[]byte切片可以通過對字元串使用類似上述操作的方式擷取
切片之間不可以使用==進行比較,隻有當其判斷是否為nil才可以使用
切片的zero value是nil,nil切片底層沒有配置設定數組,nil切片的len和cap都為0,但是非nil切片的len和cap也可以為0(Go中len == 0的切片處理方式基本相同)
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
The append Function
使用append為slice追加内容,如果cap == len,則會觸發slice擴容,下面是一個幫助了解的例子(使用了2倍擴容,并非是Go内置的append處理流程,那将會更加精細,api也更加豐富):
4.3 映射
map(hash table) — 無序集合,key必須是可以比較的(除了浮點數,這不是一個好的選擇)
x := make(map[string]int)
y := map[string]int{
"alice": 12,
"tom": 34
}
z := map[string]int{}
// 内置函數
delete(y, "alice")
對map的元素進行取位址并不是一個好的注意,因為map的擴容過程中可能伴随着rehash,導緻位址發生變化(那麼map的擴容規則?)
ages["carol"] = 21 // panic if assignment to entry in nil map
// 判斷key-value是否存在的方式
age, ok := ages["alice"]
if age, ok := ages["bob"]; !ok
4.4 結構體
type Point struct {
x, y int
}
type Circle struct {
center Point
radius int
}
type Wheel struct {
circle Circle
spokes int
}
w := Wheel{Circle{Point{8, 8}, 5}, 20}
w := Wheel{
circle: Circle{
center: Point{x: 8, y: 8},
radius: 5,
},
spokes: 20,
}
4.5 JSON
// 将結構體轉成存放json編碼的byte切片
type Movie struct {
Title string
Year int `json:"released"` // 重定義json屬性名稱
Color bool `json:"color,omitempty"` // 如果是空值則轉成json時忽略
}
data, err := json.Marshal(movie)
data2, err := json.MarshalIndent(movie, "", " ")
// 輸出結果
{"Title":"s","released":1,"color":true}
{
"Title": "s",
"released": 1,
"color": true
}
// json解碼
4.6 文本和HTML模闆
略
五、方法
5.1 方法聲明
// 可以提前聲明傳回值z
func add(x, y int) (z int) {
z = x-y
return
如果兩個方法的參數清單和傳回值清單相同,則稱之為擁有相同類型(same type)
參數是值拷貝,但是如果傳入的參數是:slice、pointer、map、function,channel雖然是值拷貝,但是也是引用類型的值,會對其指向的值做出相應變更
你可能會遇到檢視某些go的内置func源碼的時候它沒有聲明func的body部分,例如append方法
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
事實上append在代碼編譯的時候,被替換成runtime.growslice以及相關彙編指令了(可以輸出彙編代碼檢視細節),你可以在go的runtime包中找到相關實作,如下:
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO:
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if asanenabled {
asanread(old.array, uintptr(old.len*int(et.size)))
}
// 省略...
聲明函數時指定傳回值的名稱,可以在return時省略
func add(x, y int) (z int, err error) {
data, err := deal(x, y)
if err != nil {
return // 此時等價于return 0, nil
}
// 這裡是指派而不是聲明,因為在傳回值清單中聲明過了
z = x+y
return // 此時等價于return z, nil
5.2 錯誤
error是一個接口,是以可以自定義實作error
type error interface {
Error() string
如果一個函數執行失敗時需要傳回的行為很單一可以通過bool來控制
func test(a int) (y int, ok bool) {
x, ok := test1(a)
if !ok {
return
}
y = x*x
return
更多情況下,函數處理時可能遇到多種類型的錯誤,則使用error,可以通過判斷err是否為nil判斷是否發生錯誤
func test(a int) (y int, err error) {
x, err := test1(a)
if err != nil {
return
}
y = x*x
return
}
// 列印錯誤的值
fmt.Println(err)
fmt.Printf("%v", err)
Go通過if和return的機制手動傳回錯誤,使得錯誤的定位更加精确,并且促使你更早的去處理這些錯誤(而不是像其他語言一樣選擇抛出異常,可能使得異常由于調用棧的深入,導緻最終處理不便)
錯誤處理政策
一個func的調用傳回了err,則調用方有責任正确處理它,下面介紹五種常見處理方式:
- 傳遞:
// 某func部分節選
resp, err := http,Get(url)
if err != nil {
// 将對Get傳回的err處理交給目前func的調用方
return nil, err
}
fmt.Errorf()格式化,添加更多描述資訊,并建立一個了新的error(參考fmt.Sprintf的格式化)
當error最終被處理的時候,需要反映出其錯誤的調用鍊式關系
并且error的内容組織在一個項目中需要統一,以便于後期借助工具統一分析
- 錯誤重試
- 優雅關閉
如果無法處理,可以選擇優雅關閉程式,但是推薦将這步工作交給main包的程式,而庫函數則選擇将error傳遞給其調用方。
使用log.Fatalf更加友善
會預設輸出error的列印時間
- 選擇将錯誤列印
或者輸出到标準錯誤流
- 少數情況下,可以選擇忽略錯誤,并且如果錯誤選擇傳回,則正确情況下省略else,保持代碼整潔
EOF(End of File)
輸入的時候沒有更多内容則觸發io.EOF,并且這個error是提前定義好的
5.3 作為值的函數
函數是一種類型類型,可以作為參數,并且對應變量是“引用類型”,其零值為nil,相同類型可以指派
函數作為參數的例子,将一個引用類型的參數傳遞給多個func,可以為這個參數多次指派(Hertz架構中使用了這種擴充性的思想)
5.4 匿名函數
函數的顯式聲明需要在package層面,但是在函數的内部也可以建立匿名函數
從上可以看出f存放着匿名函數的引用,并且它是有狀态的,維護了一個遞增的x
捕獲疊代變量引發的問題
正确版本
錯誤版本
所有循環内建立的func捕獲并共享了dir變量(相當于引用類型),是以建立後rmdirs切片内所有元素都有同一個dir,而不是每個元素獲得dir周遊時的中間狀态
是以正确版本中dir := d的操作為周遊的dir申請了新的記憶體存放
func main() {
arr := []int{1, 2, 3, 4, 5}
temp := make([]func(), 0)
for _, value := range arr {
temp = append(temp, func() {
fmt.Println(value)
})
}
for i := range temp {
temp[i]()
}
}
// 結果
5
5
5
5
5
另一種錯誤版本(i最終達到數組長度上界後結束循環,并且導緻dirs[i]發生越界)
// 同樣是越界的測試函數
func main() {
arr := []int{1, 2, 3, 4, 5}
temp := make([]func(), 0)
for i := 0; i < 5; i++ {
temp = append(temp, func() {
fmt.Println(arr[i])
})
}
for i := range temp {
temp[i]()
}
}
// 結果
panic: runtime error: index out of range [5] with length 5
以上捕獲疊代變量引發的問題容易出現在延遲了func執行的情況下(先完成循環建立func、後執行func)
5.5 變參函數
vals此時是一個int類型的切片,下面是不同的調用方式
雖然...int參數的作用與[]int很相似,但是其類型還是不同的,變參函數經常用于字元串的格式化printf
測試
func test(arr ...int) int {
arr[0] = 5
sum := 0
for i := 0; i < len(arr); i++ {
sum += arr[i]
}
return sum
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(test(arr...))
fmt.Println(arr)
}
// 切片确實被修改了
19
[5 2 3 4 5]
5.6 延後函數調用
defer通常用于資源的釋放,對應于(open&close|connect&disconnect|lock&unlock)
defer最佳實踐是在資源申請的位置緊跟使用,defer在目前函數return之前觸發,如果有多個defer聲明,則後進先出順序觸發
defer也可以用于調試複雜的函數(通過return一個func的形式)
測試1:
func test() func() {
fmt.Println("start")
defer func() {
fmt.Println("test-defer")
}()
return func() {
fmt.Println("end")
}
}
func main() {
defer test()()
fmt.Println("middle")
}
// 輸出
start
test-defer
可以觀察到test()()分為兩步執行,start在defer聲明處列印,end在main函數return前列印,并且test内定義的defer也在test函數return前列印test-defer
此時start和end包圍了main函數,是以可以用這種方式調試一些複雜函數,如統計執行時間
測試2:
func test() func() {
fmt.Println("start")
defer func() {
fmt.Println("test-defer")
}()
return func() {
fmt.Println("end")
}
}
func main() {
defer test()
fmt.Println("middle")
}
// 輸出
middle
start
test-defer
此時将test()()改為test(),則未觸發test列印end,并且先執行了列印middle
另一個特性:defer可以修改return傳回值:
此時double(x)的結果先計算出來,後經過了defer内result += x的指派,最後得到12
此外因為defer一般涉及到資源回收,那麼如果有循環形式的資源申請,需要在循環内defer,否則可能出現遺漏
5.7 panic(崩潰)
Go的編譯器已經在編譯時檢測了許多錯誤,如果Go在運作時觸發如越界、空指針引用等問題,會觸發panic(崩潰)
panic也可以手動聲明觸發條件
發生panic時,defer所定義的函數會觸發(逆序),程式會在控制台列印panic的日志,并且列印出panic發生時的函數調用棧,用于定位錯誤出現的位置
func test() {
fmt.Println("start")
}
func main() {
defer test()
panic("panic")
}
// 結果
start
panic: panic
panic不要随意使用,雖然預檢查是一個好的習慣,但是大多數情況下你無法預估runtime時錯誤觸發的原因
手動觸發panic發生在一些重大的error出現時,當然如果發生程式的崩潰,應該優雅釋放資源如檔案io
關于panic發生時defer的逆序觸發如下:
5.8 recover(恢複)
panic發生時,可以通過recover關鍵字進行接收(有點像異常2捕獲),可以做一些資源釋放,或者錯誤報告工作,是以可以優雅關閉系統,而不是直接崩潰
如果recover()在defer中被調用,則目前函數運作發生panic,會觸發defer中的recover(),并且傳回的是panic的相關資訊,否則在其他時刻調用recover()将傳回nil(沒有發揮recover()作用)
上圖中的案例recover()接受到panic後,選擇列印panic内容,将其看作是一個錯誤,而不選擇停止程式運作,是以也就有了“恢複”的含義