如何封裝安全的go
在業務程式碼開發過程中,我們會有很大概率使用go語言的goroutine來開啟一個新的goroutine執行另外一段業務,或者開啟多個goroutine來並行執行多個業務邏輯。所以我為hade框架增加了兩個方法goroutine.SafeGo 和 goroutine.SafeGoAndWait。
封裝
SafeGo
SafeGo 這個函式,提供了一種goroutine安全的函式呼叫方式。主要適用於業務中需要進行開啟非同步goroutine業務邏輯呼叫的場景。
// SafeGo 進行安全的goroutine呼叫
// 第一個引數是context介面,如果還實現了Container介面,且繫結了日誌服務,則使用日誌服務
// 第二個引數是匿名函式handler, 進行最終的業務邏輯
// SafeGo 函式並不會返回error,panic都會進入hade的日誌服務
func SafeGo(ctx context.Context, handler func())
呼叫方式參照如下的單元測試用例:
func TestSafeGo(t *testing.T) {
container := tests.InitBaseContainer()
container.Bind(&log.HadeTestingLogProvider{})
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
goroutine.SafeGo(ctx, func() {
time.Sleep(1 * time.Second)
return
})
t.Log("safe go main start")
time.Sleep(2 * time.Second)
t.Log("safe go main end")
goroutine.SafeGo(ctx, func() {
time.Sleep(1 * time.Second)
panic("safe go test panic")
})
t.Log("safe go2 main start")
time.Sleep(2 * time.Second)
t.Log("safe go2 main end")
}
SafeGoAndWait
SafeGoAndWait 這個函式,提供安全的多併發呼叫方式。該函式等待所有函式都結束後才返回。
// SafeGoAndWait 進行併發安全並行呼叫
// 第一個引數是context介面,如果還實現了Container介面,且繫結了日誌服務,則使用日誌服務
// 第二個引數是匿名函式handlers陣列, 進行最終的業務邏輯
// 返回handlers中任何一個錯誤(如果handlers中有業務邏輯返回錯誤)
func SafeGoAndWait(ctx context.Context, handlers ...func() error) error
呼叫方式參照如下的單元測試用例:
func TestSafeGoAndWait(t *testing.T) {
container := tests.InitBaseContainer()
container.Bind(&log.HadeTestingLogProvider{})
errStr := "safe go test error"
t.Log("safe go and wait start", time.Now().String())
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
err := goroutine.SafeGoAndWait(ctx, func() error {
time.Sleep(1 * time.Second)
return errors.New(errStr)
}, func() error {
time.Sleep(2 * time.Second)
return nil
}, func() error {
time.Sleep(3 * time.Second)
return nil
})
t.Log("safe go and wait end", time.Now().String())
if err == nil {
t.Error("err not be nil")
} else if err.Error() != errStr {
t.Error("err content not same")
}
// panic error
err = goroutine.SafeGoAndWait(ctx, func() error {
time.Sleep(1 * time.Second)
return errors.New(errStr)
}, func() error {
time.Sleep(2 * time.Second)
panic("test2")
}, func() error {
time.Sleep(3 * time.Second)
return nil
})
if err == nil {
t.Error("err not be nil")
} else if err.Error() != errStr {
t.Error("err content not same")
}
}
實現說明
實現方面,有幾個難點記錄下。
首先是介面設計方面
可以看到handler函式在兩個介面中是不一樣的。在SafeGo介面中,handler定義為func()
而在SafeGoAndWait中,定義為func() error
兩者的區別就在於SafeGo這個介面是沒有能力處理error的,因為它go出去一個goroutine就直接進行接下來的操作了。而SafeGoAndWait是必須等到所有的請求結束,所以它是有能力接收到error的。
所以SafeGo的handler沒有必要設定error返回值,而SafeGoAndWait是可以設定error的。
其次是日誌相容hade
如果出現了panic,如何將panic的日誌列印出來。
整個框架我們並不希望有任何的全域性變數,包括全域性的Log,所以我這裡做了一個相容邏輯。
如果只是傳遞一個context,我們就使用官方的log包進行列印。
如果傳遞的是一個即實現了context,又實現了container介面的結構,我們就從container中獲取日誌服務,來進行日誌列印。這樣框架的所有日誌就能統一在日誌列印裡面。
if logger != nil {
logger.Error(ctx, "safe go handler panic", map[string]interface{}{
"stack": string(buf),
"err": e,
})
} else {
log.Printf("panic\t%v\t%s", e, buf)
}
由於我們修改了gin的context,讓它支援了我們的container容器結構,所以我們可以直接將gin.Context傳遞進來。具體使用起來就像這樣了:
// DemoGoroutine goroutine 的使用示例
func (api *DemoApi) DemoGoroutine(c *gin.Context) {
logger := c.MustMakeLog()
logger.Info(c, "request start", nil)
// 初始化一個orm.DB
gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
db, err := gormService.GetDB(orm.WithConfigPath("database.default"))
if err != nil {
logger.Error(c, err.Error(), nil)
c.AbortWithError(50001, err)
return
}
db.WithContext(c)
err = goroutine.SafeGoAndWait(c, func() error {
// 查詢一條資料
queryUser := &User{ID: 1}
err = db.First(queryUser).Error
logger.Info(c, "query user1", map[string]interface{}{
"err": err,
"name": queryUser.Name,
})
return err
}, func() error {
// 查詢一條資料
queryUser := &User{ID: 2}
err = db.First(queryUser).Error
logger.Info(c, "query user2", map[string]interface{}{
"err": err,
"name": queryUser.Name,
})
return err
})
if err != nil {
c.AbortWithError(50001, err)
return
}
c.JSON(200, "ok")
}
最後是列印panic的trace記錄
官方的panic其實列印的是所有goroutine的堆疊資訊。但是這裡我們希望列印的是出panic的那個堆疊資訊。所以我們會使用
debug.Stack()
來列印出問題的goroutine的堆疊資訊。
為了列印美觀,這裡將換行符統一替換為\n
來進行展示。
具體的實現程式碼可以參考github地址:https://github.com/gohade/hade/blob/main/framework/util/goroutine/goroutine.go
說明文件:https://github.com/gohade/hade/blob/main/docs/guide/util.md
總結
為hade封裝了兩個SafeGo方法。特別是第二個SafeGoAndWait,在實際工作中確實是非常有用的。