go-mssqldb에서의 오류 처리 및 재시도 패턴

프로덕션 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)
    }
}

오류 필드

Field 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 심각한 오류 연결이 끊어졌습니다. 다시 연결하고 시도하세요.

오류를 일시적 오류와 영구적 오류로 분류하세요

일시적 오류는 네트워크 블립, 연결 제한, 짧은 자원 경쟁과 같이 스스로 해결되는 일시적인 상태입니다. 영구 오류는 코드나 구성 변경이 필요합니다.

일반적인 일시적 오류 번호

다음 공유 카탈로그를 일시적 연결 설정 및 요청 경로 전송 오류의 표준 목록으로 사용하세요:

다음 오류는 연결 설정 중 또는 서버에 요청을 보내는 동안 발생하는 경우 일시적입니다. 짧고 제한된 백오프에서 다시 시도합니다. 몇 번의 재시도 후에도 지속되는 오류는 일반적으로 다시 시도해도 해결되지 않는 구성 문제(잘못된 서버, 사용 권한 누락, 할당량 사용)를 나타냅니다.

오류 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 리소스 거버넌스 제한을 초과합니다. 리소스 ID 1은 작업자 제한을 나타냅니다. 리소스 ID 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 장애 조치 중 오류 40197의 Error code %d 슬롯에서 보고되었습니다. 일부 경로에서는 최상위 오류 번호로 표시되는 40197 장애 조치 메시지에 포함된 하위 코드입니다. 40197과 동일하게 처리합니다.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Azure SQL에서의 소프트웨어 업그레이드, 하드웨어 장애 또는 기타 장애 조치 이벤트. 다시 연결하면 정상 복제본으로 라우팅됩니다. 포함된 오류 코드는 장애 조치 유형을 식별합니다. 오류가 지속되면 세션 추적 ID를 캡처하고 지원에 문의하세요.
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'. 데이터베이스는 일반적으로 장애 조치 중이거나 크기 조정 작업 중에 잠시 사용할 수 없습니다. 백오프에서 다시 시도; 몇 분 이상 지속되면 세션 추적 ID를 캡처하고 지원 사례를 엽니다.
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, Azure SQL Database, Azure SQL Managed Instance, Microsoft Fabric의 SQL 데이터베이스 및 Azure Synapse Analytics의 전용 SQL 풀에서 어떤 오류가 재시도 대상이 되는지 설명합니다.

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

팁 (조언)

모든 트랜잭션에서 테이블에 일정한 순서로 접근하고 트랜잭션을 짧게 하여 교착 상태를 줄이세요.

애플리케이션 코드에서는 재시도가 올바른 응답이지만, 같은 쿼리에서 반복되는 교착 상태는 설계 문제를 나타냅니다. SQL Server 교착 그래프(확장 이벤트나 시스템 건강 세션을 통해 캡처됨)를 사용하여 경쟁하는 문장과 잠금 유형을 식별하세요. 교착 상태 분석 및 예방에 대한 자세한 안내는 교착 상태 가이드를 참조하세요. 거래에 특화된 교착 상태 처리 전략에 대해서는 교착 상태 처리를 참조하세요.

연결 풀 고갈을 처리하세요

풀 내 모든 연결이 사용 중이고 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)
            }
        }
    }
}

일반적인 원인 및 해결 방법

증상 원인 해결 방법
WaitCount 점차 증가 MaxOpenConns 너무 낮다 MaxOpenConns을(를) 동시성에 맞게 늘리세요.
InUse는 장기간 MaxOpenConns와 같습니다 연결이 풀로 반환되지 않습니다 *sql.Rows을(를) 닫고, *sql.Tx을(를) 커밋 또는 롤백한 후, *sql.Conn을(를) 즉시 닫으세요.
OpenConnections 계속 성장한다 연결은 MaxIdleConns가 재활용할 수 있는 속도보다 더 빨리 새어 나갑니다. ConnMaxLifetimeConnMaxIdleTime을 연결 유지 기간 제한으로 설정합니다.
쿼리 중 맥락 마감 시간 초과 풀은 포화 상태이고 전화 요청자가 너무 오래 기다리는 경우가 많습니다 풀 크기를 늘리거나, 쿼리 실행 시간을 줄이거나, 쿼리 타임아웃을 추가하세요.

특정 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
}

충돌 감지를 지원하는 업서트 패턴

행을 원자적으로 삽입하거나 업데이트하려면 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.ErrNoRowsSQL 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.Aserrors.Is를 사용하여 근본 원인 오류를 검사할 수 있습니다.

오류 처리 체크리스트

Area 권장 사항
형식 어설션 var mssqlErr mssql.Error를 선언한 다음 SQL Server 오류 필드에 액세스하려면 errors.As(err, &mssqlErr)를 사용하세요.
과도 감지 재시도 여부를 결정하기 전에 오류를 수와 심각도별로 분류하세요.
재시도 논리 지터와 함께 지수 백오프를 사용합니다. 최대 시도 횟수와 전체 타임아웃을 상황에 따라 설정하세요.
교착 상태 개별 명세서가 아니라 거래 전체를 다시 시도하세요. 테이블에 일관되게 접근하여 교착 상태를 줄이세요.
풀 고갈 모든 데이터베이스 호출에 대해 컨텍스트 마감일을 모니터링하고 설정하세요 db.Stats() .
에르노로우 QueryRowContext에 대해 sql.ErrNoRows를 명시적으로 처리합니다. 서버 오류가 아닙니다.
오류 감싸기 오류 체인을 유지하면서 컨텍스트를 추가하려면 fmt.Errorf을(를) %w와(과) 함께 사용하세요.
제약 위반 충돌을 원활하게 처리하려면 오류 번호 2627, 2601(고유), 547(외래키)을 확인하세요.