Golang 學習筆記八 錯誤異常
一、錯誤異常
《快學 Go 語言》第 10 課 —— 錯誤與異常
Go 語言的異常處理語法絕對是獨樹一幟,在我見過的諸多高階語言中,Go 語言的錯誤處理形式就是一朵奇葩。一方面它鼓勵你使用 C 語言的形式將錯誤通過返回值來進行傳遞,另一方面它還提供了高階語言一般都有的異常丟擲和捕獲的形式,但是又不鼓勵你使用這個形式。後面我們統一將返回值形式的稱為「錯誤」,將丟擲捕獲形式的稱為「異常」。
1.錯誤介面
Go 語言規定凡是實現了錯誤介面的物件都是錯誤物件,這個錯誤介面只定義了一個方法。
type error interface {
Error() string
}
注意這個介面的名稱,它是小寫的,是內建的全域性介面。通常一個名字如果是小寫字母開頭,那麼它在包外就是不可見的,不過 error 是內建的特殊名稱,它是全域性可見的。
編寫一個錯誤物件很簡單,寫一個結構體,然後掛在 Error() 方法就可以了。
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() 函式讓我們方便地建立一個通用錯誤。var err = errors.New("something happened")
如果你的錯誤字串需要定製一些引數,可使用 fmt 包提供了 Errorf 函式
var thing = "something"
var err = fmt.Errorf("%s happened", thing)
1.錯誤處理首體驗
在 Java 語言裡,如果遇到 IO 問題通常會丟擲 IOException 型別的異常,在 Go 語言裡面它不會拋異常,而是以返回值的形式來通知上層邏輯來處理錯誤。下面我們通過讀檔案來嘗試一下 Go 語言的錯誤處理,讀檔案需要使用內建的 os 包。
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 語言普遍沒有使用多返回值的習慣,僅僅是在需要返回錯誤的時候才會需要兩個返回值。除了錯誤之外,還有一個地方需要兩個返回值,那就是字典,通過第二個返回值來告知讀取的結果是零值還是根本就不存在。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 來明確到底讀了多少位元組。
2.體驗 Redis 的錯誤處理
上面讀檔案的例子並沒有讓讀者感受到錯誤處理的不爽,下面我們要引入 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 程式的朋友們來說,這樣繁瑣的錯誤判斷簡直太地獄了。不過還是那句話,習慣了就好。
二、異常與捕捉
1.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
}
}
}()
2.異常的真實應用
Go 語言官方表態不要輕易使用 panic recover,除非你真的無法預料中間可能會發生的錯誤,或者它能非常顯著地簡化你的程式碼。簡單一點說除非逼不得已,否則不要使用它。
在一個常見的 Web 應用中,不能因為個別 URL 處理器丟擲異常而導致整個程式崩潰,就需要在每個 URL 處理器外面包括一層 recover() 來恢復異常。
在 json 序列化過程中,邏輯上需要遞迴處理 json 內部的各種型別,每一種容器型別內部都可能會遇到不能序列化的型別。如果對每個函式都使用返回錯誤的方式來編寫程式碼,會顯得非常繁瑣。所以在內建的 json 包裡也使用了 panic,然後在呼叫的最外層包裹了 recover 函式來進行恢復,最終統一返回一個 error 型別。
你可以想象一下,內建 json 包的開發者在設計開發這個包的時候應該也是糾結的焦頭爛額,最終還是使用了 panic 和 recover 來讓自己的程式碼變的好看一些。
知乎最近發表了內部的 Go 語言實踐方案,因為忍受不了程式碼裡太多的錯誤判斷語句,它們的業務異常也改用 panic 丟擲來,雖然這並不是官方的推薦模式。
3.多個 defer 語句
有時候我們需要在一個函式裡使用多次 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
相關文章
- Python 3 學習筆記之——錯誤和異常Python筆記
- java學習筆記(異常)Java筆記
- rust學習十、異常處理(錯誤處理)Rust
- swoft 學習筆記之異常處理筆記
- golang 學習筆記Golang筆記
- SpringMVC學習筆記10-異常處理SpringMVC筆記
- PHP錯誤和異常PHP
- python錯誤與異常Python
- goLang學習筆記(一)Golang筆記
- goLang學習筆記(二)Golang筆記
- goLang學習筆記(三)Golang筆記
- goLang學習筆記(四)Golang筆記
- 【學習筆記】Golang 切片筆記Golang
- GOLang 學習筆記(一)Golang筆記
- golang 學習筆記1Golang筆記
- JavaScript學習筆記(八)—— 補JavaScript筆記
- Oracle異常錯誤處理Oracle
- ORACLE 異常錯誤處理Oracle
- Flutter之異常和錯誤Flutter
- kotlin學習筆記-異常好玩的list集合總結Kotlin筆記
- Golang 學習——error 錯誤處理淺談GolangError
- 錯誤和異常 (一):錯誤基礎知識
- Java 筆記《異常》Java筆記
- HexMap學習筆記(八)——水體筆記
- hive學習筆記之八:SqoopHive筆記OOP
- 異常錯誤資訊處理
- Golang學習筆記之方法(method)Golang筆記
- Golang學習筆記-1.6 函式Golang筆記函式
- Golang學習筆記(1):包管理Golang筆記
- 異常處理 - Go 學習記錄Go
- 認真一點學 Go:16. 錯誤與異常Go
- 吳恩達機器學習筆記 —— 16 異常點檢測吳恩達機器學習筆記
- C#學習筆記---異常捕獲和變數運算子C#筆記變數
- ES6學習筆記(八)【class】筆記
- Redis學習筆記八:叢集模式Redis筆記模式
- C++錯誤和異常處理C++
- web前端之異常/錯誤監控Web前端
- php錯誤與異常處理方法PHP