《Go語言程式設計》讀書筆記(二)函數
- 2019 年 12 月 30 日
- 筆記
《Go 語言程式設計》在線閱讀地址:https://yar999.gitbooks.io/gopl-zh/content/
函數
函數聲明
- 函數聲明包括函數名、形式參數列表、返回值列表(可省略)以及函數體。
func name(parameter-list) (result-list) { body }
形式參數列表描述了函數的參數名以及參數類型。這些參數作為局部變數,其值由參數調用者提供。返回值也可以像形式參數一樣被命名,在這種情況下,每個返回值被聲明成一個局部變數,並初始化為其類型的零值。
- 用 _ 符號作為形參名可以強調某個參數未被使用。
func first(x int, _ int) int { return x }
- 函數的類型被稱為函數的標識符。如果兩個函數形式參數列表和返回值列表中的變數類型一一對應,那麼這兩個函數被認為有相同的類型和標識符。
- 在函數調用時,Go語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返回值的變數名對於函數調用者而言沒有意義。
- 實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行修改不會影響實參。但是,如果實參包括引用類型,如指針,slice(切片)、map、function、channel等類型,實參可能會由於函數的引用而被修改。
- golang.org/x/… 目錄下存儲了一些由Go團隊設計、維護,對網路編程、國際化文件處理、移動平台、影像處理、加密解密、開發者工具提供支援的擴展包。未將這些擴展包加入到標準庫原因有二,一是部分包仍在開發中,二是對大多數Go語言的開發者而言,擴展包提供的功能很少被使用。
遞歸調用
- 大部分程式語言使用固定大小的函數調用棧,常見的大小從64KB到2MB不等。固定大小棧會限制遞歸的深度,當你用遞歸處理大量數據時,需要避免棧溢出;除此之外,還會導致安全性問題。與相反,Go語言使用可變棧,棧的大小按需增加(初始時很小)。這使得我們使用遞歸時不必考慮溢出和安全問題
- 雖然Go的垃圾回收機制會回收不被使用的記憶體,但是這不包括作業系統層面的資源,比如打開的文件、網路連接。因此我們必須顯式的釋放這些資源。
多返回值函數
- 調用多返回值函數時,返回給調用者的是一組值,調用者必須顯式的將這些值分配給變數:
links, err := findLinks(url)
如果某個值不被使用,可以將其分配給blank identifier:
links, _ := findLinks(url) // errors ignored
- 如果一個函數將所有的返回值都顯示的變數名,那麼該函數的return語句可以省略操作數。這稱之為bare return。
// CountWordsAndImages does an HTTP GET request for the HTML // document url and returns the number of words and images in it. func CountWordsAndImages(url string) (words, images int, err error) { resp, err := http.Get(url) if err != nil { return } doc, err := html.Parse(resp.Body) resp.Body.Close() if err != nil { err = fmt.Errorf("parsing HTML: %s", err) return } words, images = countWordsAndImages(doc) return } func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
按照函數聲明中返回值列表的次序,返回所有的返回值,在上面的例子中,每一個return語句等價於:
return words, images, err
- 當一個函數有多處return語句以及許多返回值時,bare return 可以減少程式碼的重複,但是使得程式碼難以被理解。如果你沒有仔細的審查上面的程式碼,很難發現前2處return等價於
return0,0,err
(Go會將返回值 words和images在函數體的開始處,根據它們的類型,將其初始化為0),最後一處return等價於returnwords,image,nil
。基於以上原因,不宜過度使用bare return。
錯誤
- 在Go的錯誤處理中,錯誤是軟體包API和應用程式用戶介面的一個重要組成部分,程式運行失敗僅被認為是幾個預期的結果之一。
- 對於那些將運行失敗看作是預期結果的函數,它們會返回一個額外的返回值,通常是最後一個,來傳遞錯誤資訊。
resp, err := http.Get(url)
- 內置的error是介面類型。nil意味著函數運行成功,non-nil表示失敗。對於non-nil的error類型,我們可以通過調用error的
Error
函數或者輸出函數獲得字元串類型的錯誤資訊。
fmt.Println(err) fmt.Printf("%v", err)
函數值
- 在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變數,傳遞給函數,從函數返回。對函數值(function value)的調用類似函數調用。例子如下:
func square(n int) int { return n * n } func negative(n int) int { return -n } func product(m, n int) int { return m * n } f := square fmt.Println(f(3)) // "9" f = negative fmt.Println(f(3)) // "-3" fmt.Printf("%Tn", f) // "func(int) int" f = product // compile error: can't assign func(int, int) int to func(int) int
- 函數類型的零值是nil。調用值為nil的函數值會引起panic錯誤:
var f func(int) int f(3) // 此處f的值為nil, 會引起panic錯誤
- 函數值可以與nil比較:
var f func(int) int if f != nil { f(3) }
但是函數值之間是不可比較的,也不能用函數值作為map的key。
匿名函數
- 擁有函數名的函數只能在包級語法塊中被聲明,通過函數字面量(function literal),我們可繞過這一限制,在任何表達式中表示一個函數值。函數字面量的語法和函數聲明相似,區別在於func關鍵字後沒有函數名。函數值字面量是一種表達式,它的值被稱為匿名函數(anonymous function)。 函數字面量允許我們在使用函數時,再定義它。通過這種技巧,我們可以改寫之前對strings.Map的調用:
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
更為重要的是,通過這種方式定義的函數可以訪問完整的詞法環境(lexical environment),這意味著在函數中定義的內部函數可以引用該函數的變數。
// squares返回一個匿名函數。 // 該匿名函數每次被調用時都會返回下一個數的平方。 func squares() func() int { var x int return func() int { x++ return x * x } }
通過這個例子,我們看到變數的生命周期不由它的作用域決定:squares返回後,變數x仍然隱式的存在於f中。
- 當匿名函數需要被遞歸調用時,我們必須首先聲明一個變數,再將匿名函數賦值給這個變數。如果不分成兩步,函數字面量無法與變數綁定,我們也無法遞歸調用該匿名函數,比如:
var visitAll func(items []string) visitAll = func(items []string) { ...... visitAll(m[item]) ...... }
否則會出現編譯錯誤
visitAll := func(items []string) { // ... visitAll(m[item]) // compile error: undefined: visitAll // ... }
可變參數
- 參數數量可變的函數稱為為可變參數函數。典型的例子就是fmt.Printf和類似函數。Printf首先接收一個的必備參數,之後接收任意個數的後續參數。 在聲明可變參數函數時,需要在參數列表的最後一個參數類型之前加上省略符號「…」,這表示該函數會接收任意數量的該類型參數。
func sum(vals...int) int { total := 0 for _, val := range vals { total += val } return total }
sum函數返回任意個int型參數的和。在函數體中,vals被看作是類型為 []int
的切片。sum可以接收任意數量的int型參數:
fmt.Println(sum()) // "0" fmt.Println(sum(3)) // "3" fmt.Println(sum(1, 2, 3, 4)) // "10"
- 在上面的程式碼中,調用者隱式的創建一個數組,並將原始參數複製到數組中,再把數組的一個切片作為參數傳給被調函數。如果原始參數已經是切片類型,我們該如何傳遞給sum?只需在最後一個參數後加上省略符。下面的程式碼功能與上個例子中最後一條語句相同。
values := []int{1, 2, 3, 4} fmt.Println(sum(values...)) // "10" // fmt.Println(sum(1, 2, 3, 4))
- 雖然在可變參數函數內部,
...int
型參數的行為看起來很像切片類型,但實際上,可變參數函數和以切片作為參數的函數是不同的。
func f(...int) {} func g([]int) {} fmt.Printf("%Tn", f) // "func(...int)" fmt.Printf("%Tn", g) // "func([]int)"
- 可變參數函數經常被用于格式化字元串。下面的errorf函數構造了一個以行號開頭的,經過格式化的錯誤資訊。函數名的後綴f是一種通用的命名規範,代表該可變參數函數可以接收Printf風格的格式化字元串。
func errorf(linenum int, format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "Line %d: ", linenum) fmt.Fprintf(os.Stderr, format, args...) fmt.Fprintln(os.Stderr) } linenum, name := 12, "count" errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
...interfac{}
表示函數在 format
參數後可以接收任意個任意類型的參數。interface{}
會在後面介紹。
Deferred 函數
- 你只需要在調用普通函數或方法前加上關鍵字defer,就完成了defer所需要的語法。當defer語句被執行時,跟在defer後面的函數會被延遲執行。直到包含該defer語句的函數執行完畢時,defer後的函數才會被執行,不論包含defer語句的函數是通過return正常結束,還是由於panic導致的異常結束。你可以在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。
- defer語句經常被用於處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通過defer機制,不論函數邏輯多複雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句後。
- 對文件的操作
package ioutil func ReadFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() return ReadAll(f) }
- 處理互斥鎖
var mu sync.Mutex var m = make(map[string]int) func lookup(key string) int { mu.Lock() defer mu.Unlock() return m[key] }
- 調試複雜程式時,defer機制也常被用於記錄何時進入和退出函數。下例中的bigSlowOperation函數,直接調用trace記錄函數的被調情況。bigSlowOperation被調時,trace會返回一個函數值,該函數值會在bigSlowOperation退出時被調用。通過這種方式, 我們可以只通過一條語句控制函數的入口和所有的出口,甚至可以記錄函數的運行時間,如例子中的start。需要注意一點:不要忘記defer語句後的圓括弧,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。 gopl.io/ch5/trace
func bigSlowOperation() { defer trace("bigSlowOperation")() // don't forget the extra parentheses // ...lots of work… time.Sleep(10 * time.Second) // simulate slow operation by sleeping } func trace(msg string) func() { start := time.Now() log.Printf("enter %s", msg) return func() { log.Printf("exit %s (%s)", msg,time.Since(start)) } }
每一次bigSlowOperation被調用,程式都會記錄函數的進入,退出,持續時間。(我們用time.Sleep模擬一個耗時的操作)
$ go build gopl.io/ch5/trace $ ./trace 2015/11/18 09:53:26 enter bigSlowOperation 2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
- 用 defer 函數記錄返回值(需要是命名返回值才能記錄)
func double(x int) (result int) { defer func() { fmt.Printf("double(%d) = %dn", x,result) }() return x + x } _ = double(4) // Output: // "double(4) = 8"
- 被延遲執行的匿名函數甚至可以修改函數返回給調用者的返回值:
func triple(x int) (result int) { defer func() { result += x }() return double(x) } fmt.Println(triple(4)) // "12"
- 在循環體中的defer語句需要特別注意,因為只有在函數執行完畢後,這些被延遲的函數才會執行。下面的程式碼會導致系統的文件描述符耗盡,因為在所有文件都被處理之前,沒有文件會被關閉。
for _, filename := range filenames { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // NOTE: risky; could run out of file descriptors // ...process f… }
一種解決方法是將循環體中的文件操作和defer語句移至另外一個函數。在每次循環時,調用這個函數。
for _, filename := range filenames { if err := doFile(filename); err != nil { return err } } func doFile(filename string) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // ...process f… }
Panic 和 Recover
- Go的類型系統會在編譯時捕獲很多錯誤,但有些錯誤只能在運行時檢查,如數組訪問越界、空指針引用等。這些運行時錯誤會引起painc異常。
- 當panic異常發生時,程式會中斷運行,並立即執行在該goroutine(可以先理解成執行緒,在第8章會詳細介紹)中被延遲的函數(defer 機制)。隨後,程式崩潰並輸出日誌資訊。日誌資訊包括panic value和函數調用的堆棧跟蹤資訊。
- 雖然Go的panic機制類似於其他語言的異常,但panic的適用場景有一些不同。由於panic會引起程式的崩潰,因此panic一般用於嚴重錯誤,如程式內部的邏輯不一致。
- 通常來說,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢復,至少我們可以在程式崩潰前,做一些操作。舉個例子,當web伺服器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連接關閉;如果不做任何處理,會使得客戶端一直處於等待狀態。
- 如果在deferred函數中調用了內置函數recover,並且定義該defer語句的函數發生了panic異常,recover會使程式從panic中恢復,並返回panic value。導致panic異常的函數不會繼續運行,但能正常返回。在未發生panic時調用recover,recover會返回nil。
- 例子中deferred函數幫助Parse從panic中恢復。在deferred函數內部,panic value被附加到錯誤資訊中;並用err變數接收錯誤資訊,返回給調用者。
func Parse(input string) (s *Syntax, err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }() // ...parser... }
- 不加區分的恢復所有的panic異常,不是可取的做法。
- 只恢復應該被恢復的panic異常,此外,這些異常所佔的比例應該儘可能的低。為了標識某個panic是否應該被恢復,我們可以將panic value設置成特殊類型。在recover時對panic value進行檢查,如果發現panic value是特殊類型,就將這個panic作為errror處理,如果不是,則按照正常的panic進行處理
func soleTitle(doc *html.Node) (title string, err error) { type bailout struct{} defer func() { switch p := recover(); p { case nil: // no panic case bailout{}: // "expected" panic err = fmt.Errorf("multiple title elements") default: panic(p) } }() forEachNode(doc, func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil { if title != "" { panic(bailout{}) // multiple titleelements } title = n.FirstChild.Data } }, nil) if title == "" { return "", fmt.Errorf("no title element") } return title, nil }