一文教你如何封装安全的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,在实际工作中确实是非常有用的。

关于如何封装安全的go的文章就介绍至此,更多相关封装安全的go内容请搜索编程教程以前的文章,希望以后支持编程教程

 一、包的导入Golang 当导入多个包时,一般按照字母顺序排列包名称,像Goland 等IDE 会在保存文件时自动完成这个动作。Golang 导入包即等同于包含了这个包的所有的代码对象。为避免名称 ...