深入理解与使用go之错误处理--实现

深入理解与使用go之错误处理–实现

引子

错误管理是构建健壮和可观察的应用程序的一个基本方面,它应该与代码库的任何其他部分一样重要。在Go中,错误管理不像大多数编程语言那样依赖于传统的try/catch机制。相反,错误作为正常返回值返回。那么问题来了:

  • 程序什么时候发生崩溃(panic)
  • 通常处理错误的方式是怎样的
  • 我们应该忽略错误么
  • 在defer里出错应该怎么处理呢
  • 所有的panic错误都是可以捕获的么

我记得刚写程序的时候,感觉对go的错误很茫然,一股脑的忽略错误,像下面这样

// json 解码
_ = json.Unmarshal(data, &User)
// 打开文件
f, _ := os.Open("hello")

或者,我们避无可避,选择这样判断错误

if strings.Contains(err.Error(), "op error") {
		return ""
}
return data
  • 如果json解码的是配置文件,忽略错误会直接导致整个程序启动崩溃
  • 打开文件如果没有权限,下面的所有操作直接panic
  • 如果A开发错误内容是小写 “op error” 而B开发是大写 “OP ERROR”, 能判断么

带着上面的这些问题,我们来讨论讨论今天要说的错误处理

错误处理

错误的分类

我觉得开始处理之前,我们很有必要对错误进行一定的分类

  1. 崩溃型错误
    • 系统调用出现的崩溃 如堆栈溢出、数组越界、空指针引用等等
    • 程序限制型崩溃,如初始化过程中的配置读取、日志路径权限等等,出现错误整个程序就不该继续往下走
  2. 普通型错误
    • 错误我们需要处理,比如数据库连接异常 我们进行重试
  3. 正常型错误
    • 数据库因为查询为空返回的错误
    • 文件读取到末尾的EOF错误
    • 我们不感兴趣且对程序执行不会产生重大影响的错误

有了这三个分类,我们来一个一个看

崩溃型错误
  • 系统调用崩溃性错误

    这种错误一般是程序为了避免进一步的不确定行为和数据损坏,而提前退出

    如 数组越界

    a := []int{1, 2, 3}
    fmt.Println(a[3])
    

    运行会直接panic

    panic: runtime error: index out of range [3] with length 3
    
    goroutine 1 [running]:
    main.main()
            /data/www/hello/main.go:12 +0x1b
    exit status 2
    
  • 程序限制型

    net/http包的server.go 有段代码

    func checkWriteHeaderCode(code int) {
    	if code < 100 || code > 999 {
    		panic(fmt.Sprintf("invalid WriteHeader code %v", code))
    	}
    }
    

    原话是:

    // We used to send "HTTP/1.1 000 0" on the wire in responses but there's
    // no equivalent bogus thing we can realistically send in HTTP/2,
    // so we'll consistently panic instead and help people find their bugs
    // early. (We can't return an error from WriteHeader even if we wanted to.)
    我们过去常常在网络上发送“HTTP/1.1 000 0”作为响应,但现在。
    我们不能在HTTP/2中实际发送等同的虚假信息,
    因此,我们将持续恐慌,帮助人们及早发现他们的缺陷。(即使我们想从WriteHeader返回错误,也不能这样做。)
    

    database/sqlsql.go

    func Register(name string, driver driver.Driver) {
    	driversMu.Lock()
    	defer driversMu.Unlock()
    	if driver == nil {
    		panic("sql: Register driver is nil")
    	}
    	if _, dup := drivers[name]; dup {
    		panic("sql: Register called twice for driver " + name)
    	}
    	drivers[name] = driver
    }
    

    解释是:

    // Register makes a database driver available by the provided name.
    // If Register is called twice with the same name or if driver is nil,
    // it panics.
    注册使数据库驱动程序按提供的名称可用。
    如果使用相同的名称调用了Register两次,或者驱动程序为空,
    它就会出现恐慌。
    

    总结来说,我们的程序需要依赖项,如果程序员给了错误的依赖项,程序无法继续正常进行,就需要提前panic

    这几种情况都被视为程序员限制型错误,我们再看一个例子

    go 的 regexp 包提供了两个函数

    func Compile(expr string) (*Regexp, error) 
    func MustCompile(str string) *Regexp
    

    第一个会多返回一个错误,而第二个则在出错的时候直接panic, 而我们更倾向于使用第二个函数

    程序员在编写代码的时候,这种可预知的正则pattern模式错误是可以尽早的提前规避的

  • 处理

    如果我们在开发测试环境都未触发这些崩溃型错误,那到了生产环境部分case出现了,直接退出程序不再提供服务么

    当然不行,我们应当在适当的地方增加捕获机制,并记录日志告警来提醒程序员及时修复这些错误

    难道直接在入口函数里加 recovery

    func main() {
    	defer func() {
    		if err := recover(); err != nil {
    			log.Println(err)
    		}
    	}()
    	a := []int{1, 2, 3}
    	fmt.Println(a[3])	
    }
    

    正常被捕获了

    2024/03/20 09:29:35 runtime error: index out of range [3] with length 3
    

    但如果这样呢

    func printElm(index int) {
    	a := []int{1, 2, 3}
    	fmt.Println(a[index])
    }
    
    func main() {
    	defer func() {
    		if err := recover(); err != nil {
    			log.Println(err)
    		}
    	}()
    	go printElm(3)
    	time.Sleep(time.Second)
    }
    

    直接panic了

    panic: runtime error: index out of range [3] with length 3
    
    goroutine 6 [running]:
    main.printElm(0x0?)
            /data/www/hello/main.go:11 +0x9a
    created by main.main
            /data/www/hello/main.go:20 +0x45
    exit status 2
    

    这里就要提到一个重要的概念:

    recover只能在相同的 Go 协程中捕获 panic

    所以在子协程中,我们也应该做好错误捕获

    func printElm(index int) {
    	defer func() {
    		if err := recover(); err != nil {
    			log.Println(err)
    		}
    	}()
    	a := []int{1, 2, 3}
    	fmt.Println(a[index])
    }
    
    普通型错误
    • 调用三方包一般性错误 例如

      num, err := strconv.Atoi("123")
      if err != nil {
        log.Println(err)
        num = 0
      }
      fmt.Println(num)
      

      这种我们要做的就是接收错误

      • 可以兼容错误,兼容后继续处理
      • 不可兼容错误,向上返回错误,或打日志后直接返回
    • 自定义错误

      正常情况下,如果我们代码包可能出现多种错误的情况下,我们需要根据不同的错误进行不同的处理,我们就需要定义错误类型

      我们以 strconv 为例截取其中部分, 看看别人的包是怎么使用的错误

      1. 定义错误类型

        var ErrSyntax = errors.New("invalid syntax")
        
      2. 实现错误接口

        type NumError struct {
        	Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat, ParseComplex)
        	Num  string // the input
        	Err  error  // the reason the conversion failed (e.g. ErrRange, ErrSyntax, etc.)
        }
        
        func (e *NumError) Error() string {
        	return "strconv." + e.Func + ": " + "parsing " + Quote(e.Num) + ": " + e.Err.Error()
        }
        
      3. 包装错误方法

        func syntaxError(fn, str string) *NumError {
        	return &NumError{fn, cloneString(str), ErrSyntax}
        }
        
      4. 抛错

        func ParseInt(s string, base int, bitSize int) (i int64, err error) {
        	const fnParseInt = "ParseInt"
        
        	if s == "" {
        		return 0, syntaxError(fnParseInt, s)
        	}
        	// code here 
        }
        
      5. 使用判断

        假设我们要根据语法错误做特殊处理

        num, err := strconv.ParseInt("", 10, 64)
        if errors.Is(err, strconv.ErrSyntax) {
            fmt.Println(err)
            // 特殊处理
        }
        fmt.Println(num)
        

      从上面的例子不难看出,完整的错误处理分了大概5步

      我们日常使用中,如果对出错的参数和上下文不关心,错误处理可能就三步(第1,4,5)第4 步我们可以继续简化成

      if s == "" {
      		return 0, ErrSyntax
      }
      
    • 包装错误

      假设我们想使用别人的错误,但是又希望添加上我们的上下文

      • 在go1.13之后,可以使用 %w 参数进行包装

      • 然后使用 errors.As 进行包装错误的判断

        这里有个问题需要考虑下,As 方法的第二个参数在实现 Error 接口时,如果传递的是 *, 我们这里就要使用 **

        否则,使用 * 即可,参考 问题

      我们包装了一个 panic 错误

      type PanicError struct {
      	Err error
      }
      
      func (p PanicError) Error() string {
      	return fmt.Sprintf("panic error: %v", p.Err)
      }
      

      错误使用处,我们添加指明了函数上下文

      func printElm(index int) (err error) {
      	defer func() {
      		if e := recover(); e != nil {
      			err = fmt.Errorf("printElm index err %w ", customerror.PanicError{
      				Err: fmt.Errorf("org error is %v", e),
      			})
      		}
      	}()
      	a := []int{1, 2, 3}
      	fmt.Println(a[index])
      	return
      }
      

      然后使用时

      err := printElm(3)
      if errors.As(err, new(customerror.PanicError)) {
      		fmt.Println("panic error")
      }
      

      这里需要注意的是,如果上面实现 我们使用了

      func (p *PanicError) Error() string
      

      这里我们需要替换成

      panicError := new(customerror.PanicError)
      if errors.As(err, &panicError) 
      
    • defer 错误

      有一种错误产生可能是在 defer 语句里的,假设这样一个场景, 我们常常从数据库读取信息

      最后需要关闭连接

      func getUserInfo(db *sql.DB, userId int) (
          Info.User, error) {
          rows, err := db.Query(query, userId)
          if err != nil {
              return nil, err
          }
          defer rows.Close()
          // other code here
      }
      
      1. 如果 rows.Close()成功了,好了没问题

      2. 如果 rows.Close()失败了呢,

        有人说把错误处理 改成这样

        defer func() {
            err = rows.Close()
        }()
        
      3. 那么问题来了,当 db.Query 出现错误后, 这里有没有错误是不是都会覆盖

        怎么办呢

        • 如果程序本身 错误不为空
          • 关闭错误有值 记录日志,返回程序本身错误
          • 关闭错误无值 ,返回程序本身错误
        • 如果程序本身无错误,直接将关闭错误赋值给返回值
        defer func() {
            closeError := rows.Close() 
            if err != nil {
                if closeError != nil {
                    log.Printf("failed to close db rows: %v", err)
                }
                return
            }
            err = closeError
        }()
        
    正常型错误

    正常型错误有两种

    1. 正常的程序逻辑状态,甚至都不应该称之为错误

      比如下面的数据库返回条数为空

      if err != nil {
          if err == sql.ErrNoRows {
              // 正常的数据为空
          } else {
              // ...
          }
      }
      
    2. 错误可以出现,但我们不感兴趣

      假设我们有个通知处理,是额外的一部分,我们对于是否出现错误,不关心, 可以如下处理

      _ = notify()
      

      如果后面其他开发看到,这个就会很困惑,最好的做法是, 我们加个注释,记录个日志

      // notify the other people, don't care about online, we also notify offline
      err := notify()
      log.Println(err)
      

错误处理就是这些,如果有更好的做法,欢迎评论区讨论

  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值