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
相关产品推荐
相关产品推荐

