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

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

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

参考文献

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

推荐文章

一个简单的打字机效果的实现
2024-11-19 04:47:27 +0800 CST
介绍 Vue 3 中的新的 `emits` 选项
2024-11-17 04:45:50 +0800 CST
mysql关于在使用中的解决方法
2024-11-18 10:18:16 +0800 CST
前端代码规范 - Commit 提交规范
2024-11-18 10:18:08 +0800 CST
微信内弹出提示外部浏览器打开
2024-11-18 19:26:44 +0800 CST
Java环境中使用Elasticsearch
2024-11-18 22:46:32 +0800 CST
在 Rust 生产项目中存储数据
2024-11-19 02:35:11 +0800 CST
设置mysql支持emoji表情
2024-11-17 04:59:45 +0800 CST
Elasticsearch 监控和警报
2024-11-19 10:02:29 +0800 CST
宝塔面板 Nginx 服务管理命令
2024-11-18 17:26:26 +0800 CST
Elasticsearch 聚合和分析
2024-11-19 06:44:08 +0800 CST
Go 中的单例模式
2024-11-17 21:23:29 +0800 CST
一个简单的html卡片元素代码
2024-11-18 18:14:27 +0800 CST
Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
rangeSlider进度条滑块
2024-11-19 06:49:50 +0800 CST
Flet 构建跨平台应用的 Python 框架
2025-03-21 08:40:53 +0800 CST
10个几乎无人使用的罕见HTML标签
2024-11-18 21:44:46 +0800 CST
JavaScript设计模式:组合模式
2024-11-18 11:14:46 +0800 CST
Elasticsearch 条件查询
2024-11-19 06:50:24 +0800 CST
程序员茄子在线接单