【青训营】错误处理

117 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第四篇笔记

今天写的内容与第三节课——高质量编程有关,文档中的总结如下:

  • panic 用于真正异常的情况

  • error 尽可能提供简明的上下文信息,方便定位问题

  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

这个总结的正确性在我的工程实践中得到了证实。毋庸置疑,panic应该慎用。

除非是数据库初始化,或整个服务端初始化都没有成功这种错误,否则使用panic的话用户体验会很差。而且前面说的这些错误都是无可挽回的错误,再运行接下来的代码意义不大,所以使用panic。

当然,如果你暂时不想处理掉某个error,只是先用panic应付一下也是可以的。

重点是error的使用技巧。

error中需要使用errorWrapper来包装自己项目中的错误。至少我不希望把握电脑上的内部错误暴露给客户,那么我就需要将给内部程序员看的报错信息和给客户看的错误信息区分开。

下面一段程序可以将服务器内部的文件打印到网页上,其中的错误处理就非常值得用心。

首先是web.go

package main

import (
   "log"
   "net/http"
   _ "net/http/pprof"
   "os"

   "imooc.com/ccmouse/learngo/lang/errhandling/filelistingserver/filelisting"
)

type appHandler func(writer http.ResponseWriter,
   request *http.Request) error

func errWrapper(
   handler appHandler) func(
   http.ResponseWriter, *http.Request) {
   return func(writer http.ResponseWriter,
      request *http.Request) {
      // panic
      defer func() {
         if r := recover(); r != nil {
            log.Printf("Panic: %v", r)
            http.Error(writer,
               http.StatusText(http.StatusInternalServerError),
               http.StatusInternalServerError)
         }
      }()

      err := handler(writer, request)

      if err != nil {
         log.Printf("Error occurred "+
            "handling request: %s",
            err.Error())

         // user error
         if userErr, ok := err.(userError); ok {
            http.Error(writer,
               userErr.Message(),
               http.StatusBadRequest)
            return
         }

         // system error
         code := http.StatusOK
         switch {
         case os.IsNotExist(err):
            code = http.StatusNotFound
         case os.IsPermission(err):
            code = http.StatusForbidden
         default:
            code = http.StatusInternalServerError
         }
         http.Error(writer,
            http.StatusText(code), code)
      }
   }
}

type userError interface {
   error
   Message() string
}

func main() {
   http.HandleFunc("/",
      errWrapper(filelisting.HandleFileList))

   err := http.ListenAndServe(":8888", nil)
   if err != nil {
      panic(err)
   }
}

其次是handler.go

package filelisting

import (
   "fmt"
   "io/ioutil"
   "net/http"
   "os"
   "strings"
)

const prefix = "/list/"

type userError string

func (e userError) Error() string {
   return e.Message()
}

func (e userError) Message() string {
   return string(e)
}

func HandleFileList(writer http.ResponseWriter,
   request *http.Request) error {
   fmt.Println()
   if strings.Index(
      request.URL.Path, prefix) != 0 {
      return userError(
         fmt.Sprintf("path %s must start "+
            "with %s",
            request.URL.Path, prefix))
   }
   path := request.URL.Path[len(prefix):]
   file, err := os.Open(path)
   if err != nil {
      return err
   }
   defer file.Close()

   all, err := ioutil.ReadAll(file)
   if err != nil {
      return err
   }

   writer.Write(all)
   return nil
}

用户可以在/list/后面查看fib.txt,访问localhost:8888/list/fib.txt即可,写在handler.go中.

然而主程序web.go中却认为/后面的都需要错误处理,但是如果访问localhost:8888/abc这样的网址就会出现“该网页无法正常运作”,很难看。

这里在web.go中使用defer中的自己写的recover函数可以解决这个问题。

今天的错误处理就分享到这里,有什么疑问就在评论区提出来吧。