Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)操作
前言
大家好,這里是白澤,這篇文章在 go-kratos 官方的 layout 項(xiàng)目的整潔架構(gòu)基礎(chǔ)上,實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)庫(kù)事務(wù)操作。

本期涉及的學(xué)習(xí)資料:
- 我的開源Golang學(xué)習(xí)倉(cāng)庫(kù):https://github.com/BaiZe1998/go-learning,這期的所有內(nèi)容匯聚成一個(gè)可運(yùn)行的 demo,
kit/transaction路徑下。 - kratos CLI 工具:
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest。 - kratos 微服務(wù)框架:https://github.com/go-kratos/kratos
- wire 依賴注入庫(kù):https://github.com/google/wire
- 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)思想:本文不多涉及,具備相關(guān)背景知識(shí)食用本文更佳。
在開始學(xué)習(xí)之前,先補(bǔ)齊一下整潔架構(gòu) & 依賴注入的前置知識(shí)。
預(yù)備知識(shí)
整潔架構(gòu)
kratos 是 Go 語(yǔ)言的一個(gè)微服務(wù)框架,github ?? 23k,https://github.com/go-kratos/kratos
該項(xiàng)目提供了 CLI 工具,允許用戶通過(guò) kratos new xxxx,新建一個(gè) xxxx 項(xiàng)目,這個(gè)項(xiàng)目將使用 kratos-layout 倉(cāng)庫(kù)的代碼結(jié)構(gòu)。
倉(cāng)庫(kù)地址:https://github.com/go-kratos/kratos-layout

kratos-layout 項(xiàng)目為用戶提供的,配合 CLI 工具生成的一個(gè)典型的 Go 項(xiàng)目布局看起來(lái)像這樣:
application |____api | |____helloworld | | |____v1 | | |____errors |____cmd | |____helloworld |____configs |____internal | |____conf | |____data | |____biz | |____service | |____server |____test |____pkg |____go.mod |____go.sum |____LICENSE |____README.md
依賴注入
?? 通過(guò)依賴注入,實(shí)現(xiàn)了資源的使用和隔離,同時(shí)避免了重復(fù)創(chuàng)建資源對(duì)象,是實(shí)現(xiàn)整潔架構(gòu)的重要一環(huán)。
kratos 的官方文檔中提到,十分建議用戶嘗試使用 wire 進(jìn)行依賴注入,整個(gè) layout 項(xiàng)目,也是基于 wire,完成了整潔架構(gòu)的搭建。
service 層,實(shí)現(xiàn) rpc 接口定義的方法,實(shí)現(xiàn)對(duì)外交互,注入了 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 作為數(shù)據(jù)訪問(wèn)的實(shí)現(xiàn)層,實(shí)現(xiàn)了上游接口,注入了數(shù)據(jù)庫(kù)實(shí)例資源。
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,作為被操作的對(duì)象。
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 優(yōu)雅事務(wù)
準(zhǔn)備
?? 項(xiàng)目獲取:強(qiáng)烈建議克隆倉(cāng)庫(kù)后實(shí)機(jī)操作。
git clone git@github.com:BaiZe1998/go-learning.git cd kit/transcation/helloworld
這個(gè)目錄基于 go-kratos CLI 工具使用 kratos new helloworld 生成,并在此基礎(chǔ)上修改,實(shí)現(xiàn)了事務(wù)支持。
運(yùn)行 demo 需要準(zhǔn)備:
- 本地?cái)?shù)據(jù)庫(kù) 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 進(jìn)行數(shù)據(jù)庫(kù)變更管理的能力(goose 也是一個(gè)開源的高 ?? 項(xiàng)目,推薦學(xué)習(xí))
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啟動(dòng)服務(wù):
go run ./cmd/helloworld/,通過(guò)config.yaml配置了 HTTP 服務(wù)監(jiān)聽 localhost:8000,GRPC 則是 localhost:9000。發(fā)起一個(gè) get 請(qǐng)求

核心邏輯
helloworld 項(xiàng)目本質(zhì)是一個(gè)打招呼服務(wù),由于 kit/transcation/helloworld 已經(jīng)是魔改后的版本,為了與默認(rèn)項(xiàng)目做對(duì)比,你可以自行生成一個(gè) helloworld 項(xiàng)目,在同級(jí)目錄下,對(duì)照學(xué)習(xí)。
在 internal/biz/greeter.go 文件中,是我更改的內(nèi)容,為了測(cè)試事務(wù),我在 biz 層的 CreateGreeter 方法中,調(diào)用了 repo 層的 Save 和 Update 兩個(gè)方法,且這兩個(gè)方法都會(huì)成功,但是 Update 方法人為拋出一個(gè)異常。
// 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 層開啟事務(wù)
如果忽略上文注釋中的內(nèi)容,因?yàn)閮蓚€(gè) repo 的數(shù)據(jù)庫(kù)操作都是獨(dú)立的。
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 都已經(jīng)成功了,且彼此不強(qiáng)關(guān)聯(lián),數(shù)據(jù)庫(kù)中會(huì)多增加一條數(shù)據(jù)。

biz 層開啟事務(wù)
因此為了 repo 層的兩個(gè)方法能夠共用一個(gè)事務(wù),應(yīng)該在 biz 層就使用 db 開啟事務(wù),且將這個(gè)事務(wù)的會(huì)話傳遞給 repo 層的方法。
?? 如何傳遞:使用 context 便成了順理成章的方案。
接下來(lái)將 internal/biz/greeter.go 文件中注釋的部分釋放,且注釋掉分開使用事務(wù)的兩行,此時(shí)重新運(yùn)行項(xiàng)目請(qǐng)求接口,則由于 Update 方法拋出 err,導(dǎo)致事務(wù)回滾,未出現(xiàn)新增的 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
}核心實(shí)現(xiàn)
由于 biz 層的 Usecase 實(shí)例持有 *DBClient,repo 層也持有 *DBClient,且二者在依賴注入的時(shí)候,代表同一個(gè)數(shù)據(jù)庫(kù)連接池實(shí)例。
在 pkg/db/db.go 中,為 *DBClient 提供了如下兩個(gè)方法: ExecTx() & DB()。
在 biz 層,通過(guò)優(yōu)先執(zhí)行 ExecTx() 方法,創(chuàng)建事務(wù),以及將待執(zhí)行的兩個(gè) repo 方法封裝在 fn 參數(shù)中,傳遞給 gorm 實(shí)例的 Transaction() 方法待執(zhí)行。
同時(shí)在 Transcation 內(nèi)部,觸發(fā) fn() 函數(shù),也就是聚合的兩個(gè) repo 操作,需要注意的是,此時(shí)將攜帶 contextTxKey 事務(wù) tx 的 ctx 作為參數(shù)傳遞給了 fn 函數(shù),因此下游的兩個(gè) repo 可以獲取到 biz 層的事務(wù)會(huì)話。
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 層執(zhí)行數(shù)據(jù)庫(kù)操作的時(shí)候,嘗試通過(guò) DB() 方法,從 ctx 中獲取到上游傳遞下來(lái)的事務(wù)會(huì)話,如果有則使用,如果沒有,則使用 repo 層自己持有的 *DBClient,進(jìn)行數(shù)據(jù)訪問(wèn)操作。
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
}參考文獻(xiàn)
到此這篇關(guān)于Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)的文章就介紹到這了,更多相關(guān)Golang實(shí)現(xiàn)事務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言中讀取命令參數(shù)的幾種方法總結(jié)
部署golang項(xiàng)目時(shí)難免要通過(guò)命令行來(lái)設(shè)置一些參數(shù),那么在golang中如何操作命令行參數(shù)呢?那么下面這篇文章就來(lái)給大家介紹了關(guān)于Go語(yǔ)言中讀取命令參數(shù)的幾種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起看看吧。2017-11-11
Go語(yǔ)言連接MySQL數(shù)據(jù)庫(kù)執(zhí)行基本的增刪改查
在后端開發(fā)中,MySQL?是最常用的關(guān)系型數(shù)據(jù)庫(kù)之一,本文主要為大家詳細(xì)介紹了如何使用?Go?連接?MySQL?數(shù)據(jù)庫(kù)并執(zhí)行基本的增刪改查吧2025-08-08
一文搞懂Go語(yǔ)言中defer關(guān)鍵字的使用
defer是golang中用的比較多的一個(gè)關(guān)鍵字,也是go面試題里經(jīng)常出現(xiàn)的問(wèn)題。今天就來(lái)整理一下關(guān)于defer的學(xué)習(xí)使用,希望對(duì)需要的朋友有所幫助2022-09-09
Go語(yǔ)言實(shí)戰(zhàn)之實(shí)現(xiàn)一個(gè)簡(jiǎn)單分布式系統(tǒng)
如今很多云原生系統(tǒng)、分布式系統(tǒng),例如?Kubernetes,都是用?Go?語(yǔ)言寫的,這是因?yàn)?Go?語(yǔ)言天然支持異步編程。本篇文章將介紹如何用?Go?語(yǔ)言編寫一個(gè)簡(jiǎn)單的分布式系統(tǒng),需要的小伙伴開業(yè)跟隨小編一起學(xué)習(xí)一下2022-10-10
Go語(yǔ)言實(shí)現(xiàn)定時(shí)器的原理及使用詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言實(shí)現(xiàn)定時(shí)器的兩種方法:一次性定時(shí)器(Timer)和周期性定時(shí)器(Ticker),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-12-12
go mock server的簡(jiǎn)易實(shí)現(xiàn)示例
這篇文章主要為大家介紹了go mock server的簡(jiǎn)易實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

