Padrões de tratamento de erros e retentativas com go-mssqldb

Aplicações Production Go precisam de um tratamento estruturado de erros para distinguir entre falhas transitórias que você pode tentar novamente e erros permanentes que exigem intervenção humana. Este artigo aborda a classificação de erros, padrões de repetição e estratégias de resiliência para o driver go-mssqldb.

Estrutura de erro do SQL Server

Quando o SQL Server retorna um erro, o go-mssqldb driver o envolve em uma mssql.Error struct. Use uma asserção de tipo para acessar os campos de erro estruturados:

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)
    }
}

Campos de erro

Campo Tipo Description
Number int32 Número de erro do SQL Server. Mapeia para sys.messages.
State uint8 Estado do erro. Fornece contexto adicional para o mesmo número de erro.
Class uint8 Nível de gravidade (0-25). As severidades de 11 a 16 são corrigíveis pelo usuário. Gravidade 17+ indica problemas de recursos ou do sistema.
Message string Texto de erro legível por humanos fornecido pelo servidor.
ServerName string Nome da instância do SQL Server que gerou o erro.
ProcName string Procedimento armazenado ou nome da função onde o erro ocorreu. Vazio para consultas ad hoc.
LineNo int32 Número da linha no lote ou procedimento armazenado do Transact-SQL (T-SQL).

Níveis de severidade

Intervalo de severidade Meaning Ação
0-10 Mensagens informativas Nenhum erro. Registre se for útil.
11-16 Erros corrigíveis pelo usuário Corrija a consulta, parâmetros ou permissões.
17-19 Erros nos recursos Tente novamente. O servidor pode estar sob carga ou sem recursos.
20-25 Erros fatais A conexão está interrompida. Reconecte e tente novamente.

Classificar erros como transitórios ou permanentes

Erros transitórios são condições temporárias que se resolvem sozinhas, como falhas de rede, limitação de conexão ou contenção breve de recursos. Erros permanentes exigem mudanças no código ou na configuração.

Números comuns de erro transitório

Use o seguinte catálogo compartilhado como a lista canônica de erros transitórios de estabelecimento de conexão e transporte de caminho de requisição:

Os seguintes erros podem ser transitórios quando ocorrem durante o estabelecimento da conexão ou ao enviar uma solicitação para o servidor. Tente novamente em uma retirada curta e limitada. Erros que persistem além de algumas tentativas geralmente indicam um problema de configuração, como servidor errado, permissões faltando, configurações de criptografia incompatíveis ou cota esgotada, que a tentativa não vai corrigir.

Erro Tipo de falha Message Resolução de problemas
64 Transporte durante o login 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.) A conexão TCP é interrompida durante o handshake. Não é uma falha de credenciais. Se persistir, verifique se há instabilidade de rede do lado do cliente ou um dispositivo intermediário que descarta conexões semi-estabelecidas.
233 Transporte antes do login ou TLS The client was unable to establish a connection because of an error during connection initialization process before login. O servidor geralmente retorna esse erro quando não consegue aceitar a conexão devido ao esgotamento de recursos, limite de conexão ou um cliente não suportado. Não é uma falha de credenciais. Verifique a integridade do servidor e verifique o tempo limite de logon do cliente, as configurações do TLS e a compatibilidade de versão do TLS cliente/servidor.
4060 Disponibilidade ou acesso ao banco de dados Cannot open database "%.*ls" requested by the login. The login failed. O logon é autenticado, mas não pode abrir o banco de dados solicitado. As causas transitórias incluem o fato de o banco de dados estar em transição (failover, restauração, redimensionamento) ou em pausa automática. As causas persistentes (o banco de dados não existe, o logon não tem acesso) não serão corrigidas por repetição; verifique o nome do banco de dados, o mapeamento de logon e o estado do banco de dados.
4221 Transição secundária legível Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. A réplica não está disponível para logon porque as versões de linha estão ausentes para as transações que estavam disponibilizadas em versão piloto quando a réplica foi reciclada. Reverta ou confirme as transações ativas na primária para resolver o problema. Mitigue evitando transações longas de gravação no servidor primário.
10053 Interrupção de transporte local 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.) O lado local aborta a conexão. Verifique a integridade da rede do lado do cliente e qualquer firewall local ou cliente VPN.
10054 Reinício do transporte remoto 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.) O lado remoto envia uma redefinição de TCP. Causas comuns: o processo par falhou, um firewall injetou uma redefinição ou o gateway do SQL do Azure fechou uma conexão ociosa. Para padrões de redefinição por inatividade, habilite o keepalive TCP no cliente ou reduza o tempo limite de inatividade do pool de conexões.
10060 Tempo de espera da conexão esgotado A connection attempt failed because the connected party did not properly respond after a period of time. O servidor ou um dispositivo de rede intermediário não atendia antes do tempo limite da conexão TCP. Verifique a saúde do servidor, roteamento, regras do firewall e se o host e a porta configurados são acessíveis.
10928 Limite de recursos do banco de dados Resource ID: %d. The %s limit for the database is %d and has been reached. O banco de dados excede um limite de governança de recursos SQL do Azure. A ID do recurso 1 indica o limite de trabalho; A ID do recurso 2 indica o limite da sessão. Identifique o tipo de limite na mensagem e, em seguida, reduza a concorrência, aumente a capacidade do banco de dados ou encurte as operações de longa duração que mantêm o recurso ocupado.
10929 Limitação de banco de dados 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. O banco de dados está acima de sua garantia mínima e o servidor subjacente está limitando. Normalmente, a repetição é bem-sucedida quando a carga do vizinho cai. Ocorrências sustentadas indicam que você precisa de uma camada de serviço mais alta ou um ambiente menos barulhento.
40020, 40143, 40166, 40540 Subcódigo de failover do SQL do Azure Relatado no slot Error code %d do erro 40197 durante o failover. Subcódigos incorporados em uma mensagem de failover 40197 que alguns caminhos exibem como o código de erro principal. Trate-os da mesma forma que 40197.
40197 SQL do Azure failover The service has encountered an error processing your request. Please try again. Error code %d. Uma atualização de software, uma falha de hardware ou outro evento de failover no SQL do Azure. Reconectar redireciona você para uma réplica íntegra. O código de erro inserido identifica o tipo de failover. Se o erro persistir, capture a ID de rastreamento da sessão e contate o suporte.
40501 SQL do Azure throttling The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Limitação do mecanismo do SQL do Azure. O mínimo recomendado é um intervalo de espera de 10 segundos. A limitação contínua indica que a carga de trabalho excedeu a alocação de recursos do banco de dados; aumente a camada de serviço ou reduza a concorrência.
40613 Banco de dados indisponível 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'. O banco de dados não está disponível, geralmente durante um failover ou por um breve período durante uma operação de escala. Tente novamente após um intervalo de espera; se o problema persistir por alguns minutos, capture a ID de rastreamento da sessão e abra um chamado de suporte.
42108 Pool SQL pausado Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. O pool de SQL dedicado (Synapse) está em pausa. A nova tentativa só é bem-sucedida depois que o pool é retomado. Retome o pool explicitamente ou agende a carga de trabalho a ser executada após a retomada do pool.
42109 Pool SQL retomando The SQL pool is warming up. Please try again. O pool de SQL dedicado está sendo reiniciado. Tente novamente uma retirada até que o pool fique online; a inicialização normalmente leva alguns minutos.
49918 Escassez de recursos de serviços Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. No momento, o servidor não pode alocar recursos suficientes para atender à solicitação. Tente novamente em uma retirada. Se o erro persistir, aumente a capacidade do banco de dados ou do pool elástico.
49919 Limitação de operações de gerenciamento Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Limite de concorrência no nível da assinatura para operações de gerenciamento. Reduza chamadas paralelas de criação/atualização ou escalone-as.
49920 Limitação por operação de assinatura Cannot process request. Too many operations in progress for subscription "%ld". Limite de concorrência no nível de assinatura em operações em pré-lançamento. Reduza o paralelismo ou aguarde até que as operações de pré-lançamento sejam concluídas.

Os erros no nível da instrução não estão nessa lista porque são disparados depois que a conexão é estabelecida e a falha deixa a sessão utilizável. Os erros de instrução passíveis de repetição mais comuns são 1205 (vítima de deadlock) e 1222 (tempo limite da solicitação de bloqueio). Repita a transação inteira em vez da única instrução com falha.

O texto da mensagem de erro vem de erros transitórios de conexão do SQL do Azure. Esses erros podem ser repetidos no SQL Server, no Banco de Dados SQL do Azure, no Instância Gerenciada de SQL do Azure, no Banco de Dados SQL no Microsoft Fabric e em pools SQL dedicados no Azure Synapse Analytics.

A função a seguir isTransient mostra um padrão de implementação do Go para classificação de repetição. Trate o catálogo compartilhado acima como a fonte da verdade e mantenha sua busca de código alinhada com ele.

// 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
}

Se você encontrar erros de configuração e cota, corrija a capacidade subjacente, banco de dados ou configuração da rede antes de tentar novamente. Os exemplos incluem:

  • 40544 (cota de tamanho do banco de dados)
  • 4060 (não é possível abrir o banco de dados)
  • 40615 (regra do firewall)

Detectar erros de rede

Erros em nível de rede não produzem valores mssql.Error. Verifique os tipos comuns de erro de rede do 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
}

Implementar a tentativa HTTP com a replicação exponencial

Tente novamente em caso de erros transitórios, com intervalos cada vez maiores entre as tentativas. Essa abordagem dá tempo ao servidor para se recuperar e evita sobrecarregá-lo com tentativas rápidas.

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)
}

Lidar com deadlocks

Deadlocks (erro 1205) são o erro transitório mais comum em aplicações multiusuário. O SQL Server termina automaticamente uma das sessões concorrentes e retorna o erro 1205 à vítima.

Detectar um impasse

Verifique se um erro do SQL Server é um bloqueio (erro 1205).

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

Repetir as transações após deadlocks

Quando ocorre um bloqueio dentro de uma transação, o servidor reverte toda a transação. Você deve tentar novamente a transação completa, não apenas a declaração falhada:

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()
    })
}

Dica

Reduza bloqueios acessando tabelas em ordem consistente em todas as transações e mantendo as transações curtas.

Tentar novamente é a resposta correta no código da aplicação, mas impasses repetidos na mesma consulta indicam um problema de design. Use o grafo de deadlock do SQL Server (capturado por meio de Eventos Estendidos ou da sessão de integridade do sistema) para identificar as instruções em conflito e os tipos de bloqueio. Para um guia completo de análise e prevenção de bloqueios, veja o guia Deadlocks. Para estratégias de tratamento de deadlocks específicas para transações, veja Tratamento de deadlocks.

Lidar com o esgotamento do pool de conexões

Quando todas as conexões no pool estiverem em uso e MaxOpenConns for atingido, novas chamadas ficarão bloqueadas até que uma conexão fique disponível ou o prazo do contexto se esgote. Essa situação se manifesta como requisições lentas ou erros de prazo de contexto, não como erros explícitos de esgotamento de pool.

Detectar pressão da piscina

Monitore as estatísticas do pool e alerte quando o número de espera aumentar.

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)
            }
        }
    }
}

Causas e soluções comuns

Sintoma Cause Solução
WaitCount aumenta de forma constante MaxOpenConns é muito baixo Aumente MaxOpenConns de acordo com sua concorrência.
InUse é igual a MaxOpenConns por períodos prolongados As conexões não são retornadas ao pool Feche *sql.Rows, confirme ou reverta *sql.Tx, e feche *sql.Conn imediatamente.
OpenConnections continua crescendo As conexões expiram mais rápido do que MaxIdleConns consegue reciclá-las Defina ConnMaxLifetime e ConnMaxIdleTime para limitar a idade da conexão.
Prazo de contexto ultrapassado durante consultas A piscina está saturada e os chamadores esperam demais Aumente o tamanho do pool, reduza o tempo de execução das consultas ou adicione tempos de espera para consultas.

Lidar com erros específicos do SQL Server

Violações de restrições

Violações de chave única e chave estrangeira são erros permanentes que indicam um problema lógico na aplicação:

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
}

Padrão upsert com detecção de conflito

Use a instrução MERGE para inserir ou atualizar uma linha atomicamente:

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
}

Erros de permissão

Detectar códigos de erro comuns de permissão negada para fornecer uma mensagem clara ao chamador:

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
}

Tratar sql.ErrNoRows

sql.ErrNoRowsnão é um erro do SQL Server. O método QueryRowContext.Scan o retorna quando a consulta não retorna nenhuma linha. Manuse-o explicitamente para distinguir "não encontrado" de erros reais:

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
}

Encapsule erros com contexto

Adicione contexto aos erros para que os chamadores possam entender onde ocorreu a falha:

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
}

Usar %w preserva a cadeia de erros para que os chamadores ainda possam usar errors.As e errors.Is inspecionar o erro subjacente.

Lista de verificação para o tratamento de erros

Area Recommendation
Asserção de tipo Declare var mssqlErr mssql.Error, e então use errors.As(err, &mssqlErr) para acessar campos de erro do SQL Server.
Detecção transitória Classifique os erros por número e gravidade antes de decidir se deve tentar novamente.
Lógica de nova tentativa Use a retirada exponencial com tremulação. Defina um número máximo de tentativas e um tempo limite geral pelo contexto.
Deadlocks Repita a transação inteira, não instruções individuais. Reduza a ocorrência de deadlocks acessando as tabelas de maneira consistente.
Exaustão na piscina Monitore db.Stats() e defina prazos de contexto para todas as chamadas ao banco de dados.
ErrNoRows Trate sql.ErrNoRows explicitamente para QueryRowContext. Não é um erro do servidor.
Encapsulamento de erros Use fmt.Errorf com %w para adicionar contexto, preservando a cadeia de erros.
Violações de restrições Verifique os números de erro 2627, 2601 (chave exclusiva) e 547 (chave estrangeira) para tratar conflitos adequadamente.