利用 Golang 中的 Recover 處理錯誤
Golang 中的 recover 是一個鮮為人知但非常有趣和強大的功能。讓我們看看它是如何工作的,以及在 Outreach.io 中如何利用它來處理 Kubernetes 中的錯誤。
Panic/Defer/Recover 基本上是 Golang 中對于其他編程語言中 throw/finally/catch 概念的替代品。它們有一些共同之處,但在一些重要細節上有所不同。
Defer
要充分理解 recover,我們首先需要談論 defer 語句。defer 關鍵字前置于函數調用之前,使得該調用在當前函數返回之前執行。當我們在一個函數中使用多個 defer 語句時,它們按照后進先出的順序執行,這使得創建清理邏輯變得非常容易,如下例所示:
package main
import (
"context"
"database/sql"
"fmt"
)
func readRecords(ctx context.Context) error {
db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory")
if err != nil {
return err
}
defer db.Close() // 這個函數調用將在 readRecords 函數返回時第三個執行
conn, err := db.Conn(ctx)
if err != nil {
return err
}
defer conn.Close() // 這個函數調用將在第二個執行
rows, err := conn.QueryContext(ctx, "SELECT id FROM users")
if err != nil {
return err
}
defer rows.Close() // 這個函數調用將在第一個執行
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return err
}
fmt.Println("ID:", id)
}
return nil
}
func main() {
readRecords(context.Background())
}
Panic
我們需要談論的第二個主題是 panic,它是一個導致當前 goroutine 進入 panic 模式的函數。當前函數中的正常執行流程被停止,僅執行 defer 語句,然后對調用者函數執行相同的操作,因此一直冒泡到堆棧的頂部(main 函數),然后使程序崩潰。panic 可以直接調用(傳遞一個值作為參數),也可以由運行時錯誤引起。例如,由于空指針解引用:
package main
import "fmt"
func main() {
var x *string
fmt.Println(*x)
}
// panic: runtime error: invalid memory address or nil pointer dereference
Recover
recover 是一個內建函數,它使我們有可能在發生 panic 時重新獲得控制。它僅在被調用的延遲函數中產生效果。在延遲函數之外調用時,它總是返回 nil。如果我們處于 panic 模式,調用 recover 會返回傳遞給 panic 函數的值。基本示例:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\\n", r)
}
}()
panic("spam, egg, sausage, and spam")
}
// Recovered: spam, egg, sausage, and spam
我們可以以同樣的方式從運行時錯誤中恢復:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\\n", r)
}
}()
var x *string
fmt.Println(*x)
}
// Recovered: runtime error: invalid memory address or nil pointer dereference
在這種情況下,recover 返回的值的類型是錯誤(更準確地說是 runtime.errorString)。
有一個限制:我們不能直接從 recover 塊中返回值,因為在 recover 塊中的 return 語句僅從延遲函數中返回,而不是從周圍的函數中返回:
package main
import "fmt"
func foo() int {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\\n", r)
return 1 // "too many return values" 因為我們僅從匿名函數返回
}
}()
panic("spam, egg, sausage, and spam")
}
func main() {
x := foo()
fmt.Println(x)
}
如果我們想要更改函數返回的值,我們需要使用命名返回值:
package main
import "fmt"
func foo() (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\\n", r)
ret = 1
}
}()
panic("spam, egg, sausage, and spam")
}
func main() {
x := foo()
fmt.Println("value:", x)
}
// Recovered: spam, egg, sausage, and spam
// value: 1
一個更實際的例子,將 panic 轉換為普通錯誤的轉換可能如下所示:
package main
import (
"fmt"
"github.com/google/uuid"
)
// processInput 嘗試將輸入字符串轉換為 uuid.UUID
// 它將 panic 轉換為錯誤
func processInput(input string) (u uuid.UUID, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// 一些可能引發 panic 的邏輯(也可以是第三方邏輯),例如:
u = uuid.MustParse(input)
return u, nil
}
func main() {
u, err := processInput("xxx")
if err != nil {
fmt.Println(err)
}
fmt.Println(u)
}
// panic: uuid: Parse(xxx): invalid UUID length: 3
// 00000000-0000-0000-0000-000000000000
現在讓我們嘗試一些稍微
復雜的東西。假設我們在 Kubernetes 中運行,并且我們想要編寫一個通用的 recover 函數,處理所有未捕獲的 panic 和運行時錯誤,并收集它們的堆棧跟蹤,以便我們可以以結構化的方式記錄它們(例如,以 JSON 格式)。
package main
import (
"fmt"
"log"
"os"
"github.com/pkg/errors"
)
func foo() string {
var s *string
return *s
}
func handlePanic(r interface{}) error {
var errWithStack error
if err, ok := r.(error); ok {
errWithStack = errors.WithStack(err)
} else {
errWithStack = errors.Errorf("%+v", r)
}
return errWithStack
}
func main() {
logger := log.New(os.Stdout, "", 0)
defer func() {
if r := recover(); r != nil {
err := handlePanic(r)
logger.Println(
"panic occurred",
"msg", err.Error(),
"stack", fmt.Sprintf("%+v", err),
)
}
}()
fmt.Println(foo())
}
// 輸出:
// panic occurred msg: runtime error: invalid memory address or nil pointer dereference
// stack: runtime error: invalid memory address or nil pointer dereference
// main.handlePanic
// /tmp/sandbox239055659/prog.go:19
// main.main.func1...
以上就是今天的內容!recover 函數并不是 Golang 開發者的日常必備工具,但正如你所看到的,它在某些情況下非常有用。