编程 Golang在整洁架构中优雅使用事务

2024-11-18 19:26:04 +0800 CST views 1066

Golang在整洁架构中优雅使用事务

学习资料

在开始学习之前,先补充整洁架构与依赖注入的前置知识。

预备知识

整洁架构

Kratos 是 Go 语言的微服务框架,GitHub 星标 23k,地址:kratos。该项目提供 CLI 工具,允许用户通过 kratos new xxxx 新建项目,使用 kratos-layout 仓库的代码结构。

kratos-layout 项目为用户提供了一个典型的 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 进行依赖注入。

Service层

在 service 层,实现 RPC 接口的方法,注入 biz:

type GreeterService struct {
   v1.UnimplementedGreeterServer
   uc *biz.GreeterUsecase
}

func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {
   return &GreeterService{uc: uc}
}

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层

在 biz 层,定义 repo 接口,注入 data 层:

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)
}

type GreeterUsecase struct {
   repo GreeterRepo
   log  *log.Helper
}

func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
   return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}

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
}

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
}

数据库连接

注入 data 作为被操作的对象:

type Data struct {
   // TODO: wrapped database client
}

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/transaction/helloworld

该目录基于 go-kratos CLI 工具生成,并在此基础上修改,实现了事务支持。

运行 demo 需要准备:

  1. 本地数据库 devroot:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
  2. 建立表:
CREATE TABLE IF NOT EXISTS greeter (
    hello VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

启动服务

运行服务:

go run ./cmd/helloworld/

通过 config.yaml 配置 HTTP 服务监听 localhost:8000,GRPC 则是 localhost:9000

核心逻辑

helloworld 项目本质是一个打招呼服务。在 internal/biz/greeter.go 文件中,为了测试事务,在 biz 层的 CreateGreeter 方法中,调用了 repo 层的 SaveUpdate 方法,且 Update 方法人为抛出一个异常。

func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
    uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
    var (
        greeter *Greeter
        err     error
    )
    err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
        greeter, err = uc.repo.Save(ctx, g)
        _, err = uc.repo.Update(ctx, g)
        return err
    })
    if err != nil {
        return nil, err
    }
    return greeter, nil
}

Repo层开启事务

为了在 repo 层共用一个事务,在 biz 层使用 db 开启事务,并将事务会话传递给 repo 层的方法。

核心实现

在 biz 层,通过优先执行 ExecTx() 方法,创建事务,并将待执行的两个 repo 方法封装在 fn 参数中,传递给 GORM 实例的 Transaction() 方法。

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 层持有的数据库实例。

参考文献

复制全文 生成海报 编程 软件架构 微服务 数据库管理

推荐文章

LangChain快速上手
2025-03-09 22:30:10 +0800 CST
Vue3中的自定义指令有哪些变化?
2024-11-18 07:48:06 +0800 CST
Node.js中接入微信支付
2024-11-19 06:28:31 +0800 CST
使用Vue 3和Axios进行API数据交互
2024-11-18 22:31:21 +0800 CST
Vue3中如何处理组件的单元测试?
2024-11-18 15:00:45 +0800 CST
Vue3中哪些API被废弃了?
2024-11-17 04:17:22 +0800 CST
基于Flask实现后台权限管理系统
2024-11-19 09:53:09 +0800 CST
Python 微软邮箱 OAuth2 认证 Demo
2024-11-20 15:42:09 +0800 CST
JavaScript设计模式:适配器模式
2024-11-18 17:51:43 +0800 CST
pip安装到指定目录上
2024-11-17 16:17:25 +0800 CST
PHP 如何输出带微秒的时间
2024-11-18 01:58:41 +0800 CST
JavaScript设计模式:装饰器模式
2024-11-19 06:05:51 +0800 CST
git使用笔记
2024-11-18 18:17:44 +0800 CST
支付页面html收银台
2025-03-06 14:59:20 +0800 CST
用 Rust 玩转 Google Sheets API
2024-11-19 02:36:20 +0800 CST
地图标注管理系统
2024-11-19 09:14:52 +0800 CST
智慧加水系统
2024-11-19 06:33:36 +0800 CST
16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
ElasticSearch 结构
2024-11-18 10:05:24 +0800 CST
js函数常见的写法以及调用方法
2024-11-19 08:55:17 +0800 CST
前端代码规范 - Commit 提交规范
2024-11-18 10:18:08 +0800 CST
手机导航效果
2024-11-19 07:53:16 +0800 CST
程序员茄子在线接单