Sobes.tech
Back to questions
Senior
5

var defaultTxOptions = &sql.TxOptions{} const defaultCurrency = "RUB" func CreateUserWithDefaultCurrency(ctx context.Context, db *sql.DB, email, passwordHash string) error { tx, err := db.BeginTx(ctx, defaultTxOptions) if err != nil { return err } defer func() { if err != nil{ tx.Rollback() } }() _, err = tx.Exec("INSERT INTO users (email, password) VALUES (?, ?)", email, passwordHash) if err != nil { return err } rows, err := tx.Query("SELECT id FROM users WHERE email = ?", email) if err != nil { return err } defer rows.Close() id := 0 for rows.Next() { err = rows.Scan(&id) if err != nil { return err } } _, err = tx.Exec("INSERT INTO balances (user_id, currency, current_balance) VALUES (?, ?, 0)", id, defaultCurrency) if err != nil { return err } return tx.Commit() }

Companies where asked
Wildberries & Russ Bank

Answer from AI

sobes.tech AI

В представленном коде есть несколько проблем, связанных с управлением транзакцией и обработкой ошибок:

  1. Проблема с defer и переменной err: В defer используется переменная err, которая может измениться после объявления defer, но в замыкании будет использоваться её значение на момент объявления. Это может привести к тому, что tx.Rollback() не вызовется при ошибках.

  2. Неявное получение ID пользователя: После вставки пользователя лучше использовать LastInsertId() или RETURNING id (в зависимости от СУБД), а не делать отдельный запрос.

  3. Отсутствие обработки ошибок при tx.Commit().

Исправленный пример:

func CreateUserWithDefaultCurrency(ctx context.Context, db *sql.DB, email, passwordHash string) error {
    tx, err := db.BeginTx(ctx, defaultTxOptions)
    if err != nil {
        return err
    }

    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p)
        } else if err != nil {
            tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()

    res, err := tx.ExecContext(ctx, "INSERT INTO users (email, password) VALUES (?, ?)", email, passwordHash)
    if err != nil {
        return err
    }

    id64, err := res.LastInsertId()
    if err != nil {
        return err
    }
    id := int(id64)

    _, err = tx.ExecContext(ctx, "INSERT INTO balances (user_id, currency, current_balance) VALUES (?, ?, 0)", id, defaultCurrency)
    if err != nil {
        return err
    }

    return err
}

Таким образом, транзакция корректно откатится при ошибках, а ID пользователя получаем напрямую после вставки, что эффективнее и безопаснее.