Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Приложения 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 (внешний ключ), чтобы корректно обрабатывать конфликты. |