Learn Go (八) 错误处理和资源处理

资源管理, 通俗的讲: 就是连接数据需要关闭, 操作文件的时候, 打开文件, 必须记得关闭; 对一个进程上锁的时候, 需要释放锁...; 错误表示程序中发生了异常情况。 假设我们正在尝试打开一个文件,但该文件在文件系统中不存在。 这是一种异常情况,表示为错误。

defer 调用

资源管理, 通俗的讲: 就是连接数据需要关闭, 操作文件的时候, 打开文件, 必须记得关闭; 对一个进程上锁的时候, 需要释放锁

  • 确保调用在函数结束时发生

    func tryDefer() {
        defer fmt.println(1)
        fmt.println(2)
    }
    // 输出结果 2, 1 
    

    说明调用 defer 的时候是在函数结束时发生

    func tryDefer() {
        defer fmt.println(1)
        defer fmt.println(2)
        fmt.println(3)
    }
    // 输出结果 3, 2, 1
    

    说明 defer 类似于一个栈, 先进后出, 所以输出 3, 2, 1

    func tryDefer() {
        defer fmt.println(1)
        defer fmt.println(2)
        fmt.println(3)
        panic("error occurred")
        fmt.println(4)
    }
    // 输出结果 3, 2, 1 panic: error occurred
    

    说明 defer 的优先级高于 panic, return. 即使代码中有return, panic 也无法阻止 defer 语句的执行

    func tryDefer2()  {
       for i := 0; i< 100; i++ {
          defer fmt.Println(i)
          if i == 30 {
             panic("printed too many")
          }
       }
    }
    // 输出结果 30, 29 ...0 
    

    说明 defer 先进后出

  • Open/Close 使用 defer

    斐波那契数列写入文件

    func writeFile(filename string) {
        // 创建文件
        file, err := os.Create(filename)
        if err != nil {
            panic(err)
        }
        // 关闭文件
        defer file.Close()
        
        writer := bufio.NewWriter(file)
        defer writer.Flush() // 导入
        // 调用 斐波那契数列函数, 注意引入包的位置
        var f = fib.Fibonacci()
        
        for i := 0; i < 20; i++ {
            fmt.println(writer, f())
        }
    }
    // 主函数调用, 即可生成文件
    
    
    
  • Lock/Unlock 使用 defer

  • PrintHeader/PrintFooter 使用 defer

错误处理概念

错误表示程序中发生了异常情况。 假设我们正在尝试打开一个文件,但该文件在文件系统中不存在。 这是一种异常情况,表示为错误。

func writeFile(filename string)  { 
   file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) 
   if err != nil { 
       panic(err)
   }
   defer file.Close()

   writer := bufio.NewWriter(file)
   defer writer.Flush() // 导入
   var f = fib.Fibonacci()
   for i := 0; i < 20; i++ {
      fmt.Fprintln(writer, f())
   }
}
  • os.O_CREATE 文件不存在则会创建
  • os.O_EXCL 与 O_CREATE 一起使用,文件不能存在。

panic 异常会终止程序运行, 且返回异常格式太生硬, 所以需要手动对异常处理下

if err != nil {
    fmt.println("file already exists")
    // 异常, 程序终止
    return
}

error 是什么东西

  • 查看源码
//错误内置接口类型为常规接口 
// 表示错误条件,nil 值表示没有错误。
type error interface {
   Error() string
}
  • 上面的异常处理又可以优化下
if err != nil {
    fmt.Println("Error:", err,Error())
}
  • 手动设置 error
err = errors.New("this is error handling")

if err != nil {
    if pathError, ok := err.(*os.PathError); !ok {
        panic(err)
    } else {
        fmt.Printf("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err)
    }
}

服务器统一处理异常

实现统一的错误处理服务(一)

  • 新建目录 filelistingserver, 新建 web.go

    package main
    
    import (
        "io/ioutil"
        "net/http"
        "os"
    )
    
    func main() {
        http.HandleFunc("/list/", func(writer http.ResponseWriter, request *http.Request){
            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)
        }
    }
    
  • 封装

    将上面的业务代码处理提取出来, 放到单独一文件里; 新建目录-->新建文件 handle.go

    package filelisting
    
    import (
       "io/ioutil"
       "net/http"
       "os"
    )
    
    func HandleFileList (writer http.ResponseWriter, request *http.Request) error {
       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)
    
    }
    

    遇到异常, 直接抛出异常, 在调用层单独处理异常

  • 修改 web.go 文件

    
    package main
    
    import (
       "net/http"
       "os"
       
       "github.com/StudyGo/errorhandling/filelistingserver/filelisting"       
       "github.com/gpmgo/gopm/modules/log"
    )
    
    // 定义结构体
    type appHandle func(writer http.ResponseWriter, request *http.Request) error
    
    
    // 异常处理, 包装; 输入一个函数, 输出一个函数, 
    
    func errWrapper(handler appHandle) func(http.ResponseWriter, *http.Request)  {
    
    return func(writer http.ResponseWriter, request *http.Request) {
          err := handler(writer, request)
          if err != nil {
             log.Warn("Error handling request: %s", err.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)
          }
       }
    }
    
    
    
    func main() {
       http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList))
    
       err := http.ListenAndServe(":8888", nil)
       if err != nil {
          panic(err)
       }
    
    }
    
    
    

实现统一的错误处理服务(二)

基于错误处理服务(一)修改; 这么一种情况, handle 文件的创建者将文件路径写成固定值/list/, 调用这个函数的人, 给的参数是/路径. 实测: http://127.0.0.1:8888/abc 网页直接崩溃, 并没有抛出上面封装的 error

  • 修改 web.go 文件
func errWrapper(handler appHandle) func(http.ResponseWriter, *http.Request)  {

   return func(writer http.ResponseWriter, request *http.Request) {
      // 核心 Start, 对 error 进行封装处理
      defer func() {
         r := recover()
         log.Error("panic: %v", r)
         http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

      }()
        // 核心End
      err := handler(writer, request)

      if err != nil {
         log.Warn("Error handling request: %s", err.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)
      }

   }
   
}
  • handle.go 文件再次修改
package filelisting

import (
   "errors"
   "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 {

    // 对路径进行校对, 不正确直接返回
   if strings.Index(request.URL.Path, prefix) != 0{
      return errors.New("path must start with "+ 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

}
  • web.go 文件进行修改
package main

import (
   "net/http"
   "os"

   "github.com/StudyGo/errorhandling/filelistingserver/filelisting"
   "github.com/gpmgo/gopm/modules/log"
)

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


func errWrapper(handler appHandle) func(http.ResponseWriter, *http.Request)  {

   return func(writer http.ResponseWriter, request *http.Request) {

      defer func() {

         if r := recover(); r != nil {
            log.Error("panic: %v", r)
            http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
         }
      }()

      err := handler(writer, request)

      if err != nil {
         log.Warn("Error handling request: %s", err.Error())
        
         // 改动在这里, 可以实现给用户展示, 你想让他看到的信息, 相对友好点的信息
         if userErr, ok := err.(userError); ok {
            http.Error(writer, userErr.Message(), http.StatusBadRequest)
            return
         }
         
         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)
   }

}

给用户提示相对友好的信息, 并不是统一返回 Internet error

panic 和 recover

panic

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

recover

  • 仅在 defer 调用中使用
  • 可以获取 panic 的值
  • 如果无法处理, 可以重新 panic
示例

新建 recover.go

package main

import (
   "fmt"
)

func tryRecover()  {
   // 匿名函数
   defer func() {
      r := recover()
      if err, ok := r.(error); ok {
         fmt.Println("Error occurred: ", err)
      } else {
         panic(fmt.Sprintf("I don't know what to do: %v", r))
      }

   }()
   //panic(errors.New("this is an error recover"))

   //b := 0
   //a := 5 / b
   //fmt.Println(a)

   panic(123445)

}

func main() {
   tryRecover()

}

error vs panic

  • 意料之中的: 使用 error
  • 意料之外的: 使用 panic. 如数组越界

你可能感兴趣的:(Learn Go (八) 错误处理和资源处理)