Go语言 错误处理以及资源管理

1,179 阅读5分钟

defer 调用

通常用于资源管理,defer可以确保在函数结束时调用

func tryDefer(){
   defer  fmt.Println(1)
   defer  fmt.Println(2)
   fmt.Println(3)
}

func main() {
   tryDefer()
}

image.png

看下 打印顺序 可以看出来: defer是可以确保 程序一定会执行的,哪怕中间return 或者panic 都不怕 defer本身的执行顺序 是 先进后出

来看下defer的具体应用

func writeFile(filename string) {
   // 创建一个文件
   file, err := os.Create(filename)
   if err != nil {
      panic(err)
   }
   // 既然open了一个文件 那一定要记得关闭 
   defer file.Close()
   // 往缓存里面写入数据
   writer := bufio.NewWriter(file)
   // 既然使用了缓存 那么一定要记住要flush
   defer writer.Flush()
   // 往缓存中写入对应的数据
   fmt.Fprintln(writer, "hello golang wuyue111")
}

还是挺好用的,和java相比 这个defer的特性可以确保我们不会出现忘记关闭资源的bug

另外要注意的是参数是在defer语句是进行计算

举例:

func tryDefer() {
   for i := 0; i < 10; i++ {
      defer fmt.Println(i)
      if i==5 {
         panic("too many times")
      }
   }
}

看下执行结果:

image.png

错误处理

之前的程序 我们可以看出来,panic是直接让程序终止的,就好像类似于java里面 丢了一个没人能处理的异常 直接导致程序运行停止了。正常情况下 我们应该避免这种情况。所以 才需要正确的错误处理方式

比如:

func writeFile2(filename string) {
   // 创建一个文件 如果之前已经存在这个文件 那么直接报错
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println("file aleady exists")
   }
   defer file.Close()
}

这样的提示方式是比较友好的,我们当然可以看看这个error到底是个啥

image.png

嗯 就是一个接口,只不过这个接口是一个Error方法

func writeFile2(filename string) {
   // 创建一个文件 如果之前已经存在这个文件 那么直接报错
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println(err.Error())
   }
   defer file.Close()
}

image.png

那到这里呢,我们还可以继续看看这个openfile函数

image.png

意思就是 如果这里发生了错误 那一定是一个patherror的指针类型


func writeFile2(filename string) {
   // 创建一个文件 如果之前已经存在这个文件 那么直接报错
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
   if err != nil {
      pathError, ok := err.(*os.PathError)
      if !ok {
         panic(err)
      } else {
         fmt.Println("op:", pathError.Op, "  path:", pathError.Path, "  err:", pathError.Err)
      }
   }
   defer file.Close()
}

那我们继续来看一下:

image.png

另外就是 一般情况下,我们错误处理的时候 还会加个return语句 确保不会在后面的程序中 走入错误的逻辑

func writeFile2(filename string) {
   // 创建一个文件 如果之前已经存在这个文件 那么直接报错
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
   if err != nil {
      pathError, ok := err.(*os.PathError)
      if !ok {
         panic(err)
      } else {
         fmt.Println("op:", pathError.Op, "  path:", pathError.Path, "  err:", pathError.Err)
      }
      // 错误处理的时候别忘记return
      return
   }
   defer file.Close()
}

当然我们也可以自己顺手创建一个error

func writeFile2(filename string) {
   // 创建一个文件 如果之前已经存在这个文件 那么直接报错
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
   // 当然也可以自己创建一个error
   if filename == "test.txt" {
      err = errors.New("file name can not be test.txt")
   }
   if err != nil {
      pathError, ok := err.(*os.PathError)
      if !ok {
         panic(err)
      } else {
         fmt.Println("op:", pathError.Op, "  path:", pathError.Path, "  err:", pathError.Err)
      }
      // 错误处理的时候别忘记return
      return
   }
   defer file.Close()
}

统一处理错误逻辑

来写个简单的web程序吧

你看我这个下面 有这么多go文件 我希望在浏览器 输入对应的路径的时候 可以看到对应go文件的内容 image.png

程序很简单:

func main() {
   http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request) {
      //看下传来的path
      path := request.URL.Path[len("/list/"):]
      //找到对应的文件
      file, err := os.Open(path)
      if err != nil {
         panic(err)
      }
      defer file.Close()
      // 读取文件的内容
      all, err := ioutil.ReadAll(file)
      if err != nil {
         panic(err)

      }
      //将文件的内容 输出
      writer.Write(all)
   })

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

也可以看到效果:

image.png

那如果输入一个错误的路径会得到啥呢?

image.png

显然这是不正确的,看下我们的server端:

image.png

虽然http库对server的panic做了保护 不会让服务器直接挂掉,但是这里访问的人在浏览器上的体验就极其糟糕了

改进一下:

func main() {
   http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request) {
      //看下传来的path
      path := request.URL.Path[len("/list/"):]
      //找到对应的文件
      file, err := os.Open(path)
      if err != nil {
         //改进一下 提供一些正常的错误信息
         http.Error(writer, err.Error(), http.StatusInternalServerError)
         return
      }
      defer file.Close()
      // 读取文件的内容
      all, err := ioutil.ReadAll(file)
      if err != nil {
         panic(err)

      }
      //将文件的内容 输出
      writer.Write(all)
   })

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

这样看起来显然正常的多

image.png

除此之外 这里的错误处理 显然要改的地方太多,正常情况下 我们应该写一个函数 统一处理

首先我们可以将handle函数 的所有error 都返回出来

func handlerFileListing(writer http.ResponseWriter, request *http.Request) error {
   //看下传来的path
   path := request.URL.Path[len("/list/"):]
   //找到对应的文件
   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)
   // 没有错误 显然就返回nil了
   return nil
}

为了方便呢,我们给这个函数取一个别名:

// 定义一个函数 返回值为error的 appHandler
type appHandler func(writer http.ResponseWriter, request *http.Request) error

最终呢我们处理一下这个函数的error

// 这个函数的返回值 是一个函数
func errWrapper(handle appHandler) func(http.ResponseWriter, *http.Request) {
   return func(writer http.ResponseWriter, request *http.Request) {
      err := handle(writer, request)
      if err != nil {
         logger := zap.NewExample()
         code := http.StatusOK
         switch {
         case os.IsNotExist(err):
            logger.Warn("handle file not exist")
            code = http.StatusNotFound
            http.Error(writer, http.StatusText(code), code)
         default:
            code = http.StatusInternalServerError
         }
      }
   }
}

最终呢 就换一种形式去调用她:

func main() {
   http.HandleFunc("/list/", errWrapper(handlerFileListing))

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

panic

前面我们见到了数次使用panic的地方,总结一下panic关键字的作用:

停止当前函数的执行 panic会一直向上返回 执行每层的defer 如果没有遇见recover 程序就退出了。

与之对应的还有recover这个关键函数

recover只能在defer调用中使用 并且还可以获取panic的值,无法处理panic的时候可以重新pani

func tryRecover() {
   defer func() {
      r := recover()
      if err, ok := r.(error); ok {
         fmt.Println("find error",err)
      }else{
         panic(r)
      }
   }()

   //panic(errors.New("new error"))
   panic("just string")

}

可以看下具体两种不同的panic的时候 程序的运行状态

image.png

image.png

除此之外呢 这里panic是有可能为nil的,所以我们必须进行判空


func tryRecover() {
   defer func() {
      r := recover()
      if r == nil {
         fmt.Println("panic is nil")
         return
      }
      if err, ok := r.(error); ok {
         fmt.Println("find error", err)
      } else {
         panic(r)
      }
   }()

   //panic(errors.New("new error"))
   //panic("just string")

}

最后总结一下,自己在设计程序的时候:

意料之中的错误 用error,意料之外的用panic