本篇是整个系列的最后一篇了,本来打算在系列的最后一两篇写一下关于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/wiki/CodeReviewComments

谈谈感受

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

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

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

结束语

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

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

项目地址

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

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

微信交流群

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

go-zero微服务实战系列(十一、大结局)的更多相关文章

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

    前两篇文章分别介绍了本系列文章的背景以及根据业务职能对商城系统做了服务的拆分,其中每个服务又可分为如下三类: api服务 - BFF层,对外提供HTTP接口 rpc服务 - 内部依赖的微服务,实现单一 ...

  2. ASP.NET Core微服务实战系列

    希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注. 前言 这里记录的是个人奋斗和成长的地方,该篇只是一个系列目录和构想 ...

  3. 微服务实战系列--Nginx官网发布(转)

    这是Nginx官网写的一个系列,共七篇文章,如下 Introduction to Microservices (this article) Building Microservices: Using ...

  4. Chris Richardson微服务实战系列

    微服务实战(一):微服务架构的优势与不足 微服务实战(二):使用API Gateway 微服务实战(三):深入微服务架构的进程间通信 微服务实战(四):服务发现的可行方案以及实践案例 微服务实践(五) ...

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

    前言 在社区中经常看到有人问有没有基于 go-zero 的比较完整的项目参考,该类问题本质上是想知道基于 go-zero 的项目的最佳实践.完整的项目应该是一个完整的产品功能,包含产品需求.架构设计. ...

  6. 微服务实战系列(七)-网关springcloud gateway

    1. 场景描述 springcloud刚推出的时候用的是netflix全家桶,路由用的zuul,但是据说zull1.0在大数据量访问的时候存在较大性能问题,2.0就没集成到springcloud中了, ...

  7. 微服务实战系列(八)-网关springcloud gateway自定义规则

    1. 场景描述 先说明下项目中使用的网关是:springcloud gateway, 因需要给各个网关服务系统提供自定义配置路由规则,实时生效,不用重启网关(重启风险大),目前已实现:动态加载自定义路 ...

  8. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇(内附开发 demo)

    简介 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系. 系列 云原生 API 网关,gRPC-Gateway V2 初探 业务流程 官方开发接入文档 ...

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

    微服务概述 微服务架构是一种架构风格,它将一个大的系统构建为多个微服务的集合,这些微服务是围绕业务功能构建的,服务关注单一的业务功能,这些服务具有以下特点: 高度可维护和可测试 松散的耦合 可独立部署 ...

随机推荐

  1. 【面试普通人VS高手系列】Redis和Mysql如何保证数据一致性

    今天分享一道一线互联网公司高频面试题. "Redis和Mysql如何保证数据一致性". 这个问题难倒了不少工作5年以上的程序员,难的不是问题本身,而是解决这个问题的思维模式. 下面 ...

  2. pyqt5 重启相同线程错误:QThread: Destroyed while thread is still running

    背景: 把一个基于QObject的类的槽运行在另一个线程,我们可以用moveToThread的方法. 1 新建一个子线程类,编写槽函数和信号,MyClass *m_MyClass=new MyClas ...

  3. 1.10 Linux桌面环境(桌面系统)大比拼[附带优缺点

    早期的 Linux 系统都是不带界面的,只能通过命令来管理,比如运行程序.编辑文档.删除文件等.所以,要想熟练使用 Linux,就必须记忆很多命令. 后来随着 Windows 的普及,计算机界面变得越 ...

  4. CEPH-5:ceph集群基本概念与管理

    ceph集群基本概念与管理 ceph集群基本概念 ceph集群整体结构图 名称 作用 osd 全称Object Storage Device,主要功能是存储数据.复制数据.平衡数据.恢复数据等.每个O ...

  5. SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统

    一.单点登录SSO介绍   目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...

  6. .netcore6.0自己配置swagger

    环境:.net core6.0 一.安装依赖包:Swashbuckle.AspNetCore 二.右击项目->属性->生成->输出,勾选文档文件,然后配置文件生成路径,注意是相对路径 ...

  7. 6. ZigZag Conversion - LeetCode

    Question 6. ZigZag Conversion Solution 题目大意:将字符串按Z字型排列,然后再一行一行按字符输出 思路:按题目中的第一个例子,画出如下图,通过n的不同值,可以找出 ...

  8. 使用 awk 命令统计文本

    2022-04-19 11:25:15.008,b4d13bfca8fe4b93a85e65a88520d945,LogScheduler#printLog,10ms,Y,xxxxxxxx 2022- ...

  9. sqlserver2008 数据库中查询存储过程的的创建修改和执行时间,以及比较常见的系统视图和存储过程

    因为各种原因数据库中存在大量无用的存储过程,想查询存储过程的最后执行情况,处理长期不使用的存储过程 下面这条语句可以查询存储过程创建 修改和执行的最后时间: SELECT a.name AS 存储过程 ...

  10. 前端向后端传递formData类型的二进制文件

    // 获取到的文件file类型转换为formData类型 let formData = new FormData(); formData.append("file", file文件 ...