You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang数据库事务执行触发空指针Panic问题排查

Go数据库事务中sql.db.BeginTx()空指针Panic问题解决

问题现象

编写Go后端数据库事务测试代码时,运行测试触发runtime error: invalid memory address or nil pointer dereference panic,报错指向store.db.BeginTx(ctx, nil)调用行。

报错信息

Begin transaction execution
Begin transaction:  0
Execute transaction in store.go
Enter execTx function
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x1136bb3]

goroutine 5 [running]:
database/sql.(*DB).conn(0x0, {0x13861a8, 0xc00011c008}, 0x1)
    /usr/local/Cellar/go/1.20.2/libexec/src/database/sql/sql.go:1282 +0x53
database/sql.(*DB).begin(0x0?, {0x13861a8, 0xc00011c008}, 0x0?, 0x40?)
    /usr/local/Cellar/go/1.20.2/libexec/src/database/sql/sql.go:1853 +0x33
database/sql.(*DB).BeginTx.func1(0x60?)
    /usr/local/Cellar/go/1.20.2/libexec/src/database/sql/sql.go:1836 +0x45
database/sql.(*DB).retry(0x12f9e40?, 0xc000149930)
    /usr/local/Cellar/go/1.20.2/libexec/src/database/sql/sql.go:1538 +0x47
database/sql.(*DB).BeginTx(0x1384440?, {0x13861a8?, 0xc00011c008?}, 0x1?)
    /usr/local/Cellar/go/1.20.2/libexec/src/database/sql/sql.go:1835 +0x8a
github.com/simple_bank/db/sqlc.(*Store).execTx(0xc00010d4b0, {0x13861a8, 0xc00011c008}, 0xc000149a70)
    /Users/**/**/**/go_backend/udamy_bank/simple_bank/db/sqlc/store.go:24 +0xaa
github.com/simple_bank/db/sqlc.(*Store).TransferTx(_, {_, _}, {_, _, _})
    /Users/**/**/**/go_backend/udamy_bank/simple_bank/db/sqlc/store.go:60 +0x15f
github.com/simple_bank/db/sqlc.TestTransferTx.func1()
    /Users/**/**/**/go_backend/udamy_bank/simple_bank/db/sqlc/store_test.go:27 +0xa7
created by github.com/simple_bank/db/sqlc.TestTransferTx
    /Users/**/**/**/go_backend/udamy_bank/simple_bank/db/sqlc/store_test.go:26 +0x2b4
FAIL    github.com/simple_bank/db/sqlc  0.456s
FAIL

相关代码

store_test.go

func TestTransferTx(t *testing.T) {
    store := NewStore(testDB)

    account1 := createRandomAccount(t)
    account2 := createRandomAccount(t)

    n := 1
    amount := int64(10)

    errs := make(chan error)
    results := make(chan TransferTxResult)

    fmt.Println("Begin transaction execution")
    for i := 0; i < n; i++ {
        fmt.Println("Begin transaction: ", i)
        go func() {
            result, err := store.TransferTx(context.Background(), TransferTxParams{
                FromAccountID: account1.ID,
                ToAccountID:   account2.ID,
                Amount:        amount,
            })
            fmt.Println("transaction: ", i)
            errs <- err
            results <- result
        }()
    }

    for i := 0; i < n; i++ {
        err := <-errs
        require.NoError(t, err)

        result := <-results
        require.NotEmpty(t, result)

        transfer := result.Transfer
        require.NotEmpty(t, transfer)
        require.Equal(t, account1.ID, transfer.FromAccountID)
        require.Equal(t, account2.ID, transfer.ToAccountID)
        require.Equal(t, amount, transfer.Amount)
        require.NotZero(t, transfer.ID)
        require.NotZero(t, transfer.CreatedAt)

        _, err = store.GetTransfer(context.Background(), transfer.ID)
        require.NoError(t, err)

        // check entries
        fromEntry := result.FromEntry
        require.NotEmpty(t, fromEntry)
        require.Equal(t, account1.ID, fromEntry.ID)
        require.Equal(t, -amount, fromEntry.Amount)
        require.NotZero(t, fromEntry.ID)
        require.NotZero(t, fromEntry.CreatedAt)

        _, err = store.GetEntry(context.Background(), fromEntry.ID)
        require.NoError(t, err)

        toEntry := result.ToEntry
        require.NotEmpty(t, toEntry)
        require.Equal(t, account2.ID, toEntry.ID)
        require.Equal(t, amount, toEntry.Amount)
        require.NotZero(t, toEntry.ID)
        require.NotZero(t, toEntry.CreatedAt)

        _, err = store.GetEntry(context.Background(), toEntry.ID)
        require.NoError(t, err)

        // check account's balance

    }
}

store.go

type Store struct {
    *Queries
    db *sql.DB
}

func NewStore(db *sql.DB) *Store {
    return &Store{
        db:      db,
        Queries: New(db),
    }
}

func (store *Store) execTx(ctx context.Context, fn func(*Queries) error) error {
    fmt.Println("Enter execTx function")
    tx, err := store.db.BeginTx(ctx, nil)
    if err != nil {
        fmt.Println("Create Tx object fails")
        return err
    }
    fmt.Println("Create Tx object successfully")

    q := New(tx)
    err = fn(q)
    if err != nil {
        if rbErr := tx.Rollback(); rbErr != nil {
            return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
        }
        return err
    }

    return tx.Commit()
}

type TransferTxParams struct {
    FromAccountID int64 `json:"from_account_id"`
    ToAccountID   int64 `json:"to_account_id"`
    Amount        int64 `json:"amount"`
}

type TransferTxResult struct {
    Transfer    Transfer `json:"transfer"`
    FromAccount Account  `json:"from_account"`
    ToAccount   Account  `json:"to_account"`
    FromEntry   Entry    `json:"from_entry"`
    ToEntry     Entry    `json:"to_entry"`
}

func (store *Store) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) {
    var result TransferTxResult
    fmt.Println("Execute transaction in store.go")
    err := store.execTx(ctx, func(q *Queries) error {
        var err error
        fmt.Println("Execute CreateTransfer in store.go")
        result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{
            FromAccountID: arg.FromAccountID,
            ToAccountID:   arg.ToAccountID,
            Amount:        arg.Amount,
        })
        if err != nil {
            return err
        }
        fmt.Println("Execute create Transfer successfully")

        result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{
            AccountID: arg.FromAccountID,
            Amount:    -arg.Amount,
        })
        if err != nil {
            return err
        }
        fmt.Println("Execute create fromEntry successfully")

        result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{
            AccountID: arg.ToAccountID,
            Amount:    arg.Amount,
        })
        if err != nil {
            return err
        }
        fmt.Println("Execute create toEntry successfully")

        return nil
    })

    return result, err
}

main_test.go

const (
    dbDriver = "postgres"
    dbSource = "postgres://root:secret@localhost:5432/simple_bank?sslmode=disable"
)

var testQueries *Queries
var testDB *sql.DB

func TestMain(m *testing.M) {
    var err error

    testDB, err := sql.Open(dbDriver, dbSource)
    if err != nil {
        log.Fatal("cannot connect to db: ", err)
    }
    // fmt.Println("Open sql server successfully")
    testQueries = New(testDB)

    os.Exit(m.Run())
}

Queries定义

type DBTX interface {
    ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
    PrepareContext(context.Context, string) (*sql.Stmt, error)
    QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
    QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}

func New(db DBTX) *Queries {
    return &Queries{db: db}
}

type Queries struct {
    db DBTX
}

func (q *Queries) WithTx(tx *sql.Tx) *Queries {
    return &Queries{
        db: tx,
    }
}

问题根源

TestMain函数中使用testDB, err := sql.Open(...)声明变量时,:=会在当前函数作用域创建局部变量testDB,遮蔽了全局变量testDB。这导致全局testDB始终未被赋值,保持初始的nil状态。

当测试代码调用NewStore(testDB)时,传入的是nil指针,Store结构体中的db字段也为nil。后续调用store.db.BeginTx()时,对nil指针进行方法调用,触发空指针解引用panic。

修复方案

修改TestMain中的变量赋值方式,去掉:=,改用=直接给全局变量赋值,避免局部变量遮蔽:

const (
    dbDriver = "postgres"
    dbSource = "postgres://root:secret@localhost:5432/simple_bank?sslmode=disable"
)

var testQueries *Queries
var testDB *sql.DB

func TestMain(m *testing.M) {
    var err error

    // 改用=赋值,直接更新全局变量testDB
    testDB, err = sql.Open(dbDriver, dbSource)
    if err != nil {
        log.Fatal("cannot connect to db: ", err)
    }
    testQueries = New(testDB)

    os.Exit(m.Run())
}

修复原理

  • :=是Go语言的短变量声明运算符,会在当前作用域创建新变量,若存在同名全局变量,会优先使用局部变量,导致全局变量未被修改。
  • 改用=赋值时,会直接对已声明的全局testDB变量赋值,确保其持有有效的*sql.DB实例。后续NewStore能拿到非nil的数据库连接,调用BeginTx时就不会触发空指针panic。

内容的提问来源于stack exchange,提问作者Chenglong Li

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 04:22:32