网络知识 娱乐 go-zero微服务实战系列(十一、大结局)

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() stringFail()Failed() boolFailNow()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() boolHelper()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 命令,可以看到输出结果如下:

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

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

=== RUN TestGenOrderID--- PASS: TestGenOrderID (0.00s)PASSok 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)PASSok 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 来查看测试覆盖率:

PASScoverage: 0.6% of statementsok github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic 1.381s

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

PASScoverage: 0.6% of statementsok 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/wiki/CodeReviewComments

谈谈感受

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

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

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

结束语

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

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

项目地址

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

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

微信交流群

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