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

《Go 語言程序設計》在線閱讀地址:https://yar999.gitbooks.io/go...html

函數

函數聲明

  • 函數聲明包括函數名、形式參數列表、返回值列表(可省略)以及函數體。
func name(parameter-list) (result-list) {
        body
    }

形式參數列表描述了函數的參數名以及參數類型。這些參數做爲局部變量,其值由參數調用者提供。返回值也能夠像形式參數同樣被命名,在這種狀況下,每一個返回值被聲明成一個局部變量,並初始化爲其類型的零值。git

  • 用 _ 符號做爲形參名能夠強調某個參數未被使用。
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:golang

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語句等價於:web

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
  }
相關文章
相關標籤/搜索