examples
项目分层
- model
- repo
- 数据层,屏蔽所有数据存取的细节
- 它的底层可以是数据库,也可以是其它服务
- business
- controller
思考:
理想情况下,一个项目应该有上面几层,但又想到,其实应该按照一个项目的复杂度来决定它的分层,当项目的复杂度不高时,非要分成上述这么多层,反而会比较繁琐(毕竟,逻辑写在一个文件里最清楚了。。)。
还有一个问题值得思考,就是我们是否应该将每层都定义成接口,然后层与层之间只是接口依赖,不依赖于具体的实现?这样做,
- 好处:
- 更灵活,每一层的实现都是可随便替换的;
- 更容易写单元测试,基于接口可以很容易写一个测试专用的mock实现。
- 坏处:
- 如果不需要写单元测试(写针对接口的集成测试),上面的好处也没多大意义了
- 如果实现(比如MySQL –> PG)基本不可能换,上面的好处也没多大意义
单纯从易于测试这个问题来说,基于接口的集成测试所需的时间肯定比mock依赖的单元测试要长,但感觉目前遇到的项目的复杂度并不高,执行一次集成测试的时间也还可以接受,因此暂且仅仅使用针对接口的集成测试看看效果,后续如果感觉需要加单元测试再慢慢迭代吧,随着认知的成长,越来越认识到,任何事物(项目、个人、公司等)的发展都有一个过程,迭代是个美妙的词,我不应该苛求每次工作刚开始做就要达到一个很高的标准,应该慢慢迭代,第一期达到基本可用,第二期增加xx功能,等等等等。
参考:
repo / 数据层如何支持数据库事务?
想了好久,参考了一些别人的做法,总算有了一个相对完美的方案。关键点如下:
- 需要抽象一个接口
queryUpdater
来代表sql.DB
和sql.TX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// queryer make sql query
type queryer interface {
NamedExec(string, interface{}) (sql.Result, error)
Get(interface{}, string, ...interface{}) error
Select(interface{}, string, ...interface{}) error
}
// execer execute a sql
type execer interface {
Exec(string, ...interface{}) (sql.Result, error)
}
type queryExecer interface {
queryer
execer
}
|
- dao对象里既有底层的sql.DB也有这个抽象的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// DAO dao
type DAO struct {
db *sqlx.DB
dbConn queryExecer
}
// NewDAO new DAO
func NewDAO() *DAO {
return &DAO{
db: db,
dbConn: db,
}
}
|
- 提供一个
WithTx
wrapper函数,只要是包在这个函数里的都是在一个数据库事务里执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// WithTx wrap txFunc in a db transaction
func (dao *DAO) WithTx(txFunc func(dao *DAO)) {
tx, _ := dao.db.Beginx()
txDAO := &DAO{
dbConn: tx,
}
defer func() {
// TODO: what if err in defer?
// eg, tx.Rollback returns error
if r := recover(); r != nil {
err := fmt.Errorf("%v", r)
// err := r.(error)
txErr := tx.Rollback()
if txErr != nil {
fmt.Printf("tx rollback failed: %+v\n", txErr)
}
fmt.Println("tx rollback")
// re-panic
panic(err)
} else {
tx.Commit()
fmt.Println("tx committed")
}
}()
txFunc(txDAO)
}
|
事务的用法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
jccoinDAO := dao.NewDAO()
var topup *model.TopUp
jccoinDAO.WithTx(func(dao *dao.DAO) {
// https://stackoverflow.com/questions/31981592/assign-struct-with-another-struct
topup = &model.TopUp{
BaseTopUp: req.BaseTopUp,
PaidTotal: req.TopUpAmount,
TimeCreated: time.Now(),
Remark: req.Remark,
}
// new topup
// idempotence support
if isDup := dao.NewTopUp(c, topup); isDup {
topup = dao.GetTopUpByOrderNo(c, topup.TopUpNo, topup.Channel)
return
}
// get buyer_account, update balance
buyerAccount := model.Account{
UserID: topup.BuyerID,
AccountType: "BUYER",
DeviceType: topup.DeviceType,
SystemCode: topup.SysCode,
IsPlatformAccount: false,
AccountNumber: "",
Status: "OPEN",
Currency: "JCC",
TimeCreated: nowFunc(),
TimeUpdated: nowFunc(),
}
buyerGiftAccount := buyerAccount
buyerGiftAccount.AccountType = "BUYERGIFT"
fmt.Printf("buyerAccount: %+v\n", buyerAccount)
ba := dao.GetOrCreateBuyerAccount(c, &buyerAccount)
if !topup.TopUpAmount.IsZero() {
buyerBalance := ba.Balance.Add(topup.TopUpAmount)
dao.UpdateAccountBalance(c, ba.ID, buyerBalance)
}
// get buyer_gift_account, update balance
fmt.Printf("giftAccount: %+v\n", buyerGiftAccount)
ga := dao.GetOrCreateBuyerAccount(c, &buyerGiftAccount)
if !topup.GiftAmount.IsZero() {
giftBalance := ga.Balance.Add(topup.GiftAmount)
dao.UpdateAccountBalance(c, ga.ID, giftBalance)
}
})
|
非事务的查询就是直接在dao上查就行:
1
2
3
4
5
|
buyerBalance := jccoinDAO.GetBuyerTotalBalance(c, req.BuyerID, req.DeviceType)
if req.CoinsAmount.GreaterThan(buyerBalance) {
err = ErrInsufficientBalance
return
}
|
完美实现了代码的复用,如果不这样的话,使用事务和不使用事务时同样的查询函数要写两遍。。或者需要在参数里把sql.DB
和sql.Tx
都带上
参考:
cache / 缓存层?
利用 Proxy Pattern,cache 层也实现repo层的接口,把repo包在里面,具体参考这个回答
代码Lint
当前使用了revive,号称 drop-in replacement for golint,确实名副其实,比golint强大多了~ 支持lint指定文件(夹)、排除文件夹等,支持lint参数定制化。当前支持的lint项在这里能看到,而且它还暴露了接口让你能自定义lint项
参考:
go design patterns
Curated list of Go design patterns, recipes and idioms
go-patterns
cmd / 临时脚本?
常常有这样的需求,有一些脚本性质的任务,偶尔需要临时执行一下,这种需求在Golang项目中该怎么做呢?
参考了几个大型项目,比如 k8s,可以这么来做,此类任务统统放在cmd
目录下,这个目录下可以继续建子目录,脚本就写在子目录中,但注意脚本里声明的package
需要是main
,如果需要执行脚本的话,可以直接执行go run <脚本路径>
即可(或者把它build成一个可执行文件)
比如在项目中我有如下脚本:
1
2
3
|
cmd
└── kafka
└── kafka_consume.go
|
那么,可以直接这样来执行它:
go run cmd/kafka/kafka_consume.go
学到的
package
的名字可以跟包含它的文件夹的名字不一样
参考: