Golang在整洁架构基础上实现事务
前言
大家好,这里是白泽,这篇文章在 go-kratos 官方的 layout 项目的整洁架构基础上,实现优雅的数据库事务操作。
视频讲解 :B站:白泽talk,公众号【白泽talk】

本期涉及的学习资料:
- 我的开源Golang学习仓库:https://github.com/BaiZe1998/go-learning,这期的所有内容汇聚成一个可运行的 demo,
kit/transaction路径下。 - kratos CLI 工具:
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest。 - kratos 微服务框架:https://github.com/go-kratos/kratos
- wire 依赖注入库:https://github.com/google/wire
- 领域驱动设计思想:本文不多涉及,具备相关背景知识食用本文更佳。
在开始学习之前,先补齐一下整洁架构 & 依赖注入的前置知识。
预备知识
整洁架构
kratos 是 Go 语言的一个微服务框架,github 23k,https://github.com/go-kratos/kratos
该项目提供了 CLI 工具,允许用户通过 kratos new xxxx,新建一个 xxxx 项目,这个项目将使用 kratos-layout 仓库的代码结构。
仓库地址:https://github.com/go-kratos/kratos-layout

kratos-layout 项目为用户提供的,配合 CLI 工具生成的一个典型的 Go 项目布局看起来像这样:
application
|____api
| |____helloworld
| | |____v1
| | |____errors
|____cmd
| |____helloworld
|____configs
|____internal
| |____conf
| |____data
| |____biz
| |____service
| |____server
|____test
|____pkg
|____go.mod
|____go.sum
|____LICENSE
|____README.md
依赖注入
通过依赖注入,实现了资源的使用和隔离,同时避免了重复创建资源对象,是实现整洁架构的重要一环。
kratos 的官方文档中提到,十分建议用户尝试使用 wire 进行依赖注入,整个 layout 项目,也是基于 wire,完成了整洁架构的搭建。
service 层,实现 rpc 接口定义的方法,实现对外交互,注入了 biz。
// GreeterService is a greeter service.
type GreeterService struct {
v1.UnimplementedGreeterServer
uc *biz.GreeterUsecase
}
// NewGreeterService new a greeter service.
func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {
return &GreeterService{uc: uc}
}
// SayHello implements helloworld.GreeterServer.
func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {
g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})
if err != nil {
return nil, err
}
return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
}
biz 层:定义 repo 接口,注入 data 层。
// GreeterRepo is a Greater repo.
type GreeterRepo interface {
Save(context.Context, *Greeter) (*Greeter, error)
Update(context.Context, *Greeter) (*Greeter, error)
FindByID(context.Context, int64) (*Greeter, error)
ListByHello(context.Context, string) ([]*Greeter, error)
ListAll(context.Context) ([]*Greeter, error)
}
// GreeterUsecase is a Greeter usecase.
type GreeterUsecase struct {
repo GreeterRepo
log *log.Helper
}
// NewGreeterUsecase new a Greeter usecase.
func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}
// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
return uc.repo.Save(ctx, g)
}
data 作为数据访问的实现层,实现了上游接口,注入了数据库实例资源。
type greeterRepo struct {
data *Data
log *log.Helper
}
// NewGreeterRepo .
func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {
return &greeterRepo{
data: data,
log: log.NewHelper(logger),
}
}
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
return g, nil
}
func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
return g, nil
}
func (r *greeterRepo) FindByID(context.Context, int64) (*biz.Greeter, error) {
return nil, nil
}
func (r *greeterRepo) ListByHello(context.Context, string) ([]*biz.Greeter, error) {
return nil, nil
}
func (r *greeterRepo) ListAll(context.Context) ([]*biz.Greeter, error) {
return nil, nil
}
db:注入 data,作为被操作的对象。
type Data struct {
// TODO wrapped database client
}
// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{}, cleanup, nil
}
Golang 优雅事务
准备
项目获取:强烈建议克隆仓库后实机操作。
git clone git@github.com:BaiZe1998/go-learning.git
cd kit/transcation/helloworld
这个目录基于 go-kratos CLI 工具使用 kratos new helloworld 生成,并在此基础上修改,实现了事务支持。
运行 demo 需要准备:
- 本地数据库 dev:
root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local - 建立表:
CREATE TABLE IF NOT EXISTS greater (
hello VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ps:Makefile 中提供了使用 goose 进行数据库变更管理的能力(goose 也是一个开源的高 项目,推荐学习)
up:
goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" up
down:
goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" down
create:
goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" create ${name} sql
启动服务:
go run ./cmd/helloworld/,通过config.yaml配置了 HTTP 服务监听 localhost:8000,GRPC 则是 localhost:9000。发起一个 get 请求

核心逻辑
helloworld 项目本质是一个打招呼服务,由于 kit/transcation/helloworld 已经是魔改后的版本,为了与默认项目做对比,你可以自行生成一个 helloworld 项目,在同级目录下,对照学习。
在 internal/biz/greeter.go 文件中,是我更改的内容,为了测试事务,我在 biz 层的 CreateGreeter 方法中,调用了 repo 层的 Save 和 Update 两个方法,且这两个方法都会成功,但是 Update 方法人为抛出一个异常。
// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
var (
greater *Greeter
err error
)
//err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
// // 更新所有 hello 为 hello + "updated",且插入新的 hello
// greater, err = uc.repo.Save(ctx, g)
// _, err = uc.repo.Update(ctx, g)
// return err
//})
greater, err = uc.repo.Save(ctx, g)
_, err = uc.repo.Update(ctx, g)
if err != nil {
return nil, err
}
return greater, nil
}
// Update 人为抛出异常
func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
if result.RowsAffected == 0 {
return nil, fmt.Errorf("greeter %s not found", g.Hello)
}
return nil, fmt.Errorf("custom error")
//return g, nil
}
repo 层开启事务
如果忽略上文注释中的内容,因为两个 repo 的数据库操作都是独立的。
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
result := r.data.db.DB(ctx).Create(g)
return g, result.Error
}
func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
if result.RowsAffected == 0 {
return nil, fmt.Errorf("greeter %s not found", g.Hello)
}
return nil, fmt.Errorf("custom error")
//return g, nil
}
即使最后抛出 Update 的异常,但是 save 和 update 都已经成功了,且彼此不强关联,数据库中会多增加一条数据。

biz 层开启事务
因此为了 repo 层的两个方法能够共用一个事务,应该在 biz 层就使用 db 开启事务,且将这个事务的会话传递给 repo 层的方法。
如何传递:使用 context 便成了顺理成章的方案。
接下来将 internal/biz/greeter.go 文件中注释的部分释放,且注释掉分开使用事务的两行,此时重新运行项目请求接口,则由于 Update 方法抛出 err,导致事务回滚,未出现新增的 xiaomingupdated 记录。
// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
var (
greater *Greeter
err error
)
err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
// 更新所有 hello 为 hello + "updated",且插入新的 hello
greater, err = uc.repo.Save(ctx, g)
_, err = uc.repo.Update(ctx, g)
return err
})
//greater, err = uc.repo.Save(ctx, g)
//_, err = uc.repo.Update(ctx, g)
if err != nil {
return nil, err
}
return greater, nil
}
核心实现
由于 biz 层的 Usecase 实例持有 *DBClient,repo 层也持有 *DBClient,且二者在依赖注入的时候,代表同一个数据库连接池实例。
在 pkg/db/db.go 中,为 *DBClient 提供了如下两个方法: ExecTx() & DB()。
在 biz 层,通过优先执行 ExecTx() 方法,创建事务,以及将待执行的两个 repo 方法封装在 fn 参数中,传递给 gorm 实例的 Transaction() 方法待执行。
同时在 Transcation 内部,触发 fn() 函数,也就是聚合的两个 repo 操作,需要注意的是,此时将携带 contextTxKey 事务 tx 的 ctx 作为参数传递给了 fn 函数,因此下游的两个 repo 可以获取到 biz 层的事务会话。
type contextTxKey struct{}
// ExecTx gorm Transaction
func (c *DBClient) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error {
return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
ctx = context.WithValue(ctx, contextTxKey{}, tx)
return fn(ctx)
})
}
func (c *DBClient) DB(ctx context.Context) *gorm.DB {
tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
if ok {
return tx
}
return c.db
}
在 repo 层执行数据库操作的时候,尝试通过 DB() 方法,从 ctx 中获取到上游传递下来的事务会话,如果有则使用,如果没有,则使用 repo 层自己持有的 *DBClient,进行数据访问操作。
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
result := r.data.db.DB(ctx).Create(g)
return g, result.Error
}
func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
if result.RowsAffected == 0 {
return nil, fmt.Errorf("greeter %s not found", g.Hello)
}
return nil, fmt.Errorf("custom error")
//return g, nil
}
参考文献
Golang在整洁架构基础上实现事务的更多相关文章
- Pass Infrastructure基础架构(上)
Pass Infrastructure基础架构(上) Operation Pass OperationPass : Op-Specific OperationPass : Op-Agnostic De ...
- Golang优秀开源项目汇总, 10大流行Go语言开源项目, golang 开源项目全集(golang/go/wiki/Projects), GitHub上优秀的Go开源项目
Golang优秀开源项目汇总(持续更新...)我把这个汇总放在github上了, 后面更新也会在github上更新. https://github.com/hackstoic/golang-open- ...
- 微服务架构基础之Service Mesh
ServiceMesh(服务网格) 概念在社区里头非常火,有人提出 2018 年是 ServiceMesh 年,还有人提出 ServiceMesh 是下一代的微服务架构基础. 那么到底什么是 Serv ...
- Spring基础系列-Spring事务不生效的问题与循环依赖问题
原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9476550.html 一.提出问题 不知道你是否遇到过这样的情况,在ssm框架中开发we ...
- 【原创】002 | 搭上SpringBoot事务源码分析专车
前言 如果这是你第二次看到师长,说明你在觊觎我的美色! 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 专车介绍** 该趟专车是开往Spring Boot事务源码分析的专车 专车问题 为 ...
- Java进阶专题(十七) 系统缓存架构设计 (上)
前言 我们将先从Redis.Nginx+Lua等技术点出发,了解缓存应用的场景.通过使用缓存相关技术,解决高并发的业务场景案例,来深入理解一套成熟的企业级缓存架构如何设计的.本文Redis部分总结 ...
- 如何在国产龙芯架构平台上运行c/c++、java、nodejs等编程语言
高能预警:本文内容过于硬核,涉及编译器原理.cpu指令集.机器码.编程语言原理.跨平台原理等计算机专业基础知识,建议具有c.c++.java.nodejs等多种编程语言开发能力,且实战经验丰富的资深开 ...
- Golang 汇编asm语言基础学习
Golang 汇编asm语言基础学习 一.CPU 基础知识 cpu 内部结构 cpu 内部主要是由寄存器.控制器.运算器和时钟四个部分组成. 寄存器:用来暂时存放指令.数据等对象.它是一个更快的内存. ...
- 基于ASP.NET Core 6.0的整洁架构
大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 本节将介绍基于ASP.NET Core的整洁架构的设计理念,同时基于理论落地的代码 ...
- 【JavaEE】SSH+Spring Security基础上配置AOP+log4j
Spring Oauth2大多数情况下还是用不到的,主要使用的还是Spring+SpringMVC+Hibernate,有时候加上SpringSecurity,因此,本文及以后的文章的example中 ...
随机推荐
- vc++6.0设置字体
vc++6.0设置字体 如上图, 在注册表上找到这个位置. 自已设置FontFace和FontSize即可. 计算机\HKEY_CURRENT_USER\Software\Microsoft\Devs ...
- 一些自托管(self hosted)服务的使用笔记
opengrok oracle的opengrok是一个项目代码查找工具,自建索引,类似工具有source insight 官方已经提供好了docker镜像,傻瓜式安装.不过增加新的项目源码时需要手动更 ...
- 设备树DTS 学习: 4-uboot 传递 dtb 给 内核
背景 得到 dtb 文件以后,我们需要想办法下载到 板子中,并给 Linux 内核使用. (高级版本的 uboot也有了 自己使用设备树支持,我们这里不讨论 uboot 使用的设备树) Linux 内 ...
- Mac Idea中获取application.properties的值,中文乱码
设置idea配置 将Properties Files (*.properties)下的Default encoding for properties files设置为UTF-8,将Transparen ...
- Mybatis 一级缓存
Mybatis一级缓存介绍 什么是缓存 程序经常要调用的对象存在内容中,方法其使用时可以快速调用,不必去数据库或者其他持久化设备中查询,主要就是提高性能 Mybatis一级缓存 简介:一级缓存的作用域 ...
- c++临时对象导致的生命周期问题
对象的生命周期是c++中非常重要的概念,它直接决定了你的程序是否正确以及是否存在安全问题. 今天要说的临时变量导致的生命周期问题是非常常见的,很多时候没有一定经验甚至没法识别出来.光是我自己写.rev ...
- 敏捷开发(Scrum)
一.敏捷的背景与动机 1.1 软件危机及软件工程的出现 速度是企业竞争致胜的关键因素,软件项目的最大挑战在于,一方面要应付变动中的需求,一方面要在紧缩的时程内完成项目,传统的软件工程难以满足这些要 ...
- 深度学习论文翻译解析(二十三):Segment Angthing
论文标题:Segment Angthing 论文作者: Alexander Kirillov Eric Mintun Nikhila Ravi Hanzi Mao... 论文地址:2304.02 ...
- 【原创软件】第7期:文件夹生成器V1.0-按照列表批量生成文件夹,简单小巧
一.背景 因为工作需要,需要批量创建文件夹.为了省去人工创建时间,使用aardio制作了一个软件. 二.功能演示 三.下载地址 https://www.123pan.com/s/9Rn9-1xppH ...
- SQL查询语句汇总
SQL查询语句汇总 students表 id class_id name gender score 1 1 小明 M 90 2 1 小红 F 95 class表 id name 1 一班 2 二班 3 ...