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的名字可以跟包含它的文件夹的名字不一样
参考: