郭學聰 Hsueh-Tsung Kuo
Sat, 14 Nov 2020
Someone (who?) said:
a game programmer should be able to draw cute anime character(?)
func MyFunc(param1 int, param2 string) (string, error) { // do something... f, err := os.Open("filename.ext") if err != nil { return "", err } // do something... return result, nil }
Why?
func MyFunc(param1 int, param2 string) (string, error) { // do something... f, err := os.Open("filename.ext") if err != nil { return "", err } // do something... return result, nil }
ErrSomething = errors.New("error description") return ErrSomething err := fmt.Errorf("error %s", detail) return err
// wrap error err = fmt.Errorf("... %w ...", ..., cause, ...) // unwrap and return NEXT error, or nil cause = errors.Unwrap(err)
errors.Is(err, ErrorInstance) // must be same address var perr *ErrorType errors.As(err, &perr)
package main import ( "errors" "fmt" ) type MyError struct { msg string err error } func NewMyError(msg string, err error) *MyError { if err != nil { msg = fmt.Sprintf("%s: %s", msg, err.Error()) } return &MyError{ msg: msg, err: err, } } func (e *MyError) Error() string { return e.msg } func (e *MyError) Unwrap() error { return e.err } func main() { var err error err = errors.New("err1") err = fmt.Errorf("err2: %w", err) myErr := NewMyError("my err3", err) myErr2 := NewMyError("my err3", err) err = myErr err = fmt.Errorf("err4: %w", err) err = fmt.Errorf("err5: %w", err) if err != nil { fmt.Println("error is:", err) if errors.Is(err, myErr) { fmt.Println("it is myErr!") } if !errors.Is(err, myErr2) { fmt.Println("it is NOT myErr2!") } fmt.Println("") var myError *MyError if errors.As(err, &myError) { fmt.Println("failed:", err.Error()) fmt.Println("MyError:", myError.Error()) } } }
error is: err5: err4: my err3: err2: err1
it is myErr!
it is NOT myErr2!
failed: err5: err4: my err3: err2: err1
MyError: my err3: err2: err1
// new error err = xerrors.New("...") // wrap error err = xerrors.Errorf("... %w ...", ..., cause, ...) // print error chain and frames fmt.Printf("%+v\n", err)
package main import ( "fmt" "golang.org/x/xerrors" ) func MyFuncInner() error { return xerrors.New("inner error") } func MyFuncMiddle() error { err := MyFuncInner() return xerrors.Errorf("middle error: %w", err) } func MyFuncOuter() error { err := MyFuncMiddle() return xerrors.Errorf("outer error: %w", err) } func main() { err := MyFuncOuter() fmt.Printf("%v\n\n", err) fmt.Printf("%+v\n", err) }
outer error: middle error: inner error
outer error:
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:20
- middle error:
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:15
- inner error:
main.MyFuncInner
/.../go/src/survive_under_the_crap_go_error_system/xerrors_example.go:10
// new error err = pkg_errors.New("...") // wrap error err = pkg_errors.Wrap(cause, "...") err = pkg_errors.Wrapf(cause, "oh noes #%d", 2) // unwrap the whole error chain and return the MOST INNER error, or return the error itself cause = pkg_errors.Cause(err)
fmt.Printf("%+v\n", err)
package main import ( "fmt" pkg_errors "github.com/pkg/errors" ) func MyFuncInner() error { return pkg_errors.New("inner error") } func MyFuncMiddle() error { err := MyFuncInner() return pkg_errors.Wrap(err, "middle error") } func MyFuncOuter() error { err := MyFuncMiddle() return pkg_errors.Wrapf(err, "outer error - %d", 2) } func main() { err := MyFuncOuter() fmt.Printf("%v\n\n", err) fmt.Printf("%+v\n", err) }
outer error - 2: middle error: inner error
inner error
main.MyFuncInner
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:10
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:14
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:19
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
middle error
main.MyFuncMiddle
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:15
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:19
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
outer error - 2
main.MyFuncOuter
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:20
main.main
/.../go/src/survive_under_the_crap_go_error_system/pkg_errors_example.go:24
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
// push error c.Error(err) // list error c.Errors.ByType(errType)
Wrap as early as possible
import ( "errors" "fmt" "golang.org/x/xerrors" pkg_errors "github.com/pkg/errors" ) func MyFunc(param1 int, param2 string) (string, error) { // do something... r, err := foreign_package.ForeignFunc() if err != nil { err = fmt.Errorf("<error description>: %w", err) err = xerrors.Errorf("<error description>: %w", err) err = pkg_errors.Wrap(err, "<error description>") return "", err } // do something... return result, nil }
Collect and log error at entry function or middleware
// for Go 1.13 error system func ExtractErrorMessageChain(err error) string { var b strings.Builder for e := err; e != nil; e = errors.Unwrap(e) { b.WriteString(e.Error()) b.WriteString("\n") } return b.String() } func MyEntryFunc() { // do something... r, err := MyFunc(param1, param2) // for Go 1.13 error system fmt.Printf("%s", ExtractErrorMessageChain(err)) // for golang.org/x/xerrors & github.com/pkg/errors fmt.Printf("%+v\n", err) }
Only show the most outer error message to end-user
// for Go 1.13 error system & golang.org/x/xerrors func WrapError(msg string, err error) error { return fmt.Errorf("%s: %w", msg, err) } // for all error frameworks func GetErrorMessageTitle(err error) string { return strings.SplitN(err.Error(), ":", 2)[0] }
package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" //"reflect" "github.com/julienschmidt/httprouter" ) // utility function // for Go 1.13 error system func ExtractErrorMessageChain(err error) string { var b strings.Builder for e := err; e != nil; e = errors.Unwrap(e) { b.WriteString(e.Error()) b.WriteString("\n") } return b.String() } // for Go 1.13 error system & golang.org/x/xerrors func WrapError(msg string, err error) error { return fmt.Errorf("%s: %w", msg, err) } // for all error frameworks func GetErrorMessageTitle(err error) string { return strings.SplitN(err.Error(), ":", 2)[0] } // compare error using first part of message before colon, for example: "<first>: <rest>" func IsErrorEqual(e1 error, e2 error) bool { if e1 == nil || e2 == nil { return e1 == e2 } else { // CAUTION: error type is useless (in most cases) // return reflect.TypeOf(e1) == reflect.TypeOf(e2) && GetErrorMessageTitle(e1) == GetErrorMessageTitle(e2) return GetErrorMessageTitle(e1) == GetErrorMessageTitle(e2) } } // ApiError type ApiError struct { statusCode int msg string err error } func NewApiError(statusCode int, msg string, err error) error { if err != nil { msg = fmt.Sprintf("%s: %s", msg, err.Error()) } return &ApiError{statusCode: statusCode, msg: msg, err: err} } func (e *ApiError) Error() string { return e.msg } func (e *ApiError) Unwrap() error { return e.err } func (e *ApiError) StatusCode() int { return e.statusCode } func WrapApiError(template error, err error) error { apiErrorTemplate, ok := template.(*ApiError) if ok { err = NewApiError(apiErrorTemplate.StatusCode(), apiErrorTemplate.Error(), err) } else { err = WrapError(template.Error(), err) } return err } // handler type HandleE func(http.ResponseWriter, *http.Request, httprouter.Params) error func ErrAwareHandle(h HandleE) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { err := h(w, r, ps) if err != nil { // respond error with simplified message errorMessage := GetErrorMessageTitle(err) var statusCode int switch e := err.(type) { case *ApiError: statusCode = e.StatusCode() default: statusCode = http.StatusInternalServerError } http.Error(w, errorMessage, statusCode) // log detailed error messageChain := ExtractErrorMessageChain(err) fmt.Fprintf(os.Stderr, "error:\n%s====\n", messageChain) } } } // usage var ( ErrEmptyMessage = errors.New("message is empty") ErrDirtyMessage = errors.New("助兵衛") ErrHentaiMessage = errors.New("変態") ) func detectNonDirtyMessage(msg string) error { if msg == "FQ" { return ErrDirtyMessage } return nil } func processMessage(msg string) (string, error) { if msg == "" { return "", ErrEmptyMessage } err := detectNonDirtyMessage(msg) if err != nil { return "", WrapApiError(ErrHentaiMessage, err) } return msg, nil } var ( ErrMessageLost = NewApiError(http.StatusBadRequest, "message lost", nil) ErrYouDirty = NewApiError(http.StatusForbidden, "you dirty", nil) ) type Input struct { Msg string `json:msg` } func example(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { name := ps.ByName("name") if name != "say" { err := errors.New("must be /error_test/say") return NewApiError(http.StatusNotFound, "not found", err) } body, err := ioutil.ReadAll(r.Body) if err != nil { // this statement won't wrap error, it just concatenates error message return fmt.Errorf("body read failed: %v", err) } if len(body) < 2 { return errors.New("body is not enough") } var input Input if err = json.Unmarshal(body, &input); err != nil { return NewApiError(http.StatusBadRequest, "bad request", err) } resp, err := processMessage(input.Msg) if IsErrorEqual(err, ErrEmptyMessage) { return WrapApiError(ErrMessageLost, err) } if IsErrorEqual(err, ErrHentaiMessage) { return WrapApiError(ErrYouDirty, err) } if err != nil { return err } w.WriteHeader(200) w.Write([]byte("you sent:\n")) w.Write([]byte(resp)) w.Write([]byte("\n")) return nil } func main() { router := httprouter.New() router.POST("/error_test/:name", ErrAwareHandle(example)) err := http.ListenAndServe(":8000", router) if err != nil { panic(err) } } // client example // curl -v -X POST --data '{"msg": "FQ2"}' http://localhost:8000/error_test/say
"403|forbidden: <error description>: <error description>: ..."
func MyFunc(param1 int, param2 string) (string, error) { // do something... f, err := os.Open("filename.ext") if err != nil { return "", err } // do something... n, err := io.ReadFull(f, buf1) if err != nil { return "", err } // do something... n, err = f.Read(buf2) if err != nil { return "", err } // do something... return result, nil }
panic!() Result<T, E> Option<T> myobj.myfn()? // ? operator
e.source() e.backtrace()
func MyFunc(param1 int, param2 string) (string, error) { // do something... f := try(os.Open("filename.ext")) // do something... n := try(io.ReadFull(f, buf1)) // do something... n = try(f.Read(buf2)) // do something... return result, nil }
Because something mess up:
if err != nil { if err != io.EOF { return "", err } else { return result, nil } }
人生オワタ \(^o^)/ I am done!
Add handle:
func MyFunc(param1 int, param2 string) (string, error) { handle err { if err != io.EOF { return "", err } else { return result, nil } } // do something... f := check os.Open("filename.ext") // do something... n := check io.ReadFull(f, buf1) // do something... n = check f.Read(buf2) // do something... return result, nil }
https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md
錯誤抓精光 碼農發大財
郭學聰 Hsueh-Tsung Kuo2020_11_14