2020年1月29日 星期三

從Java到Go系列 - Finally and defers

source: funnyZpc



在 Java 世界最習慣的模式就是 try-finally and try-catch-finally ,主要的用途有三種:
  • try-catch 用來處理例外
  • 到了1.7 版 更可以直接用來處理 AutoClosable 的 resource
  • finally 就是用來處理一堆雜七雜八最後處理的東西,包含
    • 經過 try-catch 處理後也出錯的錯誤處理
    • 或者是一些無法 AutoClosable 的 resource
 延伸閱讀:Java Try With Resources


但是到了 Go 的世界這兩件事被拆開來了,分別是 error handling and delay handling

Error handling


首先最讓 Java 開發者最不習慣的 error handling ,每個 function 都是透過回傳 error 來處理,於是就會有一堆處理 error 的 if 到處出現,舉例如下:


uid, err := ts.ValidateAuthToken(ctx, token)
if err != nil {
   ts.logger.Errorf("ValidateAuthToken err %+v.\n", err)
   return fmt.Errorf("authToken not found")
}


然後就這樣一路往上丟,丟到有人要處理為止..... (默默的告訴自己要習慣....)



Illustration created for “A Journey With Go”, made from the original Go Gopher
created by Renee French.

delay handling - defer


Defer 有點像 finally,會確保一個 function call 執行結束前執行,主要會被用來執行一些清理工作(比如說清理資源,關閉資源),不過 Go 的世界跟 Java 的世界最大的差異就是不能無腦亂用這些東西(咦?),所以在使用前必須先瞭解 defer 的特性:

  • defer 它是以類似 Stack 的方式執行,也就是在 function 執行結束後按照呼叫的順序反相執行,可以參考『Inlined defers in Go』,在這篇文章裡舉了一個很有實際的例子,讓我們可以很清楚 defer 的行為模式就是類似 Stack 的方,下面的 function 內執行兩個 defer function


0: func run() {
1:    defer foo()
2:    defer bar()
3:
4:    fmt.Println("hello")
5: }

而實際上執行起來的順序如下:


runtime.deferproc(foo) // generated for line 1
runtime.deferproc(bar) // generated for line 2
// Other code...
runtime.deferreturn(bar) // generated for line 5
runtime.deferreturn(foo) // generated for line 5

  • 即使 function 發生 嚴重錯誤也會被執行,這邊跟 java 的 finally 一樣
  • 支持 anonymous function 的呼叫,所以代表可以延遲執行某個 function

下面這些則是跟 Java 界差異比較大的特性(包含 pointer & lock)

  • 常用於清理資源、文件關閉、解鎖(unlock)以及記錄時間等善後操作(解鎖就是大雷區,這我也還在熟悉中...)
  • 如果 function 內某個變數作為 defer 時 anonymous function 的參數,則在定義 defer 就拷貝一份(類似 call by value ),否則則是引用某個變數的位址(call by referecne)

Panic handling 也就是程式 crash 後的處理

 懷念被 try catch 保護的日子~~(淚)

  • Go 的異常機制需要更多人為介入,不像 JVM 就算收到一堆 uncatch runtime exception 程式都還能運作,想要能睡好覺,就必須要研究一下透過  panic/recover 模式來處理錯誤。 Update:
  •  
     
  • panic 可以在任何地方引發,但 recover 只有在 defer 調用的函數中有效

這時候不得不說 Java 攻城獅真的被照顧得很好,就算程式邏輯有錯誤,都還能相對安穩的睡覺等到隔天再起來 Debug(並不會,還是得盡快處理...Orz..), 但是 Go 如果發生任何 panic 事件(代表無法被 error 處理),系統就直接死給你看,就算使用了 recover,如果 recover 的 defer function 沒寫好,系統還是無限循環死給你看....

最近遇到的案例就是使用 Bufflao 的 ORM ,因為我們在處理 Dao 的部分沒有寫好,所以系統一直發生 panic ,產生下面讓我這種新手很難理解的錯誤訊息:


reflect: call of reflect.Value.FieldByName on ptr Value


好死不死 Bufflao 內建有 recover 機制,一直把把這個錯誤攔截掉,並且透過 recover 重啟系統,所以系統還能順利運行,但是卻又沒有 release DB connection,所以之前使用過的資源就一直卡住,在 recover 幾次後系統就 deadlock 在那邊完全沒反應,害我們 debug 找超久,程式如下:


// PanicHandler recovers from panics gracefully and calls
// the error handling code for a 500 error.
func (a *App) PanicHandler(next Handler) Handler {
 return func(c Context) error {
  defer func() { //catch or finally
   r := recover()
   var err error
   if r != nil { //catch
    switch t := r.(type) {
    case error:
     err = t
    case string:
     err = fmt.Errorf(t)
    default:
     err = fmt.Errorf(fmt.Sprint(t))
    }
    err = err
    events.EmitError(events.ErrPanic, err,
     map[string]interface{}{
      "context": c,
      "app":     a,
     },
    )
    eh := a.ErrorHandlers.Get(http.StatusInternalServerError)
    eh(http.StatusInternalServerError, err, c)
   }
  }()
  return next(c)
 }
}


這算是一個標準範例,當 http server 攔截到 500 (internal server error)的 panic 錯誤,就會自動進行 revcover 的動作,但是我猜測在這邊沒有看到 release db connection,所以很快就會把 Go 內建的 connection pool 吃光光而造成 deadlock。

 也有看到許多相關的討論(對新手來說這邊雷很多...):
不過在來又牽涉到 Go DB connection 的部分,在這篇文章就先不深入討論....
(因為我也還在研究)





沒有留言 :