小能豆

Failed to execute a database transaction in golang

go

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.


阅读 73

收藏
2023-12-18

共1个答案

小能豆

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:

  1. Initialization of Store:

  2. 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)

  3. Double-check that the db variable used to initialize the Store is not nil.

  4. Connection Pooling:

  5. Make sure that the database connection is properly configured and open.

  6. 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.

  7. Check Database Driver:

  8. Confirm that the database driver you are using supports transactions. Not all drivers provide transaction support.

  9. Check Database DSN:

  10. 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.

  11. Print Statements:

  12. 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.

2023-12-18