I am studying go backend development and coding a project on database transaction according to a tutorial course. After writing code to execute a transaction and run the test, it prompts runtime error. It seems that the go sql function sql.db.BeginTx() leads to the error after I printed logs. Here is my code in store_test.go
runtime error
sql.db.BeginTx()
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 } }
and code in 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 }
and the errors it prompts
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
According to the print log, it executes the line tx, err := store.db.BeginTx(ctx, nil) and get a panic.
tx, err := store.db.BeginTx(ctx, nil)
The issue you’re facing seems to be related to the fact that the store.db field in your Store struct is nil. The panic occurs when the code attempts to dereference a nil pointer in the store.db.BeginTx(ctx, nil) call.
store.db
Store
nil
store.db.BeginTx(ctx, nil)
Ensure that you are properly initializing the Store instance with a valid *sql.DB connection before calling the TransferTx method.
*sql.DB
TransferTx
Here’s a checklist to help you troubleshoot:
Initialization of Store:
Ensure that you are creating the
instance with a valid
connection. For example:
db, err := sql.Open("driverName", "dataSourceName") if err != nil { log.Fatal(err) } store := NewStore(db)
Double-check that the db variable used to initialize the Store is not nil.
db
Connection Pooling:
Make sure that the database connection is properly configured and open.
Verify that you are using a database driver that supports the sql.DB connection pooling mechanism. The sql.Open function should return a valid *sql.DB instance.
sql.DB
sql.Open
Check Database Driver:
Confirm that the database driver you are using supports transactions. Not all drivers provide transaction support.
Check Database DSN:
Double-check the Data Source Name (DSN) used to open the database connection. Ensure that it contains the correct information, such as the database name, user, password, and other required parameters.
Print Statements:
Add print statements or log statements in your test or main application to verify the value of the store.db field before calling the TransferTx method.
Here’s an example of how you might print the value of store.db before calling TransferTx in your test:
func TestTransferTx(t *testing.T) { store := NewStore(testDB) fmt.Printf("store.db: %v\n", store.db) // Add this line to print store.db // ... }
By checking and printing the value of store.db before using it in the TransferTx method, you can identify whether it’s nil or a valid database connection. If it’s nil, you’ll need to investigate how the store.db is being initialized in your application. If it’s a valid connection, the issue may be elsewhere in your code.