《Go 語言程式設計》讀書筆記 (二)函式

KevinYan發表於2019-12-20

《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等價於 return 0,0,err(Go會將返回值 words和images在函式體的開始處,根據它們的型別,將其初始化為0),最後一處return等價於 return words,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("%T\n", 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("%T\n", f) // "func(...int)"
  fmt.Printf("%T\n", 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語句後的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。
  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) = %d\n", 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
  }
本作品採用《CC 協議》,轉載必須註明作者和本文連結
公眾號:網管叨bi叨 | Golang、Laravel、Docker、K8s等學習經驗分享

相關文章