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. 如数组越界
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容