如何封裝安全的 go

在業務代碼開發過程中,我們會有很大概率使用 go 語言的 goroutine 來開啓一個新的 goroutine 執行另外一段業務,或者開啓多個 goroutine 來並行執行多個業務邏輯。

在業務代碼開發過程中,我們會有很大概率使用 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,在實際工作中確實是非常有用的。

 

軒脈刃的刀光劍影 工作生活中遇到的日常點滴記錄,或許有技術筆記,或許有日常思考。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UWakTncHOQxECiuH3T2HHA