Golang中使用Scoped Database Context是否可行或值得推荐?

Golang中使用Scoped Database Context是否可行或值得推荐? 在 ASP.NET Core 中,有一种便捷的方式来管理数据库上下文,即使用 AddDbContext()。通过在 Main 中这样配置,我可以使用以下方式创建一个作用域实例:

using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

这种方法将数据库上下文的生命周期限制在创建它的作用域内,并在使用后自动释放。

在 Go 语言中,是否有类似的方法来创建一个作用域的数据库上下文,而不必将其传递给每个函数或结构体?在 Go 应用程序中,这会被认为是好习惯还是坏习惯?


更多关于Golang中使用Scoped Database Context是否可行或值得推荐?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

根据文档

DbContext 的生命周期从实例创建时开始,到实例被释放时结束。DbContext 实例设计用于单个工作单元。这意味着 DbContext 实例的生命周期通常非常短暂。

我想我不太确定 DbContext 到底能给你带来什么好处。它能处理像超时这样的事情吗?在我大多数的 Go 项目中,我会创建一个连接池,并为诸如最大连接数等设置合理的默认值。连接池是线程安全的。PGX(我经常使用)确实有一个概念,即每个查询都有一个上下文:

err = conn.QueryRow(context.Background(), "select * from my_table").Scan(&prop)

如果你愿意,可以在这里传递请求上下文。或者为那个特定的查询设置超时。问题在于你不确定如何将连接池传递到你的处理函数中吗?如果是这种情况,通常我看到人们会创建某种“服务器”结构体(如果应用程序很大,他们通常会按领域来拆分它们!但随着应用程序规模的增长,这是一个简单的重构):

package main

import (
    "database/sql"
    "net/http"
)

type Env struct {
    DB *sql.DB
}

func main() {
    db, err := sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        log.Fatal(err)
    }
    e := &Env{DB: db}
    http.HandleFunc("/", seomeHandler)
}

func (e *Env) homeHandler(w http.ResponseWriter, r *http.Request) {
    // Use e.DB to access connection pool
}

这显然是人为编写的代码,而且那个 main 函数甚至没有监听和服务。但即便如此——这是我见过最多的实现方式。

来自 ASP.NET 的你可能会对一些事情感到烦恼,因为在 ASP.NET 中只有一种做事的方式,框架“为你处理一切”,但在 Go 中,更多的是由程序员来决定如何实现。这里有一个权衡:更少的“魔法”,你将确切地知道你的应用程序是如何工作的以及它在做什么。但一开始可能会有点不适应。

更多关于Golang中使用Scoped Database Context是否可行或值得推荐?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在 Go 中实现类似 ASP.NET Core 的 Scoped Database Context 是可行的,但需要结合 Go 的上下文(context.Context)和依赖注入模式来实现。以下是具体实现方案:

1. 使用 context.Context 传递数据库连接

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    
    _ "github.com/lib/pq"
)

type key string

const dbKey key = "database"

// 创建带数据库连接的作用域上下文
func WithDatabase(ctx context.Context, db *sql.DB) context.Context {
    return context.WithValue(ctx, dbKey, db)
}

// 从上下文中获取数据库连接
func DatabaseFromContext(ctx context.Context) (*sql.DB, error) {
    db, ok := ctx.Value(dbKey).(*sql.DB)
    if !ok {
        return nil, fmt.Errorf("database not found in context")
    }
    return db, nil
}

// 使用示例
func GetUser(ctx context.Context, userID int) error {
    db, err := DatabaseFromContext(ctx)
    if err != nil {
        return err
    }
    
    var name string
    err = db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", userID).Scan(&name)
    if err != nil {
        return err
    }
    
    fmt.Printf("User: %s\n", name)
    return nil
}

2. 使用依赖注入容器实现作用域生命周期

package main

import (
    "context"
    "database/sql"
    "sync"
    
    _ "github.com/lib/pq"
    "go.uber.org/dig"
)

type DatabaseProvider struct {
    db     *sql.DB
    mu     sync.RWMutex
    scopes map[context.Context]*sql.DB
}

func NewDatabaseProvider(dsn string) (*DatabaseProvider, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, err
    }
    
    return &DatabaseProvider{
        db:     db,
        scopes: make(map[context.Context]*sql.DB),
    }, nil
}

// 创建作用域数据库连接
func (p *DatabaseProvider) CreateScopedDB(ctx context.Context) *sql.DB {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    // 为每个作用域创建新的连接池
    scopedDB, err := sql.Open("postgres", p.dsn)
    if err != nil {
        // 处理错误
        return p.db // 回退到全局连接
    }
    
    p.scopes[ctx] = scopedDB
    
    // 设置上下文取消时关闭连接
    go func() {
        <-ctx.Done()
        p.CloseScope(ctx)
    }()
    
    return scopedDB
}

func (p *DatabaseProvider) CloseScope(ctx context.Context) {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    if db, ok := p.scopes[ctx]; ok {
        db.Close()
        delete(p.scopes, ctx)
    }
}

3. 使用 wire 或 fx 等依赖注入框架

// 使用 go.uber.org/fx 示例
package main

import (
    "context"
    "database/sql"
    "fmt"
    
    _ "github.com/lib/pq"
    "go.uber.org/fx"
)

type Database struct {
    *sql.DB
}

func NewDatabase(lc fx.Lifecycle) (*Database, error) {
    db, err := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")
    if err != nil {
        return nil, err
    }
    
    d := &Database{DB: db}
    
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            return db.PingContext(ctx)
        },
        OnStop: func(ctx context.Context) error {
            return db.Close()
        },
    })
    
    return d, nil
}

type UserService struct {
    db *Database
}

func NewUserService(db *Database) *UserService {
    return &UserService{db: db}
}

func (s *UserService) GetUser(ctx context.Context, id int) (string, error) {
    var name string
    err := s.db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name)
    return name, err
}

func main() {
    app := fx.New(
        fx.Provide(NewDatabase),
        fx.Provide(NewUserService),
        fx.Invoke(func(s *UserService) {
            // 使用服务
            name, err := s.GetUser(context.Background(), 1)
            if err != nil {
                fmt.Printf("Error: %v\n", err)
                return
            }
            fmt.Printf("User: %s\n", name)
        }),
    )
    
    app.Run()
}

4. 中间件模式实现请求作用域

package main

import (
    "context"
    "database/sql"
    "net/http"
    
    _ "github.com/lib/pq"
)

func DatabaseMiddleware(db *sql.DB) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 为每个请求创建数据库连接作用域
            ctx := r.Context()
            
            // 可以在这里开启事务或设置连接参数
            tx, err := db.BeginTx(ctx, nil)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            
            // 将事务放入上下文
            ctx = context.WithValue(ctx, "tx", tx)
            
            // 处理请求
            next.ServeHTTP(w, r.WithContext(ctx))
            
            // 请求结束后提交或回滚事务
            if err := tx.Commit(); err != nil {
                tx.Rollback()
            }
        })
    }
}

func UserHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    tx, ok := ctx.Value("tx").(*sql.Tx)
    if !ok {
        http.Error(w, "transaction not found", http.StatusInternalServerError)
        return
    }
    
    // 使用事务执行查询
    var count int
    err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&count)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Write([]byte(fmt.Sprintf("Total users: %d", count)))
}

在 Go 中,这种模式通常被认为是可接受的,特别是对于 Web 应用程序。关键优势包括:

  1. 明确的资源生命周期管理
  2. 简化函数签名
  3. 便于测试和依赖注入
  4. 与 Go 的并发模型良好配合

需要注意确保线程安全和正确处理上下文取消,避免资源泄漏。

回到顶部