Learn Go (八) 錯誤處理和資源處理

資源管理, 通俗的講: 就是連接數(shù)據(jù)需要關(guān)閉, 操作文件的時候, 打開文件, 必須記得關(guān)閉; 對一個進(jìn)程上鎖的時候, 需要釋放鎖...; 錯誤表示程序中發(fā)生了異常情況。 假設(shè)我們正在嘗試打開一個文件,但該文件在文件系統(tǒng)中不存在劳殖。 這是一種異常情況桃犬,表示為錯誤。

defer 調(diào)用

資源管理, 通俗的講: 就是連接數(shù)據(jù)需要關(guān)閉, 操作文件的時候, 打開文件, 必須記得關(guān)閉; 對一個進(jìn)程上鎖的時候, 需要釋放鎖

  • 確保調(diào)用在函數(shù)結(jié)束時發(fā)生

    func tryDefer() {
        defer fmt.println(1)
        fmt.println(2)
    }
    // 輸出結(jié)果 2, 1 
    

    說明調(diào)用 defer 的時候是在函數(shù)結(jié)束時發(fā)生

    func tryDefer() {
        defer fmt.println(1)
        defer fmt.println(2)
        fmt.println(3)
    }
    // 輸出結(jié)果 3, 2, 1
    

    說明 defer 類似于一個棧, 先進(jìn)后出, 所以輸出 3, 2, 1

    func tryDefer() {
        defer fmt.println(1)
        defer fmt.println(2)
        fmt.println(3)
        panic("error occurred")
        fmt.println(4)
    }
    // 輸出結(jié)果 3, 2, 1 panic: error occurred
    

    說明 defer 的優(yōu)先級高于 panic, return. 即使代碼中有return, panic 也無法阻止 defer 語句的執(zhí)行

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

    說明 defer 先進(jìn)后出

  • Open/Close 使用 defer

    斐波那契數(shù)列寫入文件

    func writeFile(filename string) {
        // 創(chuàng)建文件
        file, err := os.Create(filename)
        if err != nil {
            panic(err)
        }
        // 關(guān)閉文件
        defer file.Close()
        
        writer := bufio.NewWriter(file)
        defer writer.Flush() // 導(dǎo)入
        // 調(diào)用 斐波那契數(shù)列函數(shù), 注意引入包的位置
        var f = fib.Fibonacci()
        
        for i := 0; i < 20; i++ {
            fmt.println(writer, f())
        }
    }
    // 主函數(shù)調(diào)用, 即可生成文件
    
    
    
  • Lock/Unlock 使用 defer

  • PrintHeader/PrintFooter 使用 defer

錯誤處理概念

錯誤表示程序中發(fā)生了異常情況。 假設(shè)我們正在嘗試打開一個文件,但該文件在文件系統(tǒng)中不存在。 這是一種異常情況维咸,表示為錯誤。

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() // 導(dǎo)入
   var f = fib.Fibonacci()
   for i := 0; i < 20; i++ {
      fmt.Fprintln(writer, f())
   }
}
  • os.O_CREATE 文件不存在則會創(chuàng)建
  • os.O_EXCL 與 O_CREATE 一起使用惠爽,文件不能存在癌蓖。

panic 異常會終止程序運(yùn)行, 且返回異常格式太生硬, 所以需要手動對異常處理下

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

error 是什么東西

  • 查看源碼
//錯誤內(nèi)置接口類型為常規(guī)接口 
// 表示錯誤條件,nil 值表示沒有錯誤疆股。
type error interface {
   Error() string
}
  • 上面的異常處理又可以優(yōu)化下
if err != nil {
    fmt.Println("Error:", err,Error())
}
  • 手動設(shè)置 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)
    }
}

服務(wù)器統(tǒng)一處理異常

實(shí)現(xiàn)統(tǒng)一的錯誤處理服務(wù)(一)

  • 新建目錄 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/"):] // 截取之后才取得真實(shí)路徑
            file, err := os.Open(path) // 打開文件
            if err != nil {
                // 拋出異常
                panic(err)
            }
            
            defer file.Close() // 關(guān)閉資源
            all, err : = ioutil.ReadAll(file)
            if err != nil {
                panic(err)
            }
            
            writer.Write(all)
        })
        
        // 起監(jiān)聽服務(wù)
        err := http.ListenAndServe("8888", nil)
        
        if err != nil {
            panic(err)
        }
    }
    
  • 封裝

    將上面的業(yè)務(wù)代碼處理提取出來, 放到單獨(dú)一文件里; 新建目錄-->新建文件 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)
    
    }
    

    遇到異常, 直接拋出異常, 在調(diào)用層單獨(dú)處理異常

  • 修改 web.go 文件

    
    package main
    
    import (
       "net/http"
       "os"
       
       "github.com/StudyGo/errorhandling/filelistingserver/filelisting"       
       "github.com/gpmgo/gopm/modules/log"
    )
    
    // 定義結(jié)構(gòu)體
    type appHandle func(writer http.ResponseWriter, request *http.Request) error
    
    
    // 異常處理, 包裝; 輸入一個函數(shù), 輸出一個函數(shù), 
    
    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)
       }
    
    }
    
    
    

實(shí)現(xiàn)統(tǒng)一的錯誤處理服務(wù)(二)

基于錯誤處理服務(wù)(一)修改; 這么一種情況, handle 文件的創(chuàng)建者將文件路徑寫成固定值/list/, 調(diào)用這個函數(shù)的人, 給的參數(shù)是/路徑. 實(shí)測: http://127.0.0.1:8888/abc 網(wǎng)頁直接崩潰, 并沒有拋出上面封裝的 error

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

   return func(writer http.ResponseWriter, request *http.Request) {
      // 核心 Start, 對 error 進(jìn)行封裝處理
      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 {

    // 對路徑進(jìn)行校對, 不正確直接返回
   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 文件進(jìn)行修改
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())
        
         // 改動在這里, 可以實(shí)現(xiàn)給用戶展示, 你想讓他看到的信息, 相對友好點(diǎn)的信息
         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)
   }

}

給用戶提示相對友好的信息, 并不是統(tǒng)一返回 Internet error

panic 和 recover

panic

  • 停止當(dāng)前函數(shù)執(zhí)行
  • 一直向上返回, 執(zhí)行每一層的 defer
  • 如果沒有遇見 recover, 程序退出

recover

  • 僅在 defer 調(diào)用中使用
  • 可以獲取 panic 的值
  • 如果無法處理, 可以重新 panic
示例

新建 recover.go

package main

import (
   "fmt"
)

func tryRecover()  {
   // 匿名函數(shù)
   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. 如數(shù)組越界
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末费坊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子旬痹,更是在濱河造成了極大的恐慌附井,老刑警劉巖讨越,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異永毅,居然都是意外死亡把跨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門沼死,熙熙樓的掌柜王于貴愁眉苦臉地迎上來着逐,“玉大人,你說我怎么就攤上這事意蛀∷时穑” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵县钥,是天一觀的道長秀姐。 經(jīng)常有香客問我,道長若贮,這世上最難降的妖魔是什么省有? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮谴麦,結(jié)果婚禮上蠢沿,老公的妹妹穿的比我還像新娘。我一直安慰自己匾效,他們只是感情好舷蟀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弧轧,像睡著了一般雪侥。 火紅的嫁衣襯著肌膚如雪碗殷。 梳的紋絲不亂的頭發(fā)上精绎,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機(jī)與錄音锌妻,去河邊找鬼代乃。 笑死,一個胖子當(dāng)著我的面吹牛仿粹,可吹牛的內(nèi)容都是我干的搁吓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吭历,長吁一口氣:“原來是場噩夢啊……” “哼堕仔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晌区,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤摩骨,失蹤者是張志新(化名)和其女友劉穎通贞,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恼五,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昌罩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了灾馒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茎用。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖睬罗,靈堂內(nèi)的尸體忽然破棺而出轨功,到底是詐尸還是另有隱情,我是刑警寧澤容达,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布夯辖,位于F島的核電站,受9級特大地震影響董饰,放射性物質(zhì)發(fā)生泄漏蒿褂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一卒暂、第九天 我趴在偏房一處隱蔽的房頂上張望啄栓。 院中可真熱鬧,春花似錦也祠、人聲如沸昙楚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堪旧。三九已至,卻和暖如春奖亚,著一層夾襖步出監(jiān)牢的瞬間淳梦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工昔字, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爆袍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓作郭,卻偏偏與公主長得像陨囊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子夹攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容