go-zero微服务实战系列(十一、大结局)

本篇是整个系列的最后一篇了,本来打算在系列的最后一两篇写一下关于k8s部署相关的内容,在构思的过程中觉得自己对k8s知识的掌握还很不足,在自己没有理解掌握的前提下我觉得也很难写出自己满意的文章,大家看了可能也会觉得内容没有干货。我最近也在学习k8s的一些最佳实践以及阅读k8s的源码,等待时机成熟的时候可能会考虑单独写一个k8s实战系列文章。

内容回顾

下面列出了整个系列的每篇文章,这个系列文章的主要特点是贴近真实的开发场景,并针对高并发请求以及常见问题进行优化,文章的内容也是循序渐进的,先是介绍了项目的背景,接着进行服务的拆分,拆分完服务进行API的定义和表结构的设计,这和我们实际在公司中的开发流程是类似的,紧接着就是做一些数据库的CRUD基本操作,后面用三篇文章来讲解了缓存,因为缓存是高并发的基础,没有缓存高并发系统就无从谈起,缓存主要是应对高并发的读,接下来又用两篇文章来对高并发的写进行优化,最后通过分布式事务保证为服务间的数据一致性。如果大家能够对每一篇文章都能理解透彻,我觉得对于工作中的绝大多数场景都能轻松应对。

对于文章配套的示例代码并没有写的很完善,有几点原因,一是商城的功能点非常多,很难把所有的逻辑都覆盖到;二是多数都是重复的业务逻辑,只要大家掌握了核心的示例代码,其他的业务逻辑可以自己把代码down下来进行补充完善,这样我觉得才会进步。如果有不理解的地方大家可以在社区群中问我,每个社区群都可以找到我。

go-zero微服务实战系列(一、开篇)

go-zero微服务实战系列(二、服务拆分)

go-zero微服务实战系列(三、API定义和表结构设计)

go-zero微服务实战系列(四、CRUD热身)

go-zero微服务实战系列(五、缓存代码怎么写)

go-zero微服务实战系列(六、缓存一致性保证)

go-zero微服务实战系列(七、请求量这么高该如何优化)

go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)

go-zero微服务实战系列(九、极致优化秒杀性能)

go-zero微服务实战系列(十、分布式事务如何实现)

单元测试

软件测试由单元测试开始(unit test)。更复杂的测试都是在单元测试之上进行的。如下所示测试的层级模型:

单元测试(unit test)是最小、最简单的软件测试形式、这些测试用来评估某一个独立的软件单元,比如一个类,或者一个函数的正确性。这些测试不考虑包含该软件单元的整体系统的正确定。单元测试同时也是一种规范,用来保证某个函数或者模块完全符合系统对其的行为要求。单元测试经常被用来引入测试驱动开发的概念。

go test工具

go语言的测试依赖go test工具,它是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀的源代码文件都是 go test 测试的一部分,不会被go build编译到最终的可执行文件。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数:

类型格式作用
测试单数函数名前缀为Test测试程序的一些逻辑行为是否正确
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example提供示例

go test会遍历所有*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数。

单测格式

每个测试函数必须导入testing包,测试函数的基本格式如下:

func TestName(t *testing.T) {
    // ......
}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,示例如下:

func TestDo(t *testing.T) { //...... }
func TestWrite(t *testing.T) { // ...... }

testing.T 用于报告测试失败和附加的日志信息,拥有的主要方法如下:

Name() string
Fail()
Failed() bool
FailNow()
logDepth(s string, depth int)
Log(args ...any)
Logf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
Skip(args ...any)
Skipf(format string, args ...any)
SkipNow()
Skipped() bool
Helper()
Cleanup(f func())
Setenv(key string, value string)

简单示例

在这个路径下lebron/apps/order/rpc/internal/logic/createorderlogic.go:44 有一个生成订单id的函数,函数如下:

func genOrderID(t time.Time) string {
    s := t.Format("20060102150405")
    m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3
    ms := sup(m, 3)
    p := os.Getpid() % 1000
    ps := sup(int64(p), 3)
    i := atomic.AddInt64(&num, 1)
    r := i % 10000
    rs := sup(r, 4)
    n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs)
    return n
}

我们创建createorderlogic_test.go文件并为该方法编写对应的单元测试函数,生成的订单id长度为24,单元测试函数如下:

func TestGenOrderID(t *testing.T) {
    oid := genOrderID(time.Now())
    if len(oid) != 24 {
        t.Errorf("oid len expected 24, got: %d", len(oid))
    }
}

在当前路径下执行 go test 命令,可以看到输出结果如下:

PASS
ok      github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic    1.395s

还可以加上 -v 输出更完整的结果,go test -v 输出结果如下:

=== RUN   TestGenOrderID
--- PASS: TestGenOrderID (0.00s)
PASS
ok      github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic    1.305s

go test -run

在执行 go test 命令的时候可以添加 -run 参数,它对应一个正则表达式,又有函数名匹配上的测试函数才会被 go test 命令执行,例如我们可以使用 go test -run=TestGenOrderID 来值运行 TestGenOrderID 这个单测。

表格驱动测试

表格驱动测试不是工具,它只是编写更清晰测试的一种方式和视角。编写好的测试并不是一件容易的事情,但在很多情况下,表格驱动测试可以涵盖很多方面,表格里的每一个条目都是一个完整的测试用例,它包含输入和预期的结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。使用表格测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。

lebron/apps/product/rpc/internal/logic/checkandupdatestocklogic.go:53 我们可以编写如下表格驱动测试:

func TestStockKey(t *testing.T) {
    tests := []struct {
        name   string
        input  int64
        output string
    }{
        {"test one", 1, "stock:1"},
        {"test two", 2, "stock:2"},
        {"test three", 3, "stock:3"},
    }

    for _, ts := range tests {
        t.Run(ts.name, func(t *testing.T) {
            ret := stockKey(ts.input)
            if ret != ts.output {
                t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
            }
        })
    }
}

执行命令 go test -run=TestStockKey -v 输出如下:

=== RUN   TestStockKey
=== RUN   TestStockKey/test_one
=== RUN   TestStockKey/test_two
=== RUN   TestStockKey/test_three
--- PASS: TestStockKey (0.00s)
    --- PASS: TestStockKey/test_one (0.00s)
    --- PASS: TestStockKey/test_two (0.00s)
    --- PASS: TestStockKey/test_three (0.00s)
PASS
ok      github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic    1.353s

并行测试

表格驱动测试中通常会定义比较多的测试用例,而go语言又天生支持并发,所以很容易发挥自身优势将表格驱动测试并行化,可以通过t.Parallel() 来实现:

func TestStockKeyParallel(t *testing.T) {
  t.Parallel()
    tests := []struct {
        name   string
        input  int64
        output string
    }{
        {"test one", 1, "stock:1"},
        {"test two", 2, "stock:2"},
        {"test three", 3, "stock:3"},
    }

    for _, ts := range tests {
        ts := ts
        t.Run(ts.name, func(t *testing.T) {
            t.Parallel()
            ret := stockKey(ts.input)
            if ret != ts.output {
                t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
            }
        })
    }
}

测试覆盖率

测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总的代码的比例。go提供内置的功能来检查代码覆盖率,即使用 go test -cover 来查看测试覆盖率:

PASS
coverage: 0.6% of statements
ok      github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic    1.381s

可以看到我们的覆盖率只有 0.6% ,哈哈,这是非常不合格滴,大大的不合格。go还提供了一个 -coverprofile 参数,用来将覆盖率相关的记录输出到文件 go test -cover -coverprofile=cover.out

PASS
coverage: 0.6% of statements
ok      github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic    1.459s

然后执行 go tool cover -html=cover.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成测试报告

解决依赖

对于单测中的依赖,我们一般采用mock的方式进行处理,gomock是Go官方提供的测试框架,它在内置的testing包或其他环境中都能够很方便的使用。我们使用它对代码中的那些接口类型进行mock,方便编写单元测试。对于gomock的使用请参考gomock文档

mock依赖interface,对于非interface场景下的依赖我们可以采用打桩的方式进行mock数据,monkey是一个Go单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。monkey库很强大,但是使用时需注意以下事项:

  • monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-l关闭Go语言的内联优化。
  • monkey不是线程安全的,所以不要把它用到并发的单元测试中。

其他

画图工具

社区中经常有人问画图用的是什么工具,本系列文章中的插图工具主要是如下两个

https://www.onemodel.app/

https://whimsical.com/

代码规范

代码不光是要实现功能,很重要的一点是代码是写给别人看的,所以我们对代码的质量要有一定的要求,要遵循规范,可以参考go官方的代码review建议

https://github.com/golang/go/...

谈谈感受

时间过得贼快,不知不觉间这个系列已经写到十一篇了。按照每周更新两篇的速度也写了一个多月了。写文章是个体力活且非常的耗时,又生怕有写的不对的地方,对大家产生误导,所以还需要反复的检查和查阅相关资料。平均一篇文章要写一天左右,平时工作日比较忙,基本都是周六日来写,因此最近一个月周六日基本没有休息过。

但我觉得收获也非常大,在写文章的过程中,对于自己掌握的知识点,是一个复习的过程,可以让自己加深对知识点的理解,对于自己没有掌握的知识点就又是一个学习新知识的过程,让自己掌握了新的知识,所以我和读者也是一起在学习进步呢。大家都知道,对于自己理解的知识,想要说出来或者写出来让别人也理解也是不容易的,因此写文章对自己的软实力也是有很大的提升。

所以,我还是会继续坚持写文章,坚持输出,和大家一起学习成长。同时,我也欢迎大家来 "微服务实践" 公众号来投稿。可能有些人觉得自己的水平不行,担心写的内容不高端,没有逼格,我觉得大可不必,只要能把知识点讲明白就非常棒了,可以是基础知识,也可以是最佳实践等等。kevin会对投稿的每一篇文章都认真审核,写的不对的地方他都会指出来,所有还有和kevin一对一交流学习的机会,小伙伴们抓紧行动起来呀。

结束语

非常感谢大家这一个多月以来的支持。看到每篇文章有那么多的点赞,我十分的开心,也更加的有动力,所以,也在计划写下个系列的文章,目前有两个待选的主题,分别是《go-zero源码系列》和《gRPC实战源码系列》,欢迎小伙伴们在评论区留下你的评论,说出你更期待哪个系列,如果本篇文章点赞数超过66的话,咱就继续开整。

代码仓库: https://github.com/zhoushuguang/lebron

项目地址

https://github.com/zeromicro/go-zero

欢迎使用 go-zerostar 支持我们!

微信交流群

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。


go-zero开源社区
go-zero相关微服务实践

go-zero作者

911 声望
3.5k 粉丝
0 条评论
推荐阅读
熔断原理分析与源码解读
熔断机制(Circuit Breaker)指的是在股票市场的交易时间中,当价格的波动幅度达到某一个限定的目标(熔断点)时,对其暂停交易一段时间的机制。此机制如同保险丝在电流过大时候熔断,故而得名。熔断机制推出的目...

kevinwan1阅读 1.1k

SegmentFault 思否正式开源问答社区软件 Answer
作为国内领先的新一代技术问答社区,SegmentFault 思否团队在社区建设上有着多年积累。Answer 不仅拥有搭建问答平台(Q&A Platform)的基础功能,还在产品设计上融入了开发团队对社区发展的思考,并将其经验产品...

SegmentFault思否29阅读 4.2k评论 14

封面图
Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀21阅读 54.6k评论 1

年度最佳【golang】map详解
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

去去100214阅读 10.9k评论 2

年度最佳【golang】GMP调度详解
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的. 这篇文章将通过分析...

去去100213阅读 11.1k评论 4

【已结束】SegmentFault 思否技术征文丨浅谈 Go 语言框架
亲爱的开发者们:我们的 11 月技术征文如期而来,这次主题围绕 「 Go 」 语言,欢迎大家来参与分享~征文时间11 月 4 日 - 11 月 27 日 23:5911 月 28 日 18:00 前发布中奖名单参与条件新老思否作者均可参加征文...

SegmentFault思否11阅读 4.6k评论 11

封面图
【Go微服务】开发gRPC总共分三步
之前我也有写过RPC相关的文章:《 Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?》,详细介绍了RPC是什么,使用边界在哪里?并且用Go和php举例,实现了跨语言调用。不了解RPC的同学建议先读这篇文...

王中阳Go8阅读 3.5k评论 6

封面图

go-zero作者

911 声望
3.5k 粉丝
宣传栏