詳解Go語言的錯誤處理和資源管理
一、defer
1. defer保證在函數(shù)結(jié)束時發(fā)生.
2. defer列表為先進后出
3. 參數(shù)在defer語句時計算.
下面來看一個例子: 寫入文件
package main import ( "aaa/functional/fbi" "bufio" "fmt" "os" ) // 我要寫文件 func writeFile() { file, err := os.Create("test.txt") if err != nil { panic("error") } defer file.Close() w := bufio.NewWriter(file) defer w.Flush() f := fbi.Feibonaccq() for i := 0; i < 20; i++ { fmt.Fprintln(w, f()) } } func main() { writeFile() }
package fbi func Feibonaccq() func() int { x, y := 0, 1 return func() int { x, y = y, x+y return x } }
將斐波那契數(shù)列寫入文件. 這里有兩個資源使用. 1. 創(chuàng)建文件, 然后文件關(guān)閉. 2. 寫入資源, 將資源從緩存中刷入文件. 這兩個操作都應(yīng)該應(yīng)該是成對出現(xiàn)的, 因此, 用defer 語句, 避免后面寫著寫著忘了, 也保證即使出錯了, 也能夠執(zhí)行defer語句的內(nèi)容
那么參數(shù)在defer語句時計算 是什么意思呢?
func tryDefer() { for i := 0; i < 10 ; i++ { defer fmt.Println(i) } }
打印結(jié)果:
9
8
7
6
5
4
3
2
1
0
二、錯誤處理
所謂的錯誤處理, 就是處理已知的錯誤, 不要拋出panic這樣導(dǎo)致系統(tǒng)掛掉的錯誤發(fā)生.
比如下面的操作:
package main import ( "aaa/functional/fbi" "bufio" "fmt" "os" ) // 我要寫文件 func writeFile(filename string) { // os.O_EXCL|os.O_CREATE創(chuàng)建一個新文件, 并且他必須不存在 file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) // 這時候打印panic就不太友好. 我們可以對錯誤類型進行處理 /*if err != nil { panic("error") }*/ // 這里就對錯誤的類型進行了捕獲處理. if err, ok := err.(*os.PathError); !ok { fmt.Println("未知錯誤") } else { fmt.Printf("%s, %s, %s", err.Path, err.Op, err.Err) } defer file.Close() w := bufio.NewWriter(file) defer w.Flush() f := fbi.Feibonaccq() for i := 0; i < 20; i++ { fmt.Fprintln(w, f()) } } func main() { writeFile("test.txt") }
紅色字體部分就是對錯誤進行了捕獲處理.
三、統(tǒng)一錯誤處理的邏輯
下面模擬一個web服務(wù)器, 在瀏覽器地址欄輸入文件的url, 然后顯示文件的內(nèi)容. 比如斐波那契數(shù)列的文件
package main import ( "io/ioutil" "net/http" "os" ) // 我們來模擬一個web服務(wù)器. 在url上輸入一個地址, 然后顯示文件內(nèi)容 // 做一個顯示文件的web server func main() { http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request) { // 獲取url路徑, 路徑是/list/之后的部分 path := request.URL.Path[len("/list/"):] // 打開文件 file, err := os.Open(path) if err != nil { panic("err") } defer file.Close() // 讀出文件 b, err := ioutil.ReadAll(file) if err != nil { panic("err") } // 寫入文件到頁面 writer.Write(b) }) // 監(jiān)聽端口:8888 err := http.ListenAndServe(":8888", nil) if err != nil { panic("err") } }
這里面主要注意一下我們對錯誤的處理. 都是直接打出panic. 這樣是很不友好的.
如果頁面輸入的文件路徑不對, 則直接404
按照之前第二步說的, 我們應(yīng)該對panic進行處理. 比如打開文件的操作, 我們改為如下
// 打開文件 file, err := os.Open(path) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } defer file.Close()
這樣就好多了, 起碼程序不會直接拋出異常
這是將系統(tǒng)的錯誤直接打出了, 比上面好一些, 但也不是特別友好, 通常我們不希望吧系統(tǒng)內(nèi)部錯誤輸出出來. 我們希望經(jīng)過包裝后輸出錯誤
于是做了如下修改.
第一步: 將http.handleFunc中的函數(shù)部分提出來, 這部分是業(yè)務(wù)邏輯.
提出來以后做了如下修改. 1. 函數(shù)增加一個返回值error. 2. 遇到錯誤,直接return. 如下紅色標出部分
package fileListener import ( "io/ioutil" "net/http" "os" ) func FileHandler(writer http.ResponseWriter, request *http.Request) error{ // 獲取url路徑, 路徑是/list/之后的部分 path := request.URL.Path[len("/list/"):] // 打開文件 file, err := os.Open(path) if err != nil { return err } defer file.Close() // 讀出文件 b, err := ioutil.ReadAll(file) if err != nil { return err } // 寫入文件到頁面 writer.Write(b) return nil }
第二: 封裝錯誤內(nèi)容
這里就體現(xiàn)了函數(shù)式編程的特點, 靈活
// 定義一個函數(shù)類型的結(jié)構(gòu), 返回值是erro type Handler func(writer http.ResponseWriter, request *http.Request) error // 封裝error func WrapHandler(handler Handler) func (http.ResponseWriter, *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { // 執(zhí)行原來的邏輯. 然后增加error的錯誤處理 err := handler(writer, request) if err != nil { code := http.StatusOK switch { case os.IsNotExist(err): code = http.StatusNotFound case os.IsPermission(err): code = http.StatusServiceUnavailable default: code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } }
調(diào)用的部分
// 我們來模擬一個web服務(wù)器. 在url上輸入一個地址, 然后顯示文件內(nèi)容 // 做一個顯示文件的web server func main() { http.HandleFunc("/list/", WrapHandler(fileListener.FileHandler)) // 監(jiān)聽端口:8888 err := http.ListenAndServe(":8888", nil) if err != nil { panic("err") } }
這樣, 當我們再次輸入錯誤的文件路徑時, 提示信息如下:
四、panic
發(fā)生panic的時候, 會做那些事呢?
1. 停止當前函數(shù)的執(zhí)行
2. 一直向上返回, 執(zhí)行每一層的defer
3. 如果沒有遇到recover, 程序就退出
五、recover
1. 在defer 中調(diào)用
2. 獲取panic的值
3. 如果無法處理, 可以重新panic
package main import ( "fmt" "github.com/pkg/errors" ) func tryRecover() { defer func(){ r := recover() if r, ok := r.(error); ok { fmt.Println("error 發(fā)生", r.Error()) } else { panic(fmt.Sprintf("未知錯誤:%v", r)) } }() panic(errors.New("錯誤")) } func main() { tryRecover() }
六、error vs panic
七、錯誤處理綜合示例
第五條的案例, 我們進行了error的統(tǒng)一管理, 但是還沒有對其他異常進行recover, 還有可能導(dǎo)致程序崩潰. 比如http://localhost:8888/abc. 繼續(xù)優(yōu)化代碼.
這樣很不友好, 我們在看看控制臺, 發(fā)現(xiàn)程序并沒有掛掉, 這是為什么呢? 想象一下, 應(yīng)該是程序自動給我們recover了.
我們來看看server.go
原來server.go已經(jīng)幫我們recover了, recover后并不是中斷進程, 而是打印輸出錯誤日志. 雖然如此, 但頁面顯示依然很難看. 因此我們要做兩件事
1. 如果出現(xiàn)異常, 我們自己進行recover, 那么他就不會走系統(tǒng)定義的recover了. 這還不夠, 這只是說控制臺不會再打印出一大堆藍色異常代碼了. 我們還有做第二件事
2. 將出現(xiàn)異常的位置捕獲出來, 并且, 打印到頁面
第一步: 自定一定recover, 代替server.go中的recover
// 封裝error func WrapError(handler Handler) func (http.ResponseWriter, *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { defer func(){ if r := recover(); r != nil { fmt.Println("發(fā)生錯誤") http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() // 執(zhí)行原來的邏輯. 然后增加error的錯誤處理 err := handler(writer, request) if err != nil { code := http.StatusOK switch { case os.IsNotExist(err): code = http.StatusNotFound case os.IsPermission(err): code = http.StatusServiceUnavailable default: code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } }
這樣異常就被我們捕獲了, 頁面打印出
這樣就好看多了. 我們在對代碼進行優(yōu)化
我們將發(fā)生異常的地方進行處理
func FileHandler(writer http.ResponseWriter, request *http.Request) error { // 獲取url路徑, 路徑是/list/之后的部分 if c := strings.Index(request.URL.Path, "/list/"); c != 0 { return errors.New("url 不是已list開頭") } path := request.URL.Path[len("/list/"):] // 打開文件 file, err := os.Open(path) if err != nil { return err } defer file.Close() // 讀出文件 b, err := ioutil.ReadAll(file) if err != nil { return err } // 寫入文件到頁面 writer.Write(b) return nil }
頁面打印效果
我們發(fā)現(xiàn)這個打印的還是系統(tǒng)給出的錯誤異常. 那么,我們有沒有辦法, 把這個異常打印出來呢?
if c := strings.Index(request.URL.Path, "/list/"); c != 0 { return errors.New("url 不是已list開頭") }
我們自己來定義一個異常處理的接口
type userError interface { error // 系統(tǒng)異常 Message() string // 用戶自定義異常 }
接口定義好了, 在哪里用呢? 你想打印出自己的異常信息, 那就不能打印系統(tǒng)的. 自定義信息在系統(tǒng)異常之前判斷
// 執(zhí)行原來的邏輯. 然后增加error的錯誤處理 err := handler(writer, request) if err != nil { if userErr, ok := err.(userError); ok { http.Error(writer, userErr.Message(), http.StatusBadRequest) return } code := http.StatusOK switch { case os.IsNotExist(err): code = http.StatusNotFound case os.IsPermission(err): code = http.StatusServiceUnavailable default: code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) }
接下來是具體實現(xiàn)了, 現(xiàn)在用戶想要實現(xiàn)自定義一個userError. 然后設(shè)置異常類型為userError
type userError string func (u userError) Error() string{ return u.Message() } func (u userError) Message() string { return string(u) }
func FileHandler(writer http.ResponseWriter, request *http.Request) error { // 獲取url路徑, 路徑是/list/之后的部分 if c := strings.Index(request.URL.Path, "/list/"); c != 0 { return userError("url 不是已list開頭") } path := request.URL.Path[len("/list/"):] // 打開文件 file, err := os.Open(path) if err != nil { return err } defer file.Close() // 讀出文件 b, err := ioutil.ReadAll(file) if err != nil { return err } // 寫入文件到頁面 writer.Write(b) return nil }
這樣一個實現(xiàn)自定義打印異常的功能就做好了. 異常也是可以封裝的.
最后再來梳理這個小案例:
1. 我們有一個想法, 模擬web請求, 在瀏覽器url上輸入一個文件路徑, 打印文件的內(nèi)容
2. 內(nèi)容可能有錯誤, 進行異常處理.
3. 有時候異常拋出的是系統(tǒng)給出, 我們自己對異常進行recover, 然后打印出來
4. 打印自定義異常.
以下是完整代碼
package handling import ( "io/ioutil" "net/http" "os" "strings" ) type UserError struct { Content string } func (u UserError) Error() string { return u.Message() } func (u UserError) Message() string { return u.Content } func Hanldering(writer http.ResponseWriter, request *http.Request) error { // 獲取url, list之后的就是url if s := strings.Index(request.URL.Path, "/list/"); s != 0 { return UserError{"path error, /list/"} } url := request.URL.Path[len("/list/"):] // 根據(jù)url打開文件 file, err := os.Open(url) if err != nil { return os.ErrNotExist } defer file.Close() // 打開以后把文件內(nèi)容讀出來 f, err := ioutil.ReadAll(file) if err != nil { return os.ErrPermission } // 讀出來以后, 寫入到頁面 writer.Write(f) return nil }
package main import ( "aaa/handlerError/linstenerFile/handling" "github.com/siddontang/go/log" "net/http" "os" ) type ErrorHandlering func(writer http.ResponseWriter, request *http.Request) error func WrapError(handler ErrorHandlering) func(http.ResponseWriter, *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { defer func() { if r := recover(); r != nil { log.Warn("other error") http.Error(writer, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } }() err := handler(writer, request) //自定義異常處理 // 錯誤處理 if err != nil { if userErr, ok := err.(UserError); ok { log.Warn("user error:", userErr.Message()) http.Error(writer, userErr.Message(), http.StatusBadRequest) return } code := http.StatusOK switch err { case os.ErrNotExist: code = http.StatusNotFound case os.ErrPermission: code = http.StatusBadRequest default: code = http.StatusInternalServerError } http.Error(writer, http.StatusText(code), code) } } } type UserError interface { error Message() string } func main() { // 模擬web請求 http.HandleFunc("/", WrapError(handling.Hanldering)) // 指定服務(wù)端口 http.ListenAndServe(":8888", nil) }
以上就是詳解Go語言的錯誤處理和資源管理的詳細內(nèi)容,更多關(guān)于Go 錯誤處理 資源管理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go調(diào)度器學(xué)習(xí)之系統(tǒng)調(diào)用詳解
這篇文章腫,將以一個簡單的文件打開的系統(tǒng)調(diào)用,來分析一下Go調(diào)度器在系統(tǒng)調(diào)用時做了什么。文中的示例代碼講解詳細,需要的可以參考一下2023-04-04Go語言之重要數(shù)組類型切片(slice)make,append函數(shù)解讀
這篇文章主要介紹了Go語言之重要數(shù)組類型切片(slice)make,append函數(shù)用法,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09golang?四則運算計算器yacc歸約手寫實現(xiàn)
這篇文章主要為大家介紹了golang?四則運算?計算器?yacc?歸約的手寫實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07