Gestión de errores y patrones de reintentos con go-mssqldb

Las aplicaciones Production Go necesitan un manejo estructurado de errores para distinguir entre fallos transitorios que puedes repetir y errores permanentes que requieren intervención humana. Este artículo trata sobre la clasificación de errores, patrones de reintentos y estrategias de resiliencia para el go-mssqldb conductor.

Estructura de error de SQL Server

Cuando SQL Server devuelve un error, el go-mssqldb controlador lo envuelve en una mssql.Error estructura. Utiliza una asertura de tipo para acceder a los campos de error estructurados:

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 error

Campo Tipo Description
Number int32 Número de error de SQL Server. Asigna a sys.messages.
State uint8 Estado de error. Proporciona contexto adicional para el mismo número de error.
Class uint8 Nivel de gravedad (0-25). Las gravidades 11-16 son corregibles por el usuario. La gravedad 17+ indica problemas de recursos o de sistema.
Message string Texto de error del servidor comprensible para el usuario.
ServerName string Nombre de la instancia de SQL Server que generó el error.
ProcName string Procedimiento almacenado o nombre de función donde ocurrió el error. Vacío para consultas ad hoc.
LineNo int32 Número de línea en el lote Transact-SQL (T-SQL) o procedimiento almacenado.

Niveles de gravedad

Intervalo de gravedad Meaning Action
0-10 Mensajes informativos Sin error. Regístralo si resulta útil.
11-16 Errores corregibles por el usuario Arregla la consulta, los parámetros o los permisos.
17-19 Errores de recursos vuelva a intentarlo. El servidor puede estar bajo carga o sin recursos.
20-25 Errores fatales La conexión está interrumpida. Reconecta y vuelve a intentarlo.

Clasificar los errores como transitorios o permanentes

Los errores transitorios son condiciones temporales que se resuelven por sí solas, como interrupciones de red, limitación de conexión o breves contenciones de recursos. Los errores permanentes requieren cambios en el código o en la configuración.

Números de error transitorios comunes

Utilice el siguiente catálogo compartido como lista canónica de errores transitorios en el establecimiento de conexiones y en el transporte de rutas de solicitud:

Los errores siguientes son transitorios cuando se producen durante el establecimiento de la conexión o al enviar una solicitud al servidor. Vuelva a intentarlo en un retroceso corto y limitado. Los errores que persisten más allá de algunos reintentos suelen indicar un problema de configuración (servidor incorrecto, permisos que faltan, cuota agotada) que el reintento no corregirá.

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.) La conexión TCP se interrumpe a mitad del establecimiento de conexión. No es un fallo de credenciales. Si persiste, compruebe si hay inestabilidad en la red del cliente o algún dispositivo intermedio que interrumpa conexiones establecidas solo parcialmente.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Error de transporte antes del inicio de sesión o de TLS. Normalmente, el servidor lo devuelve cuando no puede aceptar la conexión (agotamiento de recursos, conexiones máximas alcanzadas o un cliente no admitido). No es un fallo de credenciales. Compruebe el estado del servidor y, a continuación, compruebe el tiempo de espera de inicio de sesión del cliente, la configuración de TLS y la compatibilidad de la versión de TLS de cliente/servidor.
4060 Cannot open database "%.*ls" requested by the login. The login failed. El inicio de sesión se autentica, pero no puede abrir la base de datos solicitada. Las causas transitorias incluyen que la base de datos está en transición (conmutación por error, restauración, escalado) o pausada automáticamente. Las causas persistentes (la base de datos no existe, el inicio de sesión no tiene acceso) no se corregirán reintentando; compruebe el nombre de la base de datos, la asociación del inicio de sesión y el estado de la base de datos.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. La réplica no está disponible para el inicio de sesión porque faltan versiones de fila para las transacciones que estaban en curso cuando se reciclaba la réplica. Revierta o confirme las transacciones activas en el servidor principal para resolver el problema. Mitíguelo evitando transacciones de escritura prolongadas en la base de datos principal.
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.) El extremo local interrumpe la conexión. Compruebe el estado de la red del lado cliente y cualquier firewall local o cliente 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.) El extremo remoto envía un reinicio de TCP. Causas comunes: el proceso remoto se bloqueó, un firewall forzó un restablecimiento de la conexión o la puerta de enlace de Azure SQL cerró una conexión inactiva. Para los patrones de reinicio por inactividad, habilite TCP keepalive en el cliente o reduzca el tiempo de espera de inactividad del pool de conexiones.
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. La base de datos supera un límite de gobernanza de recursos Azure SQL. El identificador de recurso 1 indica el límite de trabajo; El identificador de recurso 2 indica el límite de sesión. Identifique el tipo de límite a partir del mensaje y reduzca la concurrencia, amplíe la base de datos o acorte las operaciones de larga duración que mantienen el recurso.
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. La base de datos supera su garantía mínima y el servidor subyacente está limitando. Normalmente, el reintento suele tener éxito cuando disminuye la carga de los vecinos. Las apariciones sostenidas indican que necesita un nivel de servicio superior o un entorno menos ruidoso.
40020, 40143, , 40166, 40540 Se informó de la ranura Error code %d del error 40197 durante la conmutación por error. Subcódigos incrustados en un mensaje de conmutación por error 40197 que algunas rutas muestran como número de error principal. Tratarlos igual que 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Una actualización de software, un error de hardware u otro evento de conmutación por error en Azure SQL. Al volver a conectarse, se le dirige a una réplica en buen estado. El código de error integrado identifica el tipo de conmutación por error. Si el error persiste, capture el identificador de seguimiento de sesión y póngase en contacto con el soporte técnico.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Limitación del motor de Azure SQL. El tiempo de espera mínimo recomendado es de 10 segundos. El estrangulamiento sostenido indica que la carga de trabajo ha superado la asignación de recursos de la base de datos; aumente el nivel de servicio o reduzca la concurrencia.
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'. La base de datos no está disponible, normalmente durante una conmutación por error o brevemente durante una operación de escalado. Reintentar tras un tiempo de espera progresivo; si el problema persiste más de unos minutos, capturar el ID de seguimiento de la sesión y abrir un caso de soporte.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. El grupo de SQL dedicado (Synapse) está en un estado en pausa. El reintento solo tiene éxito después de que se reanude la agrupación. Reanude explícitamente el grupo o programe la carga de trabajo para que se ejecute después de reanudar el grupo.
42109 The SQL pool is warming up. Please try again. El grupo de SQL dedicado se está reactivando. Vuelva a intentarlo con un intervalo de espera creciente hasta que el pool esté en línea; el calentamiento suele tardar unos minutos.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. El servidor no puede asignar recursos suficientes actualmente para satisfacer la solicitud. Vuelva a intentarlo después de un tiempo de espera. Si el error persiste, aumente la capacidad de la base de datos o del grupo elástico.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Límite de concurrencia a nivel de suscripción en las operaciones de administración. Reduzca las llamadas de creación y actualización paralelas o escalonelas.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Límite de concurrencia a nivel de suscripción para las operaciones en ejecución. Reduzca el paralelismo o espere a que finalicen las operaciones en curso.

Los errores a nivel de instrucción no están en esta lista porque se producen después de que se haya establecido la conexión y el fallo no impide seguir usando la sesión. Los errores de instrucciones que se pueden reintentar más comunes son 1205 (víctima de interbloqueo) y 1222 (tiempo de espera de la solicitud de bloqueo). Reintente la transacción completa en lugar de la sola instrucción que falla.

El texto del mensaje de error procede de Azure SQL errores de conexión transitorios. Los controladores individuales mantienen sus propias listas integradas de reintentos; este catálogo describe qué errores son susceptibles de reintento en SQL Server, Azure SQL Database, Azure SQL Managed Instance, la base de datos SQL de Microsoft Fabric y los grupos dedicados de SQL en Azure Synapse Analytics.

La siguiente isTransient función muestra un patrón de implementación Go para la clasificación de reintentos. Trata el catálogo compartido anterior como la fuente de verdad y mantén tu búsqueda de código alineada con él.

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

Si experimentas errores de configuración y cuota, corrige la capacidad subyacente, la base de datos o la configuración de red antes de volver a intentarlo. Algunos ejemplos son:

  • 40544 (cuota de tamaño de base de datos)
  • 4060 (no puede abrir la base de datos)
  • 40615 (regla del firewall)

Detectar errores de red

Los errores a nivel de red no generan valores mssql.Error. Comprueba los tipos de error de red Go más comunes:

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
}

Implementa reintento con retroceso exponencial

Reintenta los errores transitorios con retrasos crecientes entre intentos. Este enfoque da tiempo al servidor para recuperarse y evita saturarlo con repeticiones 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)
}

Controlar interbloqueos

Los bloqueos (error 1205) son el error transitorio más común en aplicaciones multiusuario. SQL Server termina automáticamente una de las sesiones competidoras y devuelve el error 1205 a la víctima.

Detectar un bloqueo

Comprueba si un error de SQL Server es un bloqueo (error 1205).

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

Reintentar transacciones tras los bloqueos

Cuando se produce un bloqueo dentro de una transacción, el servidor reverte toda la transacción. Debes intentar de nuevo la transacción completa, no solo la declaración fallida:

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

Reduce los bloqueos accediendo a las tablas en un orden coherente en todas las transacciones y manteniendo las transacciones cortas.

Reintentar es la respuesta correcta en el código de la aplicación, pero los bloqueos repetidos en la misma consulta indican un problema de diseño. Utiliza el grafo de deadlock de SQL Server (capturado mediante Eventos Extendidos o la sesión de salud del sistema) para identificar las sentencias y tipos de bloqueo en competencia. Para una guía completa sobre el análisis y prevención de bloqueos, consulta la guía de bloqueos. Para estrategias de gestión de bloqueos específicas de transacciones, véase Gestión de bloqueos.

Gestionar el agotamiento del pool de conexiones

Cuando todas las conexiones del pool están en uso y se alcanza MaxOpenConns, las nuevas llamadas quedan bloqueadas hasta que haya una conexión disponible o venza el tiempo límite del contexto. Esta situación se manifiesta como peticiones lentas o errores de fecha límite contextual, no como errores explícitos de agotamiento de pool.

Detectar presión de la piscina

Supervisa las estadísticas del pool y genera una alerta cuando aumenten los recuentos de espera.

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 habituales y sus soluciones

Síntoma Causa Solución
WaitCount aumenta de forma constante MaxOpenConns es demasiado bajo Aumenta MaxOpenConns para ajustarlo a tu nivel de concurrencia.
InUse es igual a MaxOpenConns durante periodos prolongados Las conexiones no se devuelven a la piscina Cierra *sql.Rows, confirma o revierte *sql.Tx y cierra *sql.Conn de inmediato.
OpenConnections sigue creciendo Las conexiones se agotan más rápido de lo que MaxIdleConns puede reciclarlas Establezca ConnMaxLifetime y ConnMaxIdleTime para limitar la antigüedad de la conexión.
Se superó el tiempo límite del contexto al realizar consultas La piscina está saturada y los llamantes esperan demasiado Aumentar el tamaño del pool, reducir el tiempo de ejecución de consultas o añadir tiempos de espera para las consultas.

Gestionar errores específicos de SQL Server

Violaciones de restricciones

Las violaciones de clave única y clave externa son errores permanentes que indican un problema lógico en la aplicación:

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
}

Patrón Upsert con detección de conflictos

Utiliza una MERGE sentencia para insertar o actualizar una fila de forma atómica:

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
}

Errores de permisos

Detectar códigos de error comunes de “permiso denegado” para proporcionar un mensaje claro a quien realiza la llamada:

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
}

Manejar sql.ErrNoRows

sql.ErrNoRowsno es un error de SQL Server. El QueryRowContext.Scan método lo devuelve cuando la consulta no devuelve filas. Manéjalo explícitamente para distinguir "no encontrado" de los errores reales:

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
}

Envuelve los errores con contexto

Añade contexto a los errores para que los llamantes puedan entender dónde ocurrió el fallo:

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
}

El uso %w preserva la cadena de errores, de modo que los llamantes pueden seguir usando errors.As e errors.Is inspeccionar el error subyacente.

Lista de comprobación para el manejo de errores

Area Recommendation
Aserción de tipos Declara var mssqlErr mssql.Error, y luego úsate errors.As(err, &mssqlErr) para acceder a los campos de error de SQL Server.
Detección de transitorios Clasifica los errores por número y gravedad antes de decidir si intentar de nuevo.
Lógica de reintento Use retroceso exponencial con vibración. Establece un número máximo de intentos y un tiempo muerto total según el contexto.
Interbloqueos Vuelve a intentar toda la transacción, no los extractos individuales. Reduce los bloqueos accediendo a las tablas de forma constante.
Agotamiento en la piscina Monitoriza db.Stats() y establece plazos contextuales en todas las llamadas a bases de datos.
ErrNoRows Maneja sql.ErrNoRows explícitamente para QueryRowContext. No es un error del servidor.
Encapsulación de errores Usa fmt.Errorf con %w para añadir contexto sin perder la cadena de errores.
Violaciones de restricciones Comprueba los números de error 2627, 2601 (único) y 547 (clave extranjera) para gestionar los conflictos con elegancia.