如何封装安全的go

技术杂谈85

如何封装安全的go

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

如何封装安全的go

封装

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

Original: https://www.cnblogs.com/yjf512/p/15921756.html
Author: 轩脉刃
Title: 如何封装安全的go



相关阅读

Title: 中小公司的软件测试过程现状与测试能力成熟度

中小公司的软件测试过程现状

产品经理通常情况下能做的就是功能验收测试,而这种测试也是基于UI界面层,基本的功能测试。固然不会有对接口的测试,对功能上的边界测试,而这些测试实际上是非常重要的。还有一些逆向场景的测试,这些场景实际上也是大部分产品经理逆向思维的一个体现。大部分产品经理实际上只是考虑了正向的场景。这也是产品经理的一个逻辑思维的体现。关于测试思维我们回顾有如下:

1 正向思维方式
正向思维是指软件可以正常运行状态下表现出来的特征如:某个功能点正确实现后是什么样?网站可以提供访问时如何展现?
说明:所以测试按照正常思维方式,检查验证系统功能是否实现,通常是以需求为准则来判断。
2 逆向思维方式
逆向思维是相对于正常思维,通常是检查正常思维相违背的地方,比如功能点未实现后如何?网站不能访问时页面是如何展现?核心服务不可用时,整个系统表现是什么?
3 全局思维方式
全局思维则是从全方位360角度去分析软件系统,如:系统上线后可能会碰到的诸如多种风险情况,针对每种情况是如何测试?
4 局部思维方式
局部思维则是相对于全局思维,通常是检查某个系统在局部情况如:手机信号测试时,可以隔离多种环境进行思考分析
5 极端思维方式
极端思维更准确的应从两端极限分析思考,如:针对信号测试时,最小/大信号范围时,表现如何?网络环境变化类似。
6 比较思维方式
比较思维则更注重的是选择某个标准物做参考,然后制定一些对比参数选项来评判,如:Google/Baidu搜索相同关键字时,返回的内容相关性,响应速度等
7 组合思维方式
组合思维是将多个对象选择组合在一起检查,判断是否正常,如:关机前,启动另一个应用程序,来检查系统是如何处理?

普通软件开发工程师来做测试工作时,当他不具备测试思维时,只是测试的正向流程,边界也不会考虑,更不会考虑性能与安全,所以这种测试必然有缺失,特别是系统中涉及支付业务时,有经过良好的完整的测试过程上线,必然会造成经济损失。过去我们真实的案例有支付退款业务导致的漏洞,是因为前期的测试不完整导致的。软件工程师为什么要懂测试 软件工程师的核心竞争力

是否有测试岗位取决于公司对软件的质量是否重视。另一方面,软件可能不需要太高的质量,这种情况下,面对于研发来说也就没有什么挑战。研发自己来测试的场景,并且整个组织中没有测试岗位,这个时候连发并不能完全保证软件的质量,大部分研发并不具备自测的能力,连单元测试都写不好,这种情况下面整个软件的质量固然不会有太多提升。没有测试岗位,即没有软件测试过程,其也是印证: 康威定律 (康威法则 , Conway's Law)

"设计系统的架构受制于产生这些设计的组织的沟通结构。Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure."
——M. Conway

我们过去经常说要提供给客户高质量的软件,软件测试过程如果做不好,何谈质量?所以本质问题是不论是不是有人在做,有没有这个岗位,而是整个过程中有没有软件测试过程。有的时候,老板认为这是一种节约成本的方式,其本质是,因为这个老板他对测试的认知是浅薄的,还是停留在测试,只是点点而已,殊不知这样的安排在实际系统上线后造成的经济损失更不可预知,部分老板需要经历这样的过程才会吃一堑长一智。而这一类老板通常都是做销售的,对软件研发质量没有完整的认识。也可以说这类的软件团队是非专业的软件过程。个人追求的是在专业的团队里面做专业的事。

互联网的产品和项目缺少性能测试,上线后的后果就是出现资源的超发,在互联网应用的并发下面,由于程序自身没有做好并发的处理,导致资源的超发,最后导致较大的经济损失,最终都需要有人来承担与处理事故,同时也影响公司的声誉。这已经是前车之鉴了,但对于部分公司依然没有引起重视,后面再次出现了线上的事故时,最终让研发自己来承担这个费用,早知道如此,为什么不做好系统性能测试与负载测试呢? 团队里面必须有一个人懂性能测试与负载测试,并能够实际落地,帮助找出系统中潜在的漏洞,不论是架构师,主程,项目经理,测试组长。

我们可以参考如下,反思当前组织测试能力。

TMM测试能力成熟度等级

TMM 级别

TMM 水平的目标

1级:初始

软件应该成功运行

在这个级别,没有识别任何过程域

测试的目的是确保软件运行良好

这个级别缺乏资源、工具和训练有素的员工

软件交付前没有质量保证检查

2级:已定义

制定测试和调试目标和策略

此级别将测试与调试区分开来,它们被视为不同的活动

测试阶段在编码之后

测试的主要目标是显示软件符合规范

基本的测试方法和技术已经到位

3级:综合

将测试集成到软件生命周期中

测试融入整个生命周期

根据需求定义测试目标

测试机构存在

测试被认为是一项专业活动

4级:管理和测量

建立测试测量程序

测试是一个测量和量化的过程

所有开发阶段的审查都被视为测试

对于重用和回归测试,测试用例被收集并记录在测试数据库中

记录缺陷并给出严重程度

5级:优化

测试过程优化

测试是管理和定义的

可以监控测试的有效性和成本

测试可以微调并不断改进

实行质量控制和缺陷预防

实践过程重用

测试相关指标也有工具支持

工具为测试用例设计和缺陷收集提供支持

还有一类团队懂得建立小而美的团队,团队中每个研发懂得软件测试的基本理论与基础,可以自行完成单元测试,接口测试,性能测试,甚至于安全测试。实际上,这样的研发团队已经是 开发测试工程师SDET组成的,这样子的团队过去只有像微软公司才会有过。一个普通的研发需要具备测试思维,需要长时间的训练。

其他参考:

安全OWASP_Top_10_2017_中文版

今天先到这儿,希望对云原生,技术领导力, 企业管理,系统架构设计与评估,团队管理, 项目管理, 产品管管,团队建设 有参考作用 , 您可能感兴趣的文章:

如有想了解更多软件设计与架构, 系统IT,企业信息化, 团队管理 资讯,请关注我的微信订阅号:

Original: https://www.cnblogs.com/wintersun/p/16096435.html
Author: PetterLiu
Title: 中小公司的软件测试过程现状与测试能力成熟度