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

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

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

参考文献

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

推荐文章

go错误处理
2024-11-18 18:17:38 +0800 CST
Nginx 防盗链配置
2024-11-19 07:52:58 +0800 CST
Vue3中如何处理跨域请求?
2024-11-19 08:43:14 +0800 CST
10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
内网穿透技术详解与工具对比
2025-04-01 22:12:02 +0800 CST
Go 语言实现 API 限流的最佳实践
2024-11-19 01:51:21 +0800 CST
Rust 高性能 XML 读写库
2024-11-19 07:50:32 +0800 CST
html一份退出酒场的告知书
2024-11-18 18:14:45 +0800 CST
Linux 常用进程命令介绍
2024-11-19 05:06:44 +0800 CST
基于Flask实现后台权限管理系统
2024-11-19 09:53:09 +0800 CST
Vue3中如何进行性能优化?
2024-11-17 22:52:59 +0800 CST
程序员茄子在线接单