如何封装安全的go
如何封装安全的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的更多相关文章
- [C#] 简单的 Helper 封装 -- RegularExpressionHelper
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- iOS开发之App间账号共享与SDK封装
上篇博客<iOS逆向工程之KeyChain与Snoop-it>中已经提到了,App间的数据共享可以使用KeyChian来实现.本篇博客就实战一下呢.开门见山,本篇博客会封装一个登录用的SD ...
- Ajax实现原理,代码封装
都知道实现页面的异步操作需要使用Ajax,那么Ajax到是怎么实现异步操作的呢? 首先需要认识一个对象 --> XMLHttpRequest 对象 --> Ajax的核心.它有许多的属性和 ...
- 用C语言封装OC对象(耐心阅读,非常重要)
用C语言封装OC对象(耐心阅读,非常重要) 本文的主要内容来自这里 前言 做iOS开发的朋友,对OC肯定非常了解,那么大家有没有想过OC中NSInteger,NSObject,NSString这些对象 ...
- 【知识必备】RxJava+Retrofit二次封装最佳结合体验,打造懒人封装框架~
一.写在前面 相信各位看官对retrofit和rxjava已经耳熟能详了,最近一直在学习retrofit+rxjava的各种封装姿势,也结合自己的理解,一步一步的做起来. 骚年,如果你还没有掌握ret ...
- 对百度WebUploader开源上传控件的二次封装,精简前端代码(两句代码搞定上传)
前言 首先声明一下,我这个是对WebUploader开源上传控件的二次封装,底层还是WebUploader实现的,只是为了更简洁的使用他而已. 下面先介绍一下WebUploader 简介: WebUp ...
- 封装集合(Encapsulate Collection)
封装就是将相关的方法或者属性抽象成为一个对象. 封装的意义: 对外隐藏内部实现,接口不变,内部实现自由修改. 只返回需要的数据和方法. 提供一种方式防止数据被修改. 更好的代码复用. 当一个类的属性类 ...
- CSharpGL(29)初步封装Texture和Framebuffer
+BIT祝威+悄悄在此留下版了个权的信息说: CSharpGL(29)初步封装Texture和Framebuffer +BIT祝威+悄悄在此留下版了个权的信息说: Texture和Framebuffe ...
- CSharpGL(7)对VAO和VBO的封装
CSharpGL(7)对VAO和VBO的封装 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入门参考 ...
- Swift -- 对AFN框架的封装
Swift -- 对AFN框架的封装 一.封装AFN的目的 简单的说: 解耦 日常工作中,我们一般都不会去直接使用AFNetWorking来直接发送网络请求,因为耦合性太强,假设有多个控制器都使用AF ...
随机推荐
- Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401开发
Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...
- 【Java】泛型
文章目录 泛型 为什么要有泛型 在集合中使用泛型 如何自定义泛型结构 自定义泛型类.接口 泛型方法 泛型在继承方面的体现 通配符的使用 有限制条件的通配符的使用 泛型 为什么要有泛型 集合容器类在设计 ...
- 【Java】集合
文章目录 集合框架的概述 数组在存储多个数据方面的特点 数组在存储多个数据方面的缺点 集合框架 Collection接口中的方法的使用 add(Object e) size() addAll(Coll ...
- Flowable实战(七)用户和组
在流程中,最重要的参与者是用户.流程定义了任务何时需要用户参与,什么用户可以参与. 组可以理解为我们常说的角色. Flowable中内置了一套简单的对用户和组的支持,身份管理(IDM ID ...
- MATLAB中拟合算法刚入门
%%%1.拟合问题:(做预测,主要使用的范围是样本比较小,拟合效果会好,样本比较多,拟合的效果就不是很好) 1.应用预测的场景:已经知道10年的样本,预测第11年以内的数据 2.用拟合的到关系式:样本 ...
- DEEP LEARNING WITH PYTORCH: A 60 MINUTE BLITZ | TENSORS
Tensor是一种特殊的数据结构,非常类似于数组和矩阵.在PyTorch中,我们使用tensor编码模型的输入和输出,以及模型的参数. Tensor类似于Numpy的数组,除了tensor可以在GPU ...
- idea 插件推荐
工欲善其事必先利其器,本文介绍几个自己在开发过程中常用的idea插件 安装方法 idea 里面在线安装 settings>plugins>marketplace 里面搜索安装 idea 官 ...
- 【记录一个问题】opencv官网的opencv android sdk使用opencl并未用到GPU
UMat u_mat;mat.copyTo(u_mat);cv::cvtColor(u_mat, cv::BGR2GARY);这样的代码反复执行,并未发现GPU占用提升.执行时间与不使用UMat相当. ...
- 不难懂--------react笔记
在jsx中不能使用class定义类名 因为class在js中是用来定义类的 定义类名的时候用className label中的for必须写成htmlFor Rea ...
- LNMP架构搭建
目录 一:LNMP架构简介 1.Nginx与uwsgi 二:django框架+python 1.创建用户 2.安装依赖包 3.安装uwsgi和django 4.测试python 5.创建django项 ...