第五章 - 花絮

在這章,我們會介紹一些 Go 語言的花絮,這些特性主要是用在 Go 語言上。

錯誤處理

Go 主要是透過返回值來處理錯誤,並沒有一般語言的異常處理。讓我們來看看 strconv.Atoi 函式,這個函式會將字串轉整數:

package main

import (
  "fmt"
  "os"
  "strconv"
)

func main() {
  if len(os.Args) != 2 {
    os.Exit(1)
  }

  n, err := strconv.Atoi(os.Args[1])
  if err != nil {
    fmt.Println("not a valid number")
  } else {
    fmt.Println(n)
  }
}

你可以建立自己的錯誤型別,唯一的要求就是要滿足內建的 錯誤 介面:

type error interface {
  Error() string
}

一般情況下,我們可以引用內建的 error 套件,並使用 new 函式來建立自己的錯誤型別:

import (
  "errors"
)


func process(count int) error {
  if count < 1 {
    return errors.New("Invalid count")
  }
  ...
  return nil
}

Go 的標準函式庫就是透過這種模式來進行錯誤處理。例如,在 io 的函式庫中有一個 EOF 的變數用來定義錯誤(譯注:原始碼可參考:https://github.com/golang/go/blob/master/src/io/io.go#L38):

var EOF = errors.New("EOF")

這是一個屬於套件級別的變數(定義在函式外部),是可以是可以被公開存取的(變數是大寫開頭)。當我們從檔案中或標準輸入中讀取資料時,許多的函式都可以返回這種錯誤。如果有上下文關係的話,你也應該使用這種錯誤處理。作為使用者,你可以這樣使用:

package main

import (
  "fmt"
  "io"
)

func main() {
  var input int
  _, err := fmt.Scan(&input)
  if err == io.EOF {
    fmt.Println("no more input!")
  }
}

最後提醒一點,Go 有 panicrecover 函式。panic 類似於拋出異常,recover 類似於 catch。不過它們很少使用。

Defer

儘管 Go 語言提供了垃圾回收的機制,還是有一些資源需要開發者明確的去釋放。比如說,在處理文件結束時,我們需要呼叫 Close() 來關閉 io。這種類型的程式碼總是比較危險的,首先,當我們寫了一個函式,很容易忘記去呼叫 Close 函式來關閉我們在第十行開啟的檔案。此外,一個函式可能會有多個返回點。Go 提供了 defer 關鍵字來處理這一類的問題:

package main

import (
  "fmt"
  "os"
)

func main() {
  file, err := os.Open("a_file_to_read")
  if err != nil {
    fmt.Println(err)
    return
  }
  defer file.Close()
  // 讀檔
}

如果你嘗試執行上面的程式碼,你可能會得到一個錯誤(因為檔案不存在)。這裡主要是想要讓你知道 defer 的用法。使用 defer 的操作,都會在函式返回前執行。這讓你可以在初始化或宣告某個操作的附近就預先宣告要釋放資源。

go fmt

大多數用 Go 寫的程式碼都遵循相同的風格,那就是,使用 tab 進行縮排、括號和程式宣告在同一行等。

我知道你有自己的風格,也很想堅持下去。我曾經有一段時間也是這樣的,但很高興最後我還是屈服了。其中最大的原因就是 go fmt 命令工具。它很容使用也很具代表性(所以沒有人為了無意義的偏好而爭執)。

當你在專案的目錄下,你可以透過下面的命令行工具將所有子專案進行程式碼格式編排:

go fmt ./...

試試看吧,這個命令行工具會幫你的程式碼縮排,也會自動地幫你對齊,並且將引用的函式庫按照字母順序排列

具有初始化功能的 if

Go 支援一種稍微不一樣的 if 敘述,一個變數可以在 if 條件執行前宣告並且初始化:

if x := 10; count > x {
  ...
}

這是一個有點愚蠢的例子,比較實際的範例如下:

if err := process(); err != nil {
  return err
}

有趣的是,透過 if 初始化的值在 if 的範圍以外是不能被存取的,但是在 else ifelse 中可以被使用。

空的 Interface 和轉換

在大多數的物件導向程式語言中,都有內建的基礎類別,通常稱做 物件。它通常是所有類別的父類別。但在 Go 中不支援繼承,所以沒有類似這種父類別的概念。Go 裡面擁有的是一個沒有任何宣告的空介面 interface{}。由於每個型別都實作了空介面的 0 個方法,而且每個介面都是隱性實作,所以每種類型都實現了空介面的契約。

如果我們想要,可以寫一個 add 函式:

func add(a interface{}, b interface{}) interface{} {
  ...
}

將一個空介面型態的變數做顯性的轉換,可以用 .(TYPE)

return a.(int) + b.(int)

要注意的是,如果欲轉換的變數並不是 int 型態,上面的程式碼將會出錯。

你也可以強大的 type switch 進行轉換:

switch a.(type) {
  case int:
    fmt.Printf("a is now an int and equals %d\n", a)
  case bool, string:
    // ...
  default:
    // ...
}

你會發現空介面的使用超出你的預期。不可否認的,這會讓你的程式碼看起來不夠乾淨。某些時候不斷轉換一個值是醜陋的且危險的,但在靜態語言中,這是唯一的選擇。

字串和位元組陣列

字串和位元組陣列有密切的關係,我們可以很容易轉換他們:

stra := "the spice must flow"
byts := []byte(stra)
strb := string(byts)

事實上,這也是大多數型態的轉換方式。某些函式會明確指定 int32int64,或其他無號的部分。你可能會發現自己必須這樣寫:

int64(count)

儘管如此,當提到位元組和字串時,這會是你經常接觸到的東西。要記住,當你使用 []byte(x)string(x) 時,你是建立資料的拷貝,這是因為字串是不可變的。字串是由 runes 組成,runes 是一個 unicode 字符。當你用 len 函式來取得字串的長度時,往往結果不如你預期。以下的範例將會印出 3:

fmt.Println(len("椒"))

如果你嘗試透過 range 函式來遍歷一個字串,你是得到一個個的 runes,而不是位元組。當然,當你將一個字串轉成 []byte 時, 你會得到正確的資料。

函式型別

函式是一級型別:

type Add func(a int, b int) int

它可以用在任何地方 - 當作一個欄位型別、參數、回傳值。

package main

import (
  "fmt"
)

type Add func(a int, b int) int

func main() {
  fmt.Println(process(func(a int, b int) int{
      return a + b
  }))
}

func process(adder Add) int {
  return adder(1, 2)
}

透過這種使用函式的方式,我們可以從特定的實作中減少耦合,就像我們使用介面一樣。

在你繼續學習之前

我們已經學習了很多 Go 語言的特性,顯而易見的,我們學習了錯誤處理、當開起檔案後如何釋放資源。許多開發者不喜歡 Go 錯誤處理的方式,它讓人覺得是一種退步。某些時候我是同意的,然而,我也會發現這樣的程式碼更容易閱讀。defer 在資源管理上是一個不常見但實用的手段。事實上,defer 不僅僅可以用在資源管理上,也可以用在其他方面,比如說你可以用在函式退出時的日誌紀錄上。

當然,我們還沒有把所有 Go 的特性都介紹完,但你應該可以在遇到任何困難時都迎刃而解。