Обработка ошибок и шаблоны повторных попыток в go-mssqldb

Приложения Production Go нуждаются в структурированной обработке ошибок, чтобы различать временные сбои, которые можно повторять, и постоянные, требующие вмешательства человека. В этой статье рассматриваются классификация ошибок, шаблоны повторных попыток и стратегии отказоустойчивости для драйвера go-mssqldb.

Структура ошибок SQL Server

Когда SQL Server возвращает ошибку, go-mssqldb драйвер оборачивает её в структуруmssql.Error. Используйте утверждение типа для доступа к структурированным полям ошибок:

import (
    "database/sql"
    "errors"
    "fmt"

    mssql "github.com/microsoft/go-mssqldb"
)

func handleError(err error) {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        fmt.Printf("Number:  %d\n", mssqlErr.Number)
        fmt.Printf("State:   %d\n", mssqlErr.State)
        fmt.Printf("Class:   %d\n", mssqlErr.Class)
        fmt.Printf("Message: %s\n", mssqlErr.Message)
        fmt.Printf("Server:  %s\n", mssqlErr.ServerName)
        fmt.Printf("Proc:    %s\n", mssqlErr.ProcName)
        fmt.Printf("Line:    %d\n", mssqlErr.LineNo)
    }
}

Поля ошибок

Поле Type Description
Number int32 Номер ошибки SQL Server. Сопоставляется с sys.messages.
State uint8 Состояние ошибки. Даёт дополнительный контекст для того же числа ошибок.
Class uint8 Уровень тяжести (0-25). Степень тяжести 11-16 можно корректировать пользователем. Степень серьёзности 17+ указывает на проблемы с ресурсами или системой.
Message string Читаемый человеком текст ошибки с сервера.
ServerName string Название экземпляра SQL Server, который вызвал ошибку.
ProcName string Сохранённое имя процедуры или функции, где произошла ошибка. Пусто для случайных запросов.
LineNo int32 Номер строки в пакете Transact-SQL (T-SQL) или сохранённой процедуре.

Уровни серьезности

Диапазон серьезности Meaning Действие
0-10 Информационные сообщения Нет ошибки. Запишите, если это полезно.
11-16 Ошибки, исправляемые пользователем Исправьте запрос, параметры или права доступа.
17-19 Ошибки ресурсов Повторите попытку. Сервер может быть под нагрузкой или без ресурсов.
20-25 Фатальные ошибки Подключение нарушено. Свяжитесь снова и попробуйте снова.

Классифицировать ошибки как временные или постоянные

Временные ошибки — это временные условия, которые разрешаются сами по себе, такие как сетевые сбои, ограничение соединения или кратковременные ограничения ресурсов. Постоянные ошибки требуют изменений кода или конфигурации.

Распространённые коды временных ошибок

Используйте следующий общий каталог в качестве эталонного списка временных ошибок установления соединения и ошибок транспортного уровня на пути прохождения запроса:

Следующие ошибки являются временными, когда они происходят во время создания подключения или при отправке запроса на сервер. Повторите попытку на коротком, ограниченном обратном выходе. Ошибки, которые сохраняются после нескольких повторных попыток, обычно указывают на проблему конфигурации (неправильный сервер, отсутствующие разрешения, исчерпанную квоту), которая не исправится.

Error Message Troubleshooting
64 A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 - The specified network name is no longer available.) Tcp-подключение удаляется в середине подтверждения. Это не ошибка учетных данных. Если он сохраняется, проверьте нестабильность сети на стороне клиента или промежуточное устройство, которое удаляет половину установленных подключений.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Сбой транспорта предварительного входа или TLS. Сервер обычно возвращает его, когда он не может принимать подключение (исчерпание ресурсов, достигнуто максимальное число подключений или неподдерживаемый клиент). Это не ошибка учетных данных. Проверьте работоспособность сервера, а затем проверьте время ожидания входа клиента, параметры TLS и совместимость версий TLS клиента и сервера.
4060 Cannot open database "%.*ls" requested by the login. The login failed. Вход в систему проходит проверку подлинности, но не удаётся открыть запрошенную базу данных. К временным причинам относятся переходное состояние базы данных (переключение на резервный ресурс, восстановление, масштабирование) или ее автоматическая приостановка. Постоянные причины (база данных не существует, логин не имеет доступа) не будут устранены повторной попыткой; проверьте имя базы данных, сопоставление логина и состояние базы данных.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. Реплика недоступна для входа в систему, так как для транзакций, которые ещё выполнялись в момент пересоздания реплики, отсутствуют версии строк. Откатите или зафиксируйте активные транзакции на основном сервере, чтобы устранить проблему. Снизьте влияние этой проблемы, избегая длительных транзакций записи на основном сервере.
10053 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An established connection was aborted by the software in your host machine.) Локальная сторона прерывает подключение. Проверьте работоспособность сети на стороне клиента и любой локальный брандмауэр или VPN-клиент.
10054 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) Удаленная сторона отправляет сброс TCP. Распространенные причины: сбой однорангового процесса, брандмауэр ввел сброс или шлюз Azure SQL закрыл неактивное подключение. Для сценариев сброса простаивающих соединений включите TCP keepalive на стороне клиента или сократите тайм-аут простоя пула подключений.
10928 Resource ID: %d. The %s limit for the database is %d and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance. База данных превышает ограничение Azure SQL управления ресурсами. Идентификатор ресурса 1 указывает на лимит рабочих процессов, а идентификатор ресурса 2 — на лимит сеансов. Определите тип ограничения из сообщения, а затем уменьшите параллелизм, масштабируйте базу данных или сократите длительные операции хранения ресурса.
10929 Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d, and the current usage for the database is %d. However, the server is currently too busy to support requests greater than %d for this database. База данных превысила минимально гарантированный уровень, и лежащий в ее основе сервер ограничивает производительность. Повторная попытка обычно бывает успешной, когда нагрузка на соседний узел снижается. Постоянное повторение таких случаев означает, что вам нужен более высокий тарифный план или менее шумная среда.
40020, 40143, 40166, 40540 Сообщение в слоте Error code %d об ошибке 40197 во время переключения при отказе. Встроенные в сообщение об аварийном переключении 40197 подкоды, которые в некоторых путях появляются как основной код ошибки. Обработайте их так же, как 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Обновление программного обеспечения, сбой оборудования или другое событие переключения при отказе в Azure SQL. Повторное подключение маршрутизирует вас к работоспособной реплике. Встроенный код ошибки определяет тип переключения при отказе. Если ошибка сохраняется, запишите идентификатор трассировки сеанса и обратитесь в службу поддержки.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Ограничение производительности ядра Azure SQL. Рекомендуемая минимальная задержка перед повторной попыткой — 10 секунд. Постоянное ограничение пропускной способности указывает на то, что рабочая нагрузка превысила выделенные базе данных ресурсы; увеличьте уровень обслуживания или уменьшите степень параллелизма.
40613 Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them with the session tracing ID of '%.*ls'. База данных недоступна, как правило, во время переключения при отказе или ненадолго во время операции масштабирования. Повторите попытку с увеличивающейся задержкой; если проблема сохраняется дольше нескольких минут, запишите идентификатор трассировки сеанса и создайте обращение в службу поддержки.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. Выделенный пул SQL (Synapse) находится в приостановленном состоянии. Повторная попытка будет успешной только после возобновления пула. Возобновите пул явным образом или запланируйте рабочую нагрузку после возобновления работы пула.
42109 The SQL pool is warming up. Please try again. Выделенный пул SQL возобновляется. Повторяйте попытки с увеличивающейся задержкой, пока пул не перейдёт в состояние онлайн; прогрев обычно занимает несколько минут.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. Сервер в настоящее время не может выделить достаточно ресурсов для удовлетворения запроса. Повторите попытку на обратном выходе. Если ошибка сохраняется, увеличьте вычислительные ресурсы базы данных или эластичного пула.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Ограничение параллелизма на уровне подписки для операций управления. Уменьшите количество параллельных вызовов создания и обновления или разнесите их по времени.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Ограничение параллелизма на уровне подписки для операций в полете. Уменьшите параллелизм или дождитесь завершения выполняющихся операций.

Ошибки на уровне инструкций не входят в этот список, поскольку они возникают после установления соединения, и после такого сбоя сеанс остаётся пригодным для использования. Наиболее распространёнными ошибками инструкций, допускающих повторное выполнение, являются 1205 (жертва взаимоблокировки) и 1222 (превышение времени ожидания запроса блокировки). Повторите всю транзакцию, а не одну инструкцию сбоя.

Текст сообщения об ошибке взят из временных ошибок подключения к Azure SQL. Отдельные драйверы поддерживают собственные встроенные списки повторных попыток; в этом каталоге описываются ошибки, которые могут быть повторами в SQL Server, База данных SQL Azure, Управляемый экземпляр SQL Azure, базе данных SQL в Microsoft Fabric и выделенных пулах SQL в Azure Synapse Analytics.

Следующая isTransient функция показывает один шаблон реализации Go для классификации повторных попыток. Относитесь к общему каталогу выше как к источнику истины и поддерживайте поиск кода в соответствии с ним.

// isTransient returns true if the error is a transient SQL Server error
// that is likely to succeed on retry.
func isTransient(err error) bool {
    var mssqlErr mssql.Error
    if !errors.As(err, &mssqlErr) {
        // Network errors, context deadlines, and connection resets
        // are also transient.
        return isNetworkError(err)
    }

    if isTransientSQLNumber(mssqlErr.Number) {
        return true
    }

    // Severity 17-19 indicates resource issues that are typically transient.
    return mssqlErr.Class >= 17 && mssqlErr.Class <= 19
}

// Keep this lookup synchronized with the shared transient catalog above.
var transientSQLNumbers = map[int32]struct{}{
    64:    {}, // Transport/connection error.
    1205:  {}, // Deadlock victim.
    40197: {}, // Service error processing request.
    40501: {}, // Service is currently busy.
    40613: {}, // Database is currently unavailable.
    49918: {}, // Cannot process request: not enough resources.
    49919: {}, // Cannot process create/update request.
    49920: {}, // Cannot process request: too many operations.
}

func isTransientSQLNumber(number int32) bool {
    _, ok := transientSQLNumbers[number]
    return ok
}

Если возникают ошибки в конфигурации и квотах, исправьте базовую ёмкость, базу данных или сетевую конфигурацию перед повторной попыткой. Вот некоторые примеры:

  • 40544 (квота по размеру базы данных)
  • 4060 (не может открыть базу данных)
  • 40615 (правило межсетевого экрана)

Обнаружение ошибок сети

Ошибки на уровне сети не приводят к значениям mssql.Error. Проверьте распространённые типы ошибок в сети Go:

import (
    "context"
    "errors"
    "net"
    "io"
)

func isNetworkError(err error) bool {
    if err == nil {
        return false
    }

    // Context deadline exceeded or canceled
    if errors.Is(err, context.DeadlineExceeded) {
        return true
    }

    // Connection reset or broken pipe
    var netErr *net.OpError
    if errors.As(err, &netErr) {
        return true
    }

    // Unexpected EOF (server dropped the connection)
    if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
        return true
    }

    return false
}

Реализуйте повторную попытку с экспоненциальным отходом

Повторяйте попытки при временных ошибках, постепенно увеличивая задержки между попытками. Такой подход даёт серверу время на восстановление и избегает перегрузки быстрыми повторами.

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "log"
    "math"
    "math/rand"
    "time"

    mssql "github.com/microsoft/go-mssqldb"
)

// RetryConfig controls retry behavior.
type RetryConfig struct {
    MaxAttempts int           // Maximum number of attempts (including the first).
    BaseDelay   time.Duration // Initial delay before the first retry.
    MaxDelay    time.Duration // Upper bound on delay between retries.
}

// DefaultRetryConfig provides sensible defaults for SQL Server workloads.
var DefaultRetryConfig = RetryConfig{
    MaxAttempts: 5,
    BaseDelay:   100 * time.Millisecond,
    MaxDelay:    10 * time.Second,
}

// RetryFunc executes fn with retries for transient errors.
func RetryFunc(ctx context.Context, cfg RetryConfig, fn func(ctx context.Context) error) error {
    var lastErr error
    for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
        lastErr = fn(ctx)
        if lastErr == nil {
            return nil
        }

        if !isTransient(lastErr) {
            return lastErr // Permanent error, don't retry.
        }

        if attempt == cfg.MaxAttempts-1 {
            break // Last attempt, don't sleep.
        }

        delay := calculateDelay(attempt, cfg.BaseDelay, cfg.MaxDelay)

        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(delay):
        }
    }
    return lastErr
}

func calculateDelay(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
    // Exponential backoff: base * 2^attempt
    delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(attempt)))
    if delay > maxDelay {
        delay = maxDelay
    }
    // Add jitter: +/- 25% to avoid thundering herd
    jitter := time.Duration(rand.Int63n(int64(delay) / 2))
    return delay/2 + jitter
}

// isTransient classifies retryable SQL Server errors.
// For a fuller example, see "Classify errors as transient or permanent" earlier in this article.
func isTransient(err error) bool {
    var mssqlErr mssql.Error
    if !errors.As(err, &mssqlErr) {
        return errors.Is(err, context.DeadlineExceeded)
    }

    switch mssqlErr.Number {
    case 1205, 40197, 40501, 40613, 49918, 49919, 49920:
        return true
    }

    return mssqlErr.Class >= 17 && mssqlErr.Class <= 19
}

// getEmployeeCount wraps a query with automatic retry.
func getEmployeeCount(ctx context.Context, db *sql.DB) (int, error) {
    var count int
    err := RetryFunc(ctx, DefaultRetryConfig, func(ctx context.Context) error {
        // This query executes on every retry attempt until success or exhaustion.
        return db.QueryRowContext(ctx, "SELECT COUNT(*) FROM HumanResources.Employee").Scan(&count)
    })
    return count, err
}

// Example call site (assumes db is already initialized).
func example(ctx context.Context, db *sql.DB) {
    queryCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
    defer cancel()

    count, err := getEmployeeCount(queryCtx, db)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Employee count: %d\n", count)
}

Управление взаимоблокировками

Тупиковые блокировки (ошибка 1205) — самая распространённая временная ошибка в многопользовательских приложениях. SQL Server автоматически завершает одну из конкурирующих сессий и возвращает ошибку 1205 жертве.

Обнаружить тупик

Проверьте, является ли ошибка SQL Server тупиковой блокировкой (ошибка 1205).

func isDeadlock(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 1205
    }
    return false
}

Повторяйте транзакции после тупиков

Когда внутри транзакции возникает тупик, сервер откатывает всю транзакцию. Вам нужно повторить всю транзакцию, а не только неудачную транзакцию:

func transferInventory(ctx context.Context, db *sql.DB, productID, fromLocationID, toLocationID int, qty int) error {
    return RetryFunc(ctx, DefaultRetryConfig, func(ctx context.Context) error {
        tx, err := db.BeginTx(ctx, &sql.TxOptions{
            Isolation: sql.LevelReadCommitted,
        })
        if err != nil {
            return err
        }
        defer tx.Rollback()

        _, err = tx.ExecContext(ctx,
            "UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @lid",
            sql.Named("qty", qty),
            sql.Named("pid", productID),
            sql.Named("lid", fromLocationID))
        if err != nil {
            return err
        }

        _, err = tx.ExecContext(ctx,
            "UPDATE Production.ProductInventory SET Quantity = Quantity + @qty WHERE ProductID = @pid AND LocationID = @lid",
            sql.Named("qty", qty),
            sql.Named("pid", productID),
            sql.Named("lid", toLocationID))
        if err != nil {
            return err
        }

        return tx.Commit()
    })
}

Tip

Сократите блокировки, получая доступ к таблицам в едином порядке для всех транзакций и сохраняя короткость транзакций.

Повторная попытка — это правильный ответ в коде приложения, но повторяющиеся блокировки по одному и тому же запросу указывают на проблему проектирования. Используйте граф взаимоблокировок SQL Server (полученный с помощью Extended Events или сеанса system health), чтобы определить конфликтующие инструкции и типы блокировок. Для полного обзора анализа и предотвращения тупиков смотрите руководство по Deadlocks. Сведения о стратегиях обработки взаимоблокировок, применимых к транзакциям, см. в разделе Обработка взаимоблокировок.

Управление истощением пула соединений

Когда все соединения в пуле заняты и достигнут предел MaxOpenConns, новые вызовы блокируются до тех пор, пока соединение не станет доступным или не истечёт крайний срок контекста. Эта ситуация проявляется как медленные запросы или ошибки превышения времени ожидания контекста, а не как явные ошибки исчерпания пула.

Обнаружение давления в бассейне

Следите за статистикой пула и оповещайте о увеличении числа ожиданий.

func monitorPool(ctx context.Context, db *sql.DB) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    var lastWaitCount int64
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            stats := db.Stats()
            newWaits := stats.WaitCount - lastWaitCount
            lastWaitCount = stats.WaitCount

            if newWaits > 0 {
                log.Printf("Pool pressure: open=%d inUse=%d idle=%d newWaits=%d waitDuration=%v",
                    stats.OpenConnections, stats.InUse, stats.Idle,
                    newWaits, stats.WaitDuration)
            }
        }
    }
}

Распространенные проблемы и их решения

Симптом Причина Solution
WaitCount Постепенно увеличивается MaxOpenConns слишком низко Увеличьте MaxOpenConns в соответствии с вашим уровнем параллелизма.
InUse равен MaxOpenConns в течение длительного времени Соединения не возвращаются в пул Закройте *sql.Rows, зафиксируйте или откатите *sql.Tx, и незамедлительно закройте *sql.Conn.
OpenConnections продолжает расти Соединения утекают быстрее, чем MaxIdleConns успевает их перерабатывать Установите значения ConnMaxLifetime и ConnMaxIdleTime, чтобы ограничить срок существования соединения.
Крайний срок контекста превышен во время запросов Бассейн перенасыщен, и звонящие ждут слишком долго Увеличьте размер пула, сократите время выполнения запросов или добавьте тайм-ауты.

Обработка конкретных ошибок SQL Server

Нарушения ограничений

Нарушения уникальных и внешних ключей — это постоянные ошибки, указывающие на логическую проблему в приложении:

func isUniqueViolation(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 2627 || // Unique constraint violation
            mssqlErr.Number == 2601       // Unique index violation
    }
    return false
}

func isForeignKeyViolation(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 547 // FK constraint violation
    }
    return false
}

Паттерн Upsert с обнаружением конфликтов

Используйте оператор MERGE, чтобы атомарно вставить или обновить строку:

func upsertDepartment(ctx context.Context, db *sql.DB, id int, name, groupName string) error {
    _, err := db.ExecContext(ctx, `
        MERGE INTO HumanResources.Department AS target
        USING (SELECT @id AS DepartmentID, @name AS Name, @grp AS GroupName) AS source
        ON target.DepartmentID = source.DepartmentID
        WHEN MATCHED THEN
            UPDATE SET Name = source.Name, GroupName = source.GroupName
        WHEN NOT MATCHED THEN
            INSERT (Name, GroupName) VALUES (source.Name, source.GroupName);`,
        sql.Named("id", id),
        sql.Named("name", name),
        sql.Named("grp", groupName))
    return err
}

Ошибки разрешений

Определять распространённые коды ошибок отказа в доступе, чтобы предоставлять вызывающей стороне понятное сообщение:

func isPermissionError(err error) bool {
    var mssqlErr mssql.Error
    if errors.As(err, &mssqlErr) {
        return mssqlErr.Number == 229 ||   // SELECT permission denied
            mssqlErr.Number == 230 ||       // Column permission denied
            mssqlErr.Number == 262 ||       // CREATE permission denied
            mssqlErr.Number == 300 ||       // VIEW permission denied
            mssqlErr.Number == 15247        // User doesn't have permission
    }
    return false
}

Обрабатывать sql.ErrNoRows

sql.ErrNoRowsэто не ошибка SQL Server. QueryRowContext.Scan Метод возвращает его, когда запрос не возвращает строк. Явно обрабатывайте этот случай, чтобы отличать «не найден» от реальных ошибок:

func getEmployee(ctx context.Context, db *sql.DB, id int) (*Employee, error) {
    var emp Employee
    err := db.QueryRowContext(ctx,
        "SELECT TOP (1) BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location FROM Sales.vSalesPerson WHERE BusinessEntityID = @p1",
        sql.Named("p1", id)).Scan(&emp.Id, &emp.Name, &emp.Location)

    if errors.Is(err, sql.ErrNoRows) {
        return nil, nil // Not found, not an error.
    }
    if err != nil {
        return nil, fmt.Errorf("query employee %d: %w", id, err)
    }
    return &emp, nil
}

Оберните ошибки контекстом

Добавьте контекст к ошибкам, чтобы звонящие могли понять, где произошёл сбой:

func getEmployeesByLocation(ctx context.Context, db *sql.DB, location string) ([]Employee, error) {
    rows, err := db.QueryContext(ctx,
        "SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
        sql.Named("p1", location))
    if err != nil {
        return nil, fmt.Errorf("query employees by location %q: %w", location, err)
    }
    defer rows.Close()

    var employees []Employee
    for rows.Next() {
        var emp Employee
        if err := rows.Scan(&emp.Id, &emp.Name, &emp.Location); err != nil {
            return nil, fmt.Errorf("scan employee row: %w", err)
        }
        employees = append(employees, emp)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("iterate employee rows: %w", err)
    }
    return employees, nil
}

Использование %w сохраняет цепочку ошибок, чтобы звонящие могли использовать errors.As и errors.Is проверять исходную ошибку.

Чек-лист обработки ошибок

Area Recommendation
Утверждение типа Объявите var mssqlErr mssql.Error, затем используйте errors.As(err, &mssqlErr) для доступа к полям ошибок SQL Server.
Обнаружение переходных процессов Классифицируйте ошибки по количеству и степени тяжести, прежде чем решать, стоит ли повторять.
Логика повторных попыток Используйте экспоненциальный отбой с джиттером. Установите максимальное количество попыток и общий тайм-аут в зависимости от контекста.
Взаимоблокировки Повторите всю транзакцию, а не отдельные выписки. Снижайте вероятность взаимоблокировок, обращаясь к таблицам единообразно.
Истощение бассейна Отслеживайте db.Stats() и устанавливайте контекстные сроки для всех вызовов базы данных.
ErrNoRows Обрабатывайте sql.ErrNoRows явно для QueryRowContext. Это не ошибка сервера.
Оборачивание ошибок Используйте fmt.Errorf с %w для добавления контекста, сохраняя цепочку ошибок.
Нарушения ограничений Проверяйте коды ошибок 2627, 2601 (нарушение уникальности) и 547 (внешний ключ), чтобы корректно обрабатывать конфликты.