《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    }