如何封装安全的go-《GO开发知识笔记》

admin 2025-11-04 01:04:09 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 封装
    • SafeGo
    • SafeGoAndWait
  • 实现说明
    • 首先是接口设计方面
    • 其次是日志兼容hade
    • 最后是打印panic的trace记录
  • 总结

    在业务代码开发过程中,我们会有很大概率使用go语言的goroutine来开启一个新的goroutine执行另外一段业务,或者开启多个goroutine来并行执行多个业务逻辑。所以我为hade框架增加了两个方法goroutine.SafeGo 和 goroutine.SafeGoAndWait。image.png

    封装

    SafeGo

    SafeGo 这个函数,提供了一种goroutine安全的函数调用方式。主要适用于业务中需要进行开启异步goroutine业务逻辑调用的场景。

    1. / SafeGo 进行安全的goroutine调用
    2. // 第一个参数是context接口,如果还实现了Container接口,且绑定了日志服务,则使用日志服务
    3. // 第二个参数是匿名函数handler, 进行最终的业务逻辑
    4. // SafeGo 函数并不会返回error,panic都会进入hade的日志服务
    5. func SafeGo(ctx context.Context, handler func())

    调用方式参照如下的单元测试用例:

    1. func TestSafeGo(t *testing.T) {
    2. container := tests.InitBaseContainer()
    3. container.Bind(&log.HadeTestingLogProvider{})
    4. ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
    5. goroutine.SafeGo(ctx, func() {
    6. time.Sleep(1 * time.Second)
    7. return
    8. })
    9. t.Log("safe go main start")
    10. time.Sleep(2 * time.Second)
    11. t.Log("safe go main end")
    12. goroutine.SafeGo(ctx, func() {
    13. time.Sleep(1 * time.Second)
    14. panic("safe go test panic")
    15. })
    16. t.Log("safe go2 main start")
    17. time.Sleep(2 * time.Second)
    18. t.Log("safe go2 main end")
    19. }

    SafeGoAndWait

    SafeGoAndWait 这个函数,提供安全的多并发调用方式。该函数等待所有函数都结束后才返回。

    1. // SafeGoAndWait 进行并发安全并行调用
    2. // 第一个参数是context接口,如果还实现了Container接口,且绑定了日志服务,则使用日志服务
    3. // 第二个参数是匿名函数handlers数组, 进行最终的业务逻辑
    4. // 返回handlers中任何一个错误(如果handlers中有业务逻辑返回错误)
    5. func SafeGoAndWait(ctx context.Context, handlers ...func() error) error

    调用方式参照如下的单元测试用例:

    1. func TestSafeGoAndWait(t *testing.T) {
    2. container := tests.InitBaseContainer()
    3. container.Bind(&log.HadeTestingLogProvider{})
    4. errStr := "safe go test error"
    5. t.Log("safe go and wait start", time.Now().String())
    6. ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
    7. err := goroutine.SafeGoAndWait(ctx, func() error {
    8. time.Sleep(1 * time.Second)
    9. return errors.New(errStr)
    10. }, func() error {
    11. time.Sleep(2 * time.Second)
    12. return nil
    13. }, func() error {
    14. time.Sleep(3 * time.Second)
    15. return nil
    16. })
    17. t.Log("safe go and wait end", time.Now().String())
    18. if err == nil {
    19. t.Error("err not be nil")
    20. } else if err.Error() != errStr {
    21. t.Error("err content not same")
    22. }
    23. // panic error
    24. err = goroutine.SafeGoAndWait(ctx, func() error {
    25. time.Sleep(1 * time.Second)
    26. return errors.New(errStr)
    27. }, func() error {
    28. time.Sleep(2 * time.Second)
    29. panic("test2")
    30. }, func() error {
    31. time.Sleep(3 * time.Second)
    32. return nil
    33. })
    34. if err == nil {
    35. t.Error("err not be nil")
    36. } else if err.Error() != errStr {
    37. t.Error("err content not same")
    38. }
    39. }

    实现说明

    实现方面,有几个难点记录下。

    首先是接口设计方面

    可以看到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中获取日志服务,来进行日志打印。这样框架的所有日志就能统一在日志打印里面。

    1. if logger != nil {
    2. logger.Error(ctx, "safe go handler panic", map[string]interface{}{
    3. "stack": string(buf),
    4. "err": e,
    5. })
    6. } else {
    7. log.Printf("panic\t%v\t%s", e, buf)
    8. }

    由于我们修改了gin的context,让它支持了我们的container容器结构,所以我们可以直接将gin.Context传递进来。具体使用起来就像这样了:

    1. // DemoGoroutine goroutine 的使用示例
    2. func (api *DemoApi) DemoGoroutine(c *gin.Context) {
    3. logger := c.MustMakeLog()
    4. logger.Info(c, "request start", nil)
    5. // 初始化一个orm.DB
    6. gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
    7. db, err := gormService.GetDB(orm.WithConfigPath("database.default"))
    8. if err != nil {
    9. logger.Error(c, err.Error(), nil)
    10. c.AbortWithError(50001, err)
    11. return
    12. }
    13. db.WithContext(c)
    14. err = goroutine.SafeGoAndWait(c, func() error {
    15. // 查询一条数据
    16. queryUser := &User{ID: 1}
    17. err = db.First(queryUser).Error
    18. logger.Info(c, "query user1", map[string]interface{}{
    19. "err": err,
    20. "name": queryUser.Name,
    21. })
    22. return err
    23. }, func() error {
    24. // 查询一条数据
    25. queryUser := &User{ID: 2}
    26. err = db.First(queryUser).Error
    27. logger.Info(c, "query user2", map[string]interface{}{
    28. "err": err,
    29. "name": queryUser.Name,
    30. })
    31. return err
    32. })
    33. if err != nil {
    34. c.AbortWithError(50001, err)
    35. return
    36. }
    37. c.JSON(200, "ok")
    38. }

    最后是打印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,在实际工作中确实是非常有用的。

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  5