學到什麼
什麼是錯誤?
如何建立錯誤?
如何處理錯誤?
errors
包的使用?什麼是異常?
如何處理異常?
defer
關鍵字的作用?recover
函式的使用?
什麼是錯誤
在寫程式碼時,不是所有情況都能處理,對於不能處理的邏輯,就需要使用錯誤機制告訴上層呼叫者。
在 Go 語言中,錯誤是被作為一個介面型別對待,它不像其它語言一樣使用 try/catch
去捕捉,只需在函式或方法之間使用一個錯誤型別變數去傳遞。
建立錯誤
這裡所說的建立錯誤,實際上就是去實現錯誤介面,介面如下:
type error interface {
Error() string
}
該介面是 Go 標準包內建的,所有建立的錯誤型別都需要實現此介面,怎麼去實現介面,不懂的看看上篇文章 《介面》。
1. errors.New
Go 語言中內建了一個處理錯誤的標準包,你不需要自己去實現 error
介面,它有函式幫你處理,如下:
import "errors"
var ErrNotFound = errors.New("not found")
匯入 errors
包,呼叫 New
函式建立了一個錯誤並儲存到 ErrNotFound
變數,該錯誤資訊為 not found
。
2. fmt.Errorf
fmt
標準包內也有一個建立錯誤的函式 Errorf
,該函式可以使用佔位符設定錯誤資訊,比 errors.New
函式更靈活。
import "fmt"
var ErrHuman = fmt.Errorf("%s不符合我們人類要求", "老苗")
3. 自定義錯誤型別
如果上述兩種方式你覺得還不夠靈活,那可以自定義錯誤型別。
type ErrorPathNotExist struct {
Filename string
}
func (*ErrorPathNotExist) Error() string {
return "檔案路徑不存在"
}
var ErrNotExist error = &ErrorPathNotExist{
Filename: "./main.go",
}
自定義了一個
ErrorPathNotExist
結構體,該結構體實現了error
介面。建立了一個
ErrNotExist
錯誤型別變數。
這種如果不明白具體怎麼應用,不著急,往下看。
補充知識點:如果方法的接收者沒有被使用可以直接省略掉,例:
func (*ErrorPathNotExist)
,不省略的話就是這樣:func (e *ErrorPathNotExist)
,當然也可以使用下劃線”_”代替 “e” 只是沒有必要性。
列印錯誤
在專案開發中,錯誤常常通過函式或方法的返回值攜帶,返回的位置也通常被放置在最後一位。
// error/file.go
package main
import "io/ioutil"
// 讀取檔案內容
func LoadConfig() (string, error) {
filename := "./config.json"
b, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
content := string(b)
if len(content) == 0 {
return "", errors.New("內容為空")
}
return content, nil
}
ReadFile
函式讀取 “config.json” 檔案內容。(string, error)
返回兩個值,第一個為檔案內容,第二個為錯誤。err != nil
用於判斷是否有錯誤,如果有return
直接返回。string(b)
變數 b 的型別為[]byte
,該操作是將[]byte
型別轉為string
。增加了一個“內容為空”的錯誤判斷,該錯誤也可以直接儲存到變數中返回。
var ErrEmpty = errors.New("內容為空")
func LoadConfig() (string, error) {
// ...
return "", ErrEmpty
// ...
}
現在假設 “config.json” 檔案不存在,呼叫 LoadConfig
函式看看結果。
package main
import (
"fmt"
"log"
)
func main() {
content, err := LoadConfig()
if err != nil {
log.Fatal(err)
}
fmt.Println("內容:", content)
}
// 輸出
2021/09/23 16:57:25 open ./config.json: The system cannot find the file specified.
當
err
不等於nil
時,列印錯誤,並退出程式。log
標準包包含列印日誌的函式集。log.Fatal
函式列印日誌,並終止程式向下執行。輸出的錯誤訊息顯示沒有找到指定的檔案。
列印錯誤時,也可以使用
fmt
包,例如:fmt.Println(err)
,只是輸出資訊沒log
包多。
os.Exit
該函式通知程式退出,並且該函式之後的邏輯將不會被執行。在呼叫時需要指定退出碼,為 0 時,表示正常退出程式。
os.Exit(0)
不主動呼叫該函式,即程式從 main 函式自然結束時,預設的退出碼為 0。在使用編寫工具時或許能看到成功的退出碼資訊,例如我使用的是 Goland,執行程式碼後輸出的結果末尾會顯示如下資訊。
Process finished with exit code 0
如果不正常退出,退出碼則為非 0,通常使用 1 表示未知錯誤。
os.Exit(1)
在使用 log.Fatal
函式時,內部就呼叫了 os.Exit(1)
。
錯誤加工
1. 錯誤拼接
在返回錯誤時,如果想攜帶附加的錯誤訊息時,可以使用 fmt.Errorf
,現在修改 LoadConfig
函式。
func LoadConfig() (string, error) {
filename := "./config.json"
b, err := ioutil.ReadFile(filename)
if err != nil {
return "", fmt.Errorf("讀取檔案出錯:%v", err)
}
// ...
}
%v
佔位符表示獲取資料的值,在這塊表示錯誤訊息,後續會詳細講解佔位符的使用。
現在重新執行上面的 main
函式,還是假設 “config.json” 檔案不存在。
// ...
content, err := LoadConfig()
if err != nil {
log.Fatal(err)
}
//...
// 輸出
2021/09/24 11:37:33 讀取檔案出錯:open ./config.json: The system cannot find the file specified.
LoadConfig
函式返回的 err
變數中攜帶了附加的錯誤訊息,但這樣有個問題,附加的錯誤和原始錯誤訊息雜糅在一塊導致不能分離。
2. 錯誤巢狀和 errors.Unwrap
上面所說的”錯誤訊息雜糅在一塊導致不能分離“問題,如果還沒有明白的話,可以再看看這塊,你應該就豁然開朗了。
錯誤巢狀就類似上圖,err1
巢狀類 err2
,err2
也可以繼續巢狀。如果想從 err1
中獲取 err2
就剝一層,類似洋蔥一樣,一層一層往裡找。
那怎麼實現這種巢狀關係呢,還是使用 fmt.Errorf
函式,只是使用另外一個佔位符%w
,w
的英文全名就是 wrap
。
繼續修改 LoadConfig
函式,如下:
func LoadConfig() (string, error) {
filename := "./config.json"
b, err := ioutil.ReadFile(filename)
if err != nil {
return "", fmt.Errorf("讀取檔案出錯:%w", err)
}
// ...
}
現在再執行 main
函式,即呼叫 LoadConfig
該函式,並列印錯誤。
2021/09/24 18:07:14 讀取檔案出錯:open ./config.json: The system cannot find the file specified.
是不是發現錯誤結果沒有變化,那修改下 main
函式。
package main
import (
"errors"
"fmt"
"log"
)
func main() {
content, err := LoadConfig()
if err != nil {
log.Fatal(errors.Unwrap(err))
}
fmt.Println("內容:", content)
}
// 輸出
2021/09/24 18:11:09 open ./config.json: The system cannot find the file specified.
在列印錯誤時,增加了一個 errors.Unwrap
函式,該函式就是用來取出巢狀的錯誤,再看看輸出的結果,附加的錯誤資訊”讀取檔案出錯:“已經沒有了。
3. 自定義錯誤型別
在上面講過了如何自定義錯誤型別,現在講講如何應用你自定義的錯誤,接下來將 LoadConfig
函式中的內容為空的錯誤改為自定義錯誤型別。
func LoadConfig() (string, error) {
filename := "./config.json"
// ...
if len(content) == 0 {
return "", &FileEmptyError{
Filename: filename,
Err: errors.New("內容為空"),
}
}
return content, nil
}
FileEmptyError
是自定義的錯誤型別,同樣的實現 error
介面。
type FileEmptyError struct {
Filename string
Err error
}
func (e *FileEmptyError) Error() string {
return fmt.Sprintf("%s %v", e.Filename, e.Err)
}
現在呼叫 LoadConfig
函式,現在假設 “config.json” 檔案存在,但內容為空,結果如下:
content, err := LoadConfig()
if err != nil {
if v, ok := err.(*FileEmptyError); ok {
fmt.Println("Filename:", v.Filename)
}
log.Fatal(err)
}
// 輸出
Filename: ./config.json
2021/09/27 14:36:40 ./config.json 內容為空
將
err
變數的介面型別推斷為*FileEmptyError
型別,並輸出Filename
欄位。列印自定義錯誤內容。
如果想使用 errors.Unwrap
函式 , 就需要實現 Wrapper
介面,fmt.Errorf
函式中的 %w
佔位符底層實現好了此介面。
type Wrapper interface {
// Unwrap returns the next error in the error chain.
// If there is no next error, Unwrap returns nil.
Unwrap() error
}
只需要我們實現了 Unwrap
方法就可以。
func (e *FileEmptyError) Unwrap() error {
return e.Err
}
下來自行去實驗,我在這就不羅嗦了。
錯誤判斷
對於一個函式或方法,返回的錯誤常常不止一個錯誤結果,如果對於不同的錯誤結果你想有不同的處理邏輯,那這個時候就要對錯誤結果進行判斷。
// ...
var (
ErrNotFoundRequest = errors.New("404")
ErrBadRequest = errors.New("請求異常")
)
func GetError() error {
// ...
// 錯誤 1
return ErrNotFoundRequest
// ...
// 錯誤 2
return ErrBadRequest
// ...
// 錯誤 3
path := "https://printlove.com"
return fmt.Errorf("%s:%w", path, ErrNotFoundRequest)
// ...
}
GetError
函式沒有寫具體邏輯,只展示了 3 個錯誤的返回,下來看看如何對這幾種情況進行判斷。
1. 最簡單”==”
最簡單的就是使用”==“判斷錯誤結果。
err := GetError()
if err == ErrNotFoundRequest {
// 錯誤 1
} else if err == ErrBadRequest {
// 錯誤 2
}
對於這種判斷方式有個問題,“錯誤 3”是不符合這兩個 if
判斷的,但從錯誤分類上說,它屬於 ErrNotFoundRequest
錯誤,只是拼接了請求地址資料 path
,下來往下看另外一種判斷方式。
2. errors.Is
使用 errors.Is
函式解決“錯誤 3”的判斷問題,下來分析為什麼?
“錯誤 3”中使用了佔位符%w
,它就是將 ErrNotFoundRequest
錯誤嵌入其中,**Is
函式的作用就是一層層的對錯誤進行剝離判斷**,直到成功或沒有巢狀的錯誤為止,沒明白的話可以結合上面的圖。
函式定義如下:
func Is(err, target error) bool
err
引數為要判斷的錯誤。target
引數為要比對的錯誤物件。
使用如下:
err := GetError()
if errors.Is(err, ErrNotFoundRequest) {
// 錯誤 1,錯誤3
} else if errors.Is(err, ErrBadRequest) {
// 錯誤 2
}
在具體專案中,呼叫函式或方法時,我們不知道底層是否進行了錯誤巢狀,如果不明確的話就統一使用
Is
函式。錯誤 1
和錯誤 2
沒有巢狀也可以判斷。
3. errors.As
這個和 errors.Is
函式類似,不同點就是該函式只判斷錯誤型別,而 errors.Is
函式不僅判斷型別,也要判斷值(錯誤訊息)。
// error/as.go
package main
import (
"errors"
"fmt"
)
type ErrorString struct {
s string
}
func (e *ErrorString) Error() string {
return e.s
}
func main() {
var targetErr *ErrorString
err := fmt.Errorf("new error:[%w]", &ErrorString{s: "target err"})
fmt.Println(errors.As(err, &targetErr))
}
// 輸出
true
targetErr
變數有幾點要求:
無需初始化。
必須是指標型別,並且實現了
error
介面。As
函式不接受nil
,因此不能直接使用targetErr
變數,要使用其引用&targetErr
。
什麼是異常
錯誤就是上述所講的,它的出現不會導致程式異常退出。
那什麼情況會異常退出呢?比如:
下標越界
除數為 0
等等
通常該異常是你沒留意到程式碼問題而丟擲,當然你可以可以主動丟擲。
panic
使用 panic
函式可以主動丟擲異常,該函式格式如下:
func panic(v interface{})
v
引數為空介面型別,那就說明可以接受任意型別資料。
例:
panic("我是異常")
// 輸出
panic: 我是異常
goroutine 1 [running]:
main.main()
C:/workspace/go/src/gobasic/panic/panic.go:4 +0x45
exit status 2
從輸出的結果上可以看出幾點:
列印出具體的異常位置,這些資訊稱作堆疊資訊。
程式終止,退出碼為 2。
處理異常
不管是主動丟擲異常,還是你程式哪塊有 bug 被動丟擲異常,這些在我們寫專案時都是很嚴重的問題,因為它會導致我們的程式異常退出。
在其它語言中,通過try/catch
機制可以捕捉異常來保證程式的正常執行,那在 Go 語言中使用 recover
函式捕捉異常。
在學習這個函式前,先要了解另外一個關鍵字 defer
。
1. defer
它不是函式,只是一個關鍵字。該關鍵字後面所跟的語句將延遲執行,在所在函式或方法正常結束時或出現異常中斷時,再提前執行。
package main
import "fmt"
func main() {
defer func() {
fmt.Println("defer")
}()
fmt.Println("main")
panic("panic")
}
// 輸出
main
defer
panic: panic
...
defer
關鍵字後面跟了一個匿名函式呼叫,有名字的函式當然也可以。遇到
defer
關鍵字,後面的語句會延遲執行,因此先輸出”main”字串。panic
丟擲異常,因此在退出前先執行defer
語句。如果呼叫了
os.Exit
函式,defer
後的語句將不會執行。
如果在一個函式或方法中出現了多個 defer
語句,那會採用先進後出原則,即先出現的 defer
語句後執行,後出現的先執行。
例:
package main
import "fmt"
func Fun1() {
fmt.Println("猜我啥時候輸出?")
}
func main() {
defer func() {
fmt.Println("defer")
}()
defer Fun1()
fmt.Println("main")
panic("panic")
}
// 輸出
main
猜我啥時候輸出?
defer
panic: panic
...
現在講了 defer
關鍵字的概念,可能還不知道實際中什麼地方用到,我現在舉個例子。
// defer/main.go
// 拷貝檔案
// srcName 路徑檔案拷貝到 dstName 路徑檔案
func CopyFile(srcName, dstName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
程式碼中有兩個 defer
關鍵字,後面的語句用來關閉檔案釋放資源。
如果不用此關鍵字,關閉檔案的Close
函式就必須寫在 io.Copy
函式後,因為該函式還要使用檔案資源,提前關閉了,就完蛋了。所以,使用 defer
關鍵字後就會延遲執行,因此就不需要考慮檔案什麼時候被使用。
當然也不是隻有這一種情況應用,只要記住,在函式或方法結束前你才想處理的語句都可以使用 defer
關鍵字。
2. recover
瞭解了 defer
關鍵字後就明白了,在程式出現異常之前 defer
語句先被執行,因此在 defer
後的語句就可以提前攔截異常。
// panic/recover.go
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("我捕捉的:", err)
fmt.Println("我好好的")
}
}()
panic("我是異常")
}
// 輸出
我捕捉的: 我是異常
我好好的
recover
返回異常資訊。
不管是主動丟擲異常,還是被動的,recover
函式都能捕捉,這樣以保證程式的正常進行。
假如函式 A
呼叫了很多函式,這些函式又呼叫了很多,只要下面被呼叫的函式出現異常,函式 A
中的 recover
函式將都可以捕捉到。但其中還是有個特例, Goroutine
中出現的異常是無法被捕捉到的,必須在新的 Goroutine
中重新使用 recover
函式,這個讓人期待的知識點後續會講。
總結
本篇講解了兩個概念,”錯誤“和”異常“ ,這兩個概念很重要,一定要掌握明白。還有一點,在處理錯誤時,有時候發覺錯誤結果對於我們沒用,那這個時候就可以忽略掉。
我寫的時候也想盡可能的講明白,有些地方就會囉嗦些。
本作品採用《CC 協議》,轉載必須註明作者和本文連結