Gorm 如何实现 创建并克隆一个新的 DB 对象 源码解读

50 min read

在 GORM 中,每个链式方法都会调用 clone 函数来创建并克隆一个新的 DB 对象。这个函数的实现如下:

func (s *DB) clone() (db *DB) {
    db = &DB{
        Error:      s.Error,
        RowsAffected: s.RowsAffected,
        Statement:  &Statement{},
        Callbacks:  s.Callbacks,
        Config:     s.Config,
        Logger:     s.Logger,
    }
    db.Statement.DB = db
    db.Statement.cloneFrom(s.Statement)
    return
}

这个函数会创建一个新的 DB 对象,并复制原来 DB 对象的一些属性,比如 ErrorRowsAffectedCallbacksConfigLogger。然后它会调用 cloneFrom 函数来复制原来 DB 对象的 Statement

这样,每次调用链式方法时,都会创建一个新的 DB 对象,而不是修改原来的对象。这样可以保证在多 goroutine 的情况下,每个 goroutine 都有自己独立的 DB 对象,从而避免了数据竞争和并发问题。

Statement对象

在 GORM 中,Statement 是一个结构体,它用于存储和管理与数据库操作相关的信息。它的定义如下:

type Statement struct {
    *DB
    TableExpr      clause.Expr
    Model          interface{}
    ReflectValue   reflect.Value
    Schema         *schema.Schema
    Clauses        map[string]clause.Clause
    NamedExprs     []clause.NamedExpr
    SQL            strings.Builder
    Vars           []interface{}
    Dest           interface{}
}

Statement 结构体包含了许多字段,比如 TableExprModelReflectValueSchemaClauses 等。这些字段用于存储和管理与数据库操作相关的信息。

例如,当你调用 Where 方法时,GORM 会在 Statement.Clauses["WHERE"] 中存储 WHERE 子句的信息。当你调用 Find(&users) 方法时,GORM 会在 Statement.Dest 中存储查询结果。

多 goroutine 中如何保证并发安全

在 GORM 中,每个链式方法都会创建并克隆一个新的 DB 对象。这意味着每次调用链式方法时,都会创建一个新的 DB 对象,而不是修改原来的对象。这样可以保证在多 goroutine 的情况下,每个 goroutine 都有自己独立的 DB 对象,从而避免了数据竞争和并发问题。

例如,在下面的代码中:

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        db.Where("id = ?", i).First(&User{})
    }(i)
}
wg.Wait()

每个 goroutine 都会调用 Where 方法来创建一个新的 DB 对象。这样,每个 goroutine 都有自己独立的 DB 对象,从而避免了数据竞争和并发问题。

总之,在 GORM 中,所有链式方法都会创建并克隆一个新的 DB 对象 (共享一个连接池),以保证在多 goroutine 的情况下并发安全。

sync.WaitGroup 的用法 代码示范

sync.WaitGroup 是 Go 语言中的一个结构体,它用于等待一组 goroutine 完成。它的常用方法有 AddDoneWait

下面是一个简单的使用 sync.WaitGroup 的例子:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(i)
        }(i)
    }
    wg.Wait()
}

在这个例子中,我们创建了一个 sync.WaitGroup 对象,并在循环中启动了 5 个 goroutine。每次启动一个 goroutine 时,我们都会调用 wg.Add(1) 来增加计数器。

在每个 goroutine 中,我们都会调用 defer wg.Done() 来减少计数器。当计数器减为 0 时,wg.Wait() 方法就会返回。

这里为什么要 defer 不使用的影响

在上面的例子中,我们使用了 defer wg.Done() 来确保在 goroutine 结束时调用 wg.Done() 方法。defer 关键字用于延迟执行一个函数,直到包含它的函数返回。

如果我们不使用 defer,而是直接调用 wg.Done(),那么就有可能在 goroutine 还没有完成时就调用了 wg.Done() 方法。这样会导致计数器减少得过快,从而使得 wg.Wait() 方法过早地返回。

例如,在下面的代码中:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

我们没有使用 defer 来延迟执行 wg.Done() 方法。这样,在某些情况下,可能会出现计数器减少得过快,从而使得 wg.Wait() 方法过早地返回。