Go 語言的異常處理語法絕對是獨樹一幟,在我見過的諸多高級語言中,Go 語言的錯誤處理形式就是一朵奇葩。一方面它鼓勵你使用 C 語言的形式將錯誤經過返回值來進行傳遞,另外一方面它還提供了高級語言通常都有的異常拋出和捕獲的形式,可是又不鼓勵你使用這個形式。後面咱們統一將返回值形式的稱爲「錯誤」,將拋出捕獲形式的稱爲「異常」。git
Go 語言的錯誤處理在業界飽受批評,不過既然咱們已經入了這個坑,那仍是好好蹲着吧。github
Go 語言規定凡是實現了錯誤接口的對象都是錯誤對象,這個錯誤接口只定義了一個方法。redis
type error interface {
Error() string
}
複製代碼
注意這個接口的名稱,它是小寫的,是內置的全局接口。一般一個名字若是是小寫字母開頭,那麼它在包外就是不可見的,不過 error 是內置的特殊名稱,它是全局可見的。json
編寫一個錯誤對象很簡單,寫一個結構體,而後掛在 Error() 方法就能夠了。bash
package main
import "fmt"
type SomeError struct {
Reason string
}
func (s SomeError) Error() string {
return s.Reason
}
func main() {
var err error = SomeError{"something happened"}
fmt.Println(err)
}
---------------
something happened
複製代碼
對於上面代碼中錯誤對象的形式很是經常使用,因此 Go 語言內置了一個通用錯誤類型,在 errors 包裏。這個包還提供了一個 New() 函數讓咱們方便地建立一個通用錯誤。app
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
複製代碼
注意這個結構體 errorString 是首字母小寫的,意味着咱們沒法直接使用這個類型的名字來構造錯誤對象,而必須使用 New() 函數。函數
var err = errors.New("something happened")
複製代碼
若是你的錯誤字符串須要定製一些參數,可以使用 fmt 包提供了 Errorf 函數性能
var thing = "something"
var err = fmt.Errorf("%s happened", thing)
複製代碼
在 Java 語言裏,若是遇到 IO 問題一般會拋出 IOException 類型的異常,在 Go 語言裏面它不會拋異常,而是以返回值的形式來通知上層邏輯來處理錯誤。下面咱們經過讀文件來嘗試一下 Go 語言的錯誤處理,讀文件須要使用內置的 os 包。ui
package main
import "os"
import "fmt"
func main() {
// 打開文件
var f, err = os.Open("main.go")
if err != nil {
// 文件不存在、權限等緣由
fmt.Println("open file failed reason:" + err.Error())
return
}
// 推遲到函數尾部調用,確保文件會關閉
defer f.Close()
// 存儲文件內容
var content = []byte{}
// 臨時的緩衝,按塊讀取,一次最多讀取 100 字節
var buf = make([]byte, 100)
for {
// 讀文件,將讀到的內容填充到緩衝
n, err := f.Read(buf)
if n > 0 {
// 將讀到的內容聚合起來
content = append(content, buf[:n]...)
}
if err != nil {
// 遇到流結束或者其它錯誤
break
}
}
// 輸出文件內容
fmt.Println(string(content))
}
-------
package main
import "os"
import "fmt"
.....
複製代碼
在這段代碼裏有幾個點須要特別注意。第一個須要注意的是 os.Open()、f.Read() 函數返回了兩個值,Go 語言不但容許函數返回兩個值,三個值四個值都是能夠的,只不過 Go 語言廣泛沒有使用多返回值的習慣,僅僅是在須要返回錯誤的時候纔會須要兩個返回值。除了錯誤以外,還有一個地方須要兩個返回值,那就是字典,經過第二個返回值來告知讀取的結果是零值仍是根本就不存在。spa
var score, ok := scores["apple"]
複製代碼
第二個須要注意的是 defer 關鍵字,它將文件的關閉調用推遲到當前函數的尾部執行,即便後面的代碼拋出了異常,文件關閉也會確保被執行,至關於 Java 語言的 finally 語句塊。defer 是 Go 語言很是重要的特性,在平常應用開發中,咱們會常用到它。
第三個須要注意的地方是 append 函數參數中出現了 ... 符號。在切片章節,咱們知道 append 函數能夠將單個元素追加到切片中,其實 append 函數能夠一次性追加多個元素,它的參數數量是可變的。
var s = []int{1,2,3,4,5}
s = append(s,6,7,8,9)
複製代碼
可是讀文件的代碼中須要將整個切片的內容追加到另外一個切片中,這時候就須要 ... 操做符,它的做用是將切片參數的全部元素展開後傳遞給 append 函數。你可能會擔憂若是切片裏有成百上千的元素,展開成元素再傳遞會不會很是耗費性能。這個沒必要擔憂,展開只是形式上的展開,在實現上其實並無展開,傳遞過去的參數本質上仍是切片。
第四個須要注意的地方是讀文件操做 f.Read() ,它會將文件的內容往切片裏填充,填充的量不會超過切片的長度(注意不是容量)。若是將緩衝改爲下面這種形式,就會死循環!
var buf = make([]byte, 0, 100)
複製代碼
另外若是遇到文件尾了,切片就不會填滿。因此須要經過返回值 n 來明確到底讀了多少字節。
上面讀文件的例子並無讓讀者感覺到錯誤處理的不爽,下面咱們要引入 Go 語言 Redis 的客戶端包,來真實體驗一下 Go 語言的錯誤處理有多讓人不快。
使用第三方包,須要使用 go get 指令下載這個包,該指令會將第三方包放到 GOPATH 目錄下。
go get github.com/go-redis/redis
複製代碼
下面我要實現一個小功能,獲取 Redis 中兩個整數值,而後相乘,再存入 Redis 中
package main
import "fmt"
import "strconv"
import "github.com/go-redis/redis"
func main() {
// 定義客戶端對象,內部包含一個鏈接池
var client = redis.NewClient(&redis.Options {
Addr: "localhost:6379",
})
// 定義三個重要的整數變量值,默認都是零
var val1, val2, val3 int
// 獲取第一個值
valstr1, err := client.Get("value1").Result()
if err == nil {
val1, err = strconv.Atoi(valstr1)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}
// 獲取第二個值
valstr2, err := client.Get("value2").Result()
if err == nil {
val2, err = strconv.Atoi(valstr2)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}
// 保存第三個值
val3 = val1 * val2
ok, err := client.Set("value3",val3, 0).Result()
if err != nil {
fmt.Println("set value error reason:" + err.Error())
return
}
fmt.Println(ok)
}
------
OK
複製代碼
由於 Go 語言中不輕易使用異常語句,因此對於任何可能出錯的地方都須要判斷返回值的錯誤信息。上面代碼中除了訪問 Redis 須要判斷以外,字符串轉整數也須要判斷。
另外還有一個須要特別注意的是由於字符串的零值是空串而不是 nil,你很差從字符串內容自己判斷出 Redis 是否存在這個 key 仍是對應 key 的 value 爲空串,須要經過返回值的錯誤信息來判斷。代碼中的 redis.Nil 就是客戶端專門爲 key 不存在這種狀況而定義的錯誤對象。
相比於寫習慣了 Python 和 Java 程序的朋友們來講,這樣繁瑣的錯誤判斷簡直太地獄了。不過仍是那句話,習慣了就好。
Go 語言提供了 panic 和 recover 全局函數讓咱們能夠拋出異常、捕獲異常。它相似於其它高級語言裏常見的 throw try catch 語句,可是又很不同,好比 panic 函數能夠拋出來任意對象。下面咱們看一個使用 panic 的例子
package main
import "fmt"
var negErr = fmt.Errorf("non positive number")
func main() {
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}
// 讓階乘函數返回錯誤太不雅觀了
// 使用 panic 會合適一些
func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}
-------
3628800
120
panic: non positive number
goroutine 1 [running]:
main.fact(0xfffffffffffffffb, 0x1)
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:16 +0x75
main.main()
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:10 +0x122
exit status 2
複製代碼
上面的代碼拋出了 negErr,直接致使了程序崩潰,程序最後打印了異常堆棧信息。下面咱們使用 recover 函數來保護它,recover 函數須要結合 defer 語句一塊兒使用,這樣能夠確保 recover() 邏輯在程序異常的時候也能夠獲得調用。
package main
import "fmt"
var negErr = fmt.Errorf("non positive number")
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("error catched", err)
}
}()
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}
func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}
-------
3628800
120
error catched non positive number
複製代碼
輸出結果中的異常堆棧信息沒有了,說明捕獲成功了,不過即便程序再也不崩潰,異常點後面的邏輯也不會再繼續執行了。上面的代碼中須要注意的是咱們使用了匿名函數 func() {...}
defer func() {
if err := recover(); err != nil {
fmt.Println("error catched", err)
}
}()
複製代碼
尾部還有個括號是怎麼回事,爲何還須要這個括號呢?它表示對匿名函數進行了調用。對比一下前面寫的文件關閉尾部的括號就能理解了
defer f.Close()
複製代碼
還有個值得注意的地方時,panic 拋出的對象未必是錯誤對象,而 recover() 返回的對象正是 panic 拋出來的對象,因此它也不必定是錯誤對象。
func panic(v interface{})
func recover() interface{}
複製代碼
咱們常常還須要對 recover() 返回的結果進行判斷,以挑選出咱們願意處理的異常對象類型,對於那些不肯意處理的,能夠選擇再次拋出來,讓上層來處理。
defer func() {
if err := recover(); err != nil {
if err == negErr {
fmt.Println("error catched", err)
} else {
panic(err) // rethrow
}
}
}()
複製代碼
Go 語言官方表態不要輕易使用 panic recover,除非你真的沒法預料中間可能會發生的錯誤,或者它能很是顯著地簡化你的代碼。簡單一點說除非逼不得已,不然不要使用它。
在一個常見的 Web 應用中,不能由於個別 URL 處理器拋出異常而致使整個程序崩潰,就須要在每一個 URL 處理器外面包括一層 recover() 來恢復異常。
在 json 序列化過程當中,邏輯上須要遞歸處理 json 內部的各類類型,每一種容器類型內部均可能會遇到不能序列化的類型。若是對每一個函數都使用返回錯誤的方式來編寫代碼,會顯得很是繁瑣。因此在內置的 json 包裏也使用了 panic,而後在調用的最外層包裹了 recover 函數來進行恢復,最終統一返回一個 error 類型。 你能夠想象一下,內置 json 包的開發者在設計開發這個包的時候應該也是糾結的焦頭爛額,最終仍是使用了 panic 和 recover 來讓本身的代碼變的好看一些。知乎最近發表了內部的 Go 語言實踐方案,由於忍受不了代碼裏太多的錯誤判斷語句,它們的業務異常也改用 panic 拋出來,雖然這並非官方的推薦模式。
有時候咱們須要在一個函數裏使用屢次 defer 語句。好比拷貝文件,須要同時打開源文件和目標文件,那就須要調用兩次 defer f.Close()。
package main
import "fmt"
import "os"
func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer fsrc.Close()
fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer fdes.Close()
fmt.Println("do something here")
}
複製代碼
須要注意的是 defer 語句的執行順序和代碼編寫的順序是反過來的,也就是說最早 defer 的語句最後執行,爲了驗證這個規則,咱們來改寫一下上面的代碼
package main
import "fmt"
import "os"
func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer func() {
fmt.Println("close source file")
fsrc.Close()
}()
fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer func() {
fmt.Println("close target file")
fdes.Close()
}()
fmt.Println("do something here")
}
--------
do something here
close target file
close source file
複製代碼
下一節咱們開講 Go 語言最重要的特點功能 —— 通道與協程
關注公衆號「碼洞」,回覆 go 獲取《快學 Go 語言》所有章節