GO 編程模式系列(二):錯誤處理
原文作者:陳皓(左耳朵耗子)
內(nèi)容出處:
https://coolshell.cn/articles/21128.html

if err != nil?。if err != nil?怎么辦這個事之前,我想先說一說編程中的錯誤處理。這樣可以讓大家在更高的層面理解編程中的錯誤處理。本文是全系列中第2 / 9篇:Go編程模式
Go編程模式:切片,接口,時間和性能 Go 編程模式:錯誤處理 Go 編程模式:Functional Options Go編程模式:委托和反轉(zhuǎn)控制 Go編程模式:Map-Reduce Go 編程模式:Go Generation Go編程模式:修飾器 Go編程模式:Pipeline Go 編程模式:k8s Visitor 模式
C語言的錯誤檢查
errno?變量并配合一個?errstr?的數(shù)組來告訴你為什么出錯。read(),?write(),?open()?這些函數(shù)的返回值其實是返回有業(yè)務邏輯的值。也就是說,這些函數(shù)的返回值有兩種語義,一種是成功的值,比如?open()?返回的文件句柄指針?FILE*?,或是錯誤?NULL。這樣會導致調(diào)用者并不知道是什么原因出錯了,需要去檢查?errno?來獲得出錯的原因,從而可以正確地處理錯誤。int atoi(const char *str)0,那么會和正常的對 “0” 字符的返回值完全混淆在一起。這樣就無法判斷出錯的情況。你可能會說,是不是要檢查一下?errno,按道理說應該是要去檢查的,但是,我們在 C99 的規(guī)格說明書中可以看到這樣的描述——7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is unde?ned.
atoi(),?atof(),?atol()?或是?atoll()?這樣的函數(shù)是不會設置?errno的,而且,還說了,如果結(jié)果無法計算的話,行為是undefined。所以,后來,libc 又給出了一個新的函數(shù)strtol(),這個函數(shù)在出錯的時會設置全局變量?errno?:long val = strtol(in_str, &endptr, 10); //10的意思是10進制//如果無法轉(zhuǎn)換if (endptr == str) {fprintf(stderr, "No digits were found\n");exit(EXIT_FAILURE);}//如果整型溢出了if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) {fprintf(stderr, "ERROR: number out of range for LONG\n");exit(EXIT_FAILURE);}//如果是其它錯誤if (errno != 0 && val == 0) {perror("strtol");exit(EXIT_FAILURE);}
strtol()?函數(shù)解決了?atoi()?函數(shù)的問題,但是我們還是能感覺到不是很舒服和自然。程序員一不小心就會忘記返回值的檢查,從而造成代碼的 Bug; 函數(shù)接口非常不純潔,正常值和錯誤值混淆在一起,導致語義有問題。
HRESULT?的返回來統(tǒng)一錯誤的返回值,這樣可以明確函數(shù)調(diào)用時的返回值是成功還是錯誤。但這樣一來,函數(shù)的 input 和 output 只能通過函數(shù)的參數(shù)來完成,于是出現(xiàn)了所謂的 入?yún)?和 出參 這樣的區(qū)別。Java的錯誤處理
try-catch-finally?通過使用異常的方式來處理錯誤,其實,這比起C語言的錯處理進了一大步,使用拋異常和抓異常的方式可以讓我們的代碼有這樣的一些好處:函數(shù)接口在 input(參數(shù))和 output(返回值)以及錯誤處理的語義是比較清楚的。 正常邏輯的代碼可以與錯誤處理和資源清理的代碼分開,提高了代碼的可讀性。 異常不能被忽略(如果要忽略也需要 catch 住,這是顯式忽略)。 在面向?qū)ο蟮恼Z言中(如 Java),異常是個對象,所以,可以實現(xiàn)多態(tài)式的 catch。 與狀態(tài)返回碼相比,異常捕捉有一個顯著的好處是,函數(shù)可以嵌套調(diào)用,或是鏈式調(diào)用。比如: int x = add(a, div(b,c));Pizza p = PizzaBuilder().SetSize(sz).SetPrice(p)...;
Go語言的錯誤處理
參數(shù)上基本上就是入?yún)?,而返回接口把結(jié)果和錯誤分離,這樣使得函數(shù)的接口語義清晰; 而且,Go 語言中的錯誤參數(shù)如果要忽略,需要顯式地忽略,用 _ 這樣的變量來忽略; 另外,因為返回的? error?是個接口(其中只有一個方法?Error(),返回一個?string?),所以你可以擴展自定義的錯誤處理。
error,你也可以使用下面這樣的方式:if err != nil {switch err.(type) {case *json.SyntaxError:...case *ZeroDivisionError:...case *NullPointerError:...default:...}}
資源清理
C語言 – 使用的是? goto fail;?的方式到一個集中的地方進行清理(有篇有意思的文章可以看一下《由蘋果的低級BUG想到的》)C++語言- 一般來說使用 RAII模式,通過面向?qū)ο蟮拇砟J?,把需要清理的資源交給一個代理類,然后在析構(gòu)函數(shù)來解決。 Java語言 – 可以在finally 語句塊里進行清理。 Go語言 – 使用? defer?關鍵詞進行清理。
func Close(c io.Closer) {err := c.Close()if err != nil {log.Fatal(err)}}func main() {r, err := Open("a")if err != nil {log.Fatalf("error opening 'a'\n")}defer Close(r) // 使用defer關鍵字在函數(shù)退出時關閉文件。r, err = Open("b")if err != nil {log.Fatalf("error opening 'b'\n")}defer Close(r) // 使用defer關鍵字在函數(shù)退出時關閉文件。}
Error Check? Hell
if err !=nil?的代碼了,這樣的代碼的確是能讓人寫到吐。那么有沒有什么好的方式呢,有的。我們先看如下的一個令人崩潰的代碼。func parse(r io.Reader) (*Point, error) {var p Pointif err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {return nil, err}if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {return nil, err}if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {return nil, err}if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {return nil, err}if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {return nil, err}}
func parse(r io.Reader) (*Point, error) {var p Pointvar err errorread := func(data interface{}) {if err != nil {return}err = binary.Read(r, binary.BigEndian, data)}read(&p.Longitude)read(&p.Latitude)read(&p.Distance)read(&p.ElevationGain)read(&p.ElevationLoss)if err != nil {return &p, err}return &p, nil}
if err!=nil?處理的很干凈了。但是會帶來一個問題,那就是有一個?err?變量和一個內(nèi)部的函數(shù),感覺不是很干凈。bufio.Scanner()中似乎可以學習到一些東西:scanner := bufio.NewScanner(input)for scanner.Scan() {token := scanner.Text()// process token}if err := scanner.Err(); err != nil {// process the error}
scanner在操作底層的I/O的時候,那個for-loop中沒有任何的?if err !=nil?的情況,退出循環(huán)后有一個?scanner.Err()?的檢查??磥硎褂昧私Y(jié)構(gòu)體的方式。模仿它,我們可以把我們的代碼重構(gòu)成下面這樣:type Reader struct {r io.Readererr error}func (r *Reader) read(data interface{}) {if r.err == nil {r.err = binary.Read(r.r, binary.BigEndian, data)}}
func parse(input io.Reader) (*Point, error) {var p Pointr := Reader{r: input}r.read(&p.Longitude)r.read(&p.Latitude)r.read(&p.Distance)r.read(&p.ElevationGain)r.read(&p.ElevationLoss)if r.err != nil {return nil, r.err}return &p, nil}
package mainimport ("bytes""encoding/binary""fmt")// 長度不夠,少一個Weightvar b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c}var r = bytes.NewReader(b)type Person struct {Name [10]byteAge uint8Weight uint8err error}func (p *Person) read(data interface{}) {if p.err == nil {p.err = binary.Read(r, binary.BigEndian, data)}}func (p *Person) ReadName() *Person {p.read(&p.Name)return p}func (p *Person) ReadAge() *Person {p.read(&p.Age)return p}func (p *Person) ReadWeight() *Person {p.read(&p.Weight)return p}func (p *Person) Print() *Person {if p.err == nil {fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)}return p}func main() {p := Person{}p.ReadName().ReadAge().ReadWeight().Print()fmt.Println(p.err) // EOF 錯誤}
if err != nil的方式。包裝錯誤
err給返回到上層,我們需要把一些執(zhí)行的上下文加入。fmt.Errorf()來完成這個事,比如:if err != nil {return fmt.Errorf("something failed: %v", err)}
type authorizationError struct {operation stringerr error // original error}func (e *authorizationError) Error() string {return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)}
causer接口中實現(xiàn)?Cause()?方法來暴露原始錯誤,以供進一步檢查:type causer interface {Cause() error}func (e *authorizationError) Cause() error {return e.err}
import"github.com/pkg/errors"//錯誤包裝if err != nil{return errors.Wrap(err, "read failed")}// Cause接口switch err := errors.Cause(err).(type){case *MyError:// handle specificallydefault:// unknown error}
參考文章
Golang Error Handling lesson by Rob Pike
http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pikeErrors are values
https://blog.golang.org/errors-are-values
評論
圖片
表情
