Golang中的error
Golang中的 error 就是一个简略的接口类型。只有实现了这个接口,就能够将其视为一种 error
type error interface {
Error() string
}
error的几种玩法
翻看Golang源码,能看到许多相似于上面的这两种error类型
哨兵谬误
var EOF = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")
var ErrNoProgress = errors.New("multiple Read calls return no data or error")
毛病:
1.让 error 具备二义性
error != nil不再意味着肯定产生了谬误
比方io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个谬误
2.在两个包之间创立了依赖
如果你应用了io.EOF来查看是否read完所有的数据,那么代码里肯定会导入io包
自定义谬误类型
一个不错的例子是os.PathError,它的长处是能够附带更多的上下文信息
type PathError struct {
Op string
Path string
Err error
}
Wrap error
到这里咱们能够发现,Golang 的 error 非常简单,然而简略也意味着有时候是不够用的
Golang的error始终有两个问题:
1.error没有附带file:line信息(也就是没有堆栈信息)
比方这种error,鬼晓得代码哪一行报了错,Debug时几乎要命
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
2.下层error想附带更多日志信息时,往往会应用fmt.Errorf()
,fmt.Errorf()
会创立一个新的error,底层的error类型就被“吞”掉了
var errNoRows = errors.New("no rows")
// 模拟sql库返回一个errNoRows
func sqlExec() error {
return errNoRows
}
func serviceNoErrWrap() error {
err := sqlExec()
if err != nil {
return fmt.Errorf("sqlExec failed.Err:%v", err)
}
return nil
}
func TestErrWrap(t *testing.T) {
// 应用fmt.Errorf创立了一个新的err,失落了底层err
err := serviceNoErrWrap()
if err != errNoRows {
log.Println("===== errType don't equal errNoRows =====")
}
}
-------------------------------代码运行后果----------------------------------
=== RUN TestErrWrap
2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
为了解决这个问题,咱们能够应用github.com/pkg/error包
,应用errors.withStack()办法
将err保
存到withStack对象
// withStack构造体保留了error,造成了一条error链。同时*stack字段保留了堆栈信息。
type withStack struct {
error
*stack
}
也能够应用errors.Wrap(err, "自定义文本")
,额定附带一些自定义的文本信息
源码解读:先将err和message包进withMessage对象
,再将withMessage对象
和堆栈信息包进withStack对象
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
Golang1.13版本error的新个性
Golang1.13版本借鉴了github.com/pkg/error包
,新增了如下函数,大大加强了 Golang 语言判断 error 类型的能力
errors.UnWrap()
// 与errors.Wrap()行为相同
// 获取err链中的底层err
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
errors.Is()
在1.13版本之前,咱们能够用err == targetErr
判断err类型errors.Is()
是其增强版:error 链上的任一err == targetErr
,即return true
// 实际:学习应用errors.Is()
var errNoRows = errors.New("no rows")
// 模拟sql库返回一个errNoRows
func sqlExec() error {
return errNoRows
}
func service() error {
err := sqlExec()
if err != nil {
return errors.WithStack(err) // 包装errNoRows
}
return nil
}
func TestErrIs(t *testing.T) {
err := service()
// errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true
if errors.Is(err, errNoRows) {
log.Println("===== errors.Is() succeeded =====")
}
//err经errors.WithStack包装,不能通过 == 判断err类型
if err == errNoRows {
log.Println("err == errNoRows")
}
}
-------------------------------代码运行后果----------------------------------
=== RUN TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====
例子解读:
因为应用errors.WithStack
包装了sqlError
,sqlError
位于error链的底层,下层的error曾经不再是sqlError
类型,所以应用==
无奈判断出底层的sqlError
源码解读:
- 咱们很容易想到其外部调用了
err = Unwrap(err)
办法来获取error链中底层的error -
自定义error类型能够实现
Is接口
来自定义error类型判断办法func Is(err, target error) bool { if target == nil { return err == target } isComparable := reflectlite.TypeOf(target).Comparable() for { if isComparable && err == target { return true } // 反对自定义error类型判断 if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { return true } if err = Unwrap(err); err == nil { return false } } }
上面咱们来看看如何自定义error类型判断:
自定义的errNoRows类型
,必须实现Is接口
,能力应用erros.Is()
进行类型判断
type errNoRows struct {
Desc string
}
func (e errNoRows) Unwrap() error { return e }
func (e errNoRows) Error() string { return e.Desc }
func (e errNoRows) Is(err error) bool {
return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
}
// 模拟sql库返回一个errNoRows
func sqlExec() error {
return &errNoRows{"Kaolengmian NB"}
}
func service() error {
err := sqlExec()
if err != nil {
return errors.WithStack(err)
}
return nil
}
func serviceNoErrWrap() error {
err := sqlExec()
if err != nil {
return fmt.Errorf("sqlExec failed.Err:%v", err)
}
return nil
}
func TestErrIs(t *testing.T) {
err := service()
if errors.Is(err, errNoRows{}) {
log.Println("===== errors.Is() succeeded =====")
}
}
-------------------------------代码运行后果----------------------------------
=== RUN TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====
errors.As()
在1.13版本之前,咱们能够用if _,ok := err.(targetErr)
判断err类型errors.As()
是其增强版:error 链上的任一err与targetErr类型雷同
,即return true
// 通过例子学习应用errors.As()
type sqlError struct {
error
}
func (e *sqlError) IsNoRows() bool {
t, ok := e.error.(ErrNoRows)
return ok && t.IsNoRows()
}
type ErrNoRows interface {
IsNoRows() bool
}
// 返回一个sqlError
func sqlExec() error {
return sqlError{}
}
// errors.WithStack包装sqlError
func service() error {
err := sqlExec()
if err != nil {
return errors.WithStack(err)
}
return nil
}
func TestErrAs(t *testing.T) {
err := service()
// 递归应用errors.UnWrap,只有Err链上有一种Err满足类型断言,即返回true
sr := &sqlError{}
if errors.As(err, sr) {
log.Println("===== errors.As() succeeded =====")
}
// 经errors.WithStack包装后,不能通过类型断言将以后Err转换成底层Err
if _, ok := err.(sqlError); ok {
log.Println("===== type assert succeeded =====")
}
}
----------------------------------代码运行后果--------------------------------------------
=== RUN TestErrAs
2022/03/25 18:09:02 ===== errors.As() succeeded =====
例子解读:
因为应用errors.WithStack
包装了sqlError
,sqlError
位于error链的底层,下层的error曾经不再是sqlError
类型,所以应用类型断言无奈判断出底层的sqlError
error解决最佳实际
下面讲了如何定义error类型,如何比拟error类型,当初咱们谈谈如何在大型项目中做好error解决
优先解决error
当一个函数返回一个非空error时,应该优先解决error,疏忽它的其余返回值
只解决error一次
在Golang中,对于每个err,咱们应该只解决一次。
-
要么立刻解决err(包含记日志等行为),return nil(把谬误吞掉)。此时因为把谬误做了降级,肯定要小心处理函数返回值。
比方上面例子json.Marshal(conf)没有return err ,那么在应用buf时肯定要小心空指针等谬误
-
要么return err,在下层解决err
反例:// 试想如果writeAll函数出错,会打印两遍日志 // 如果整个我的项目都这么做,最初会惊奇的发现咱们在处处打日志,我的项目中存在大量没有价值的垃圾日志 // unable to write:io.EOF // could not write config:io.EOF type config struct {} func writeAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) return err } return nil } func writeConfig(w io.Writer, conf *config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config:%v", err) } if err := writeAll(w, buf); err != nil { log.Println("count not write config: %v", err) return err } return nil }
不要重复包装error
咱们应该包装error,但只包装一次
下层业务代码倡议Wrap error
,然而底层根底Kit库不倡议
如果底层根底 Kit 库包装了一次,下层业务代码又包装了一次,就反复包装了 error,日志就会打重
比方咱们罕用的sql库
会返回sql.ErrNoRows
这种预约义谬误,而不是给咱们一个包装过的 error不通明的错误处理
在大型项目中,举荐应用
不通明的错误处理(Opaque errors)
:不关怀谬误类型,只关怀error是否为nil
益处: -
耦合小,不须要判断特定谬误类型,就不须要导入相干包的依赖。
不过有时候,这种解决error的形式不够用,比方:业务须要对参数异样error类型
做降级解决,打印Warn级别的日志type ParamInvalidError struct { Desc string } func (e ParamInvalidError) Unwrap() error { return e } func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc } func (e ParamInvalidError) Is(err error) bool { return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() } func NewParamInvalidErr(desc string) error { return errors.WithStack(&ParamInvalidError{Desc: desc}) } ------------------------------顶层打印日志--------------------------------- if errors.Is(err, Err.ParamInvalidError{}) { logger.Warnf(ctx, "%s", err.Error()) return } if err != nil { logger.Errorf(ctx, " error:%+v", err) }
简化错误处理
Golang因为代码中有数的
if err != nil
被诟病,当初咱们看看如何缩小if err != nil
这种代码bufio.scan
CountLines() 实现了”读取内容的行数”性能
能够利用 bufio.scan() 简化 error 的解决:func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err := br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, nilsadwawa } return lines, nil } func CountLinesGracefulErr(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() }
bufio.NewScanner()
返回一个 Scanner
对象,构造体外部蕴含了 error 类型,调用Err()
办法即可返回封装好的error
Golang源代码中蕴含着大量的优良设计思维,咱们在浏览源码时从中学习,并在实践中得以使用
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
func (s *Scanner) Err() error {
if s.err == io.EOF {
return nil
}
return s.err
}
errWriter
WriteResponse()
函数实现了"构建HttpResponse"
性能
利用下面学到的思路,咱们能够本人实现一个errWriter
对象,简化对 error 的解决
type Header struct {
Key, Value string
}
type Status struct {
Code int
Reason string
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}
for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
return err
}
_, err = io.Copy(w, body)
return err
}
type errWriter struct {
io.Writer
err error
}
func (e *errWriter) Write(buf []byte) (n int, err error) {
if e.err != nil {
return 0, e.err
}
n, e.err = e.Writer.Write(buf)
return n, nil
}
func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWriter{w, nil}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprintf(w, "\r\n")
io.Copy(ew, body)
return ew.err
}
何时该用panic
在 Golang 中panic
会导致程序间接退出,是一个致命的谬误。
倡议产生致命的程序谬误时才应用 panic,例如索引越界、不可复原的环境问题、栈溢出等等
小补充
errors.New()
返回的是errorString对象
的指针,其起因是避免字符串产生碰撞,如果产生碰撞,两个 error 对象会相等。
源码:
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
实际:error1
和error2
的text都是"error"
,然而二者并不相等
func TestErrString(t *testing.T) {
var error1 = errors.New("error")
var error2 = errors.New("error")
if error1 != error2 {
log.Println("error1 != error2")
}
}
---------------------代码运行后果--------------------------
=== RUN TestXXXX
2022/03/25 22:05:40 error1 != error2
创作不易,大家棘手点个赞~这对我很重要,蟹蟹各位啦~
参考文献
《Effective GO》
《Go程序设计语言》
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling
发表回复