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
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.
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.
Ensure that you are properly initializing the Store
instance with a valid *sql.DB
connection before calling the TransferTx
method.
Here’s a checklist to help you troubleshoot:
Initialization of Store:
Ensure that you are creating the
Store
instance with a valid
*sql.DB
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
.
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.
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.