Fehlerbehandlung und Wiederholungsmuster mit go-mssqldb

Production Go-Anwendungen benötigen strukturierte Fehlerbehandlung, um zwischen vorübergehenden Fehlern, die man erneut versuchen kann, und dauerhaften Fehlern, die menschliches Eingreifen erfordern, zu unterscheiden. Dieser Artikel behandelt Fehlerklassifikation, Wiederholungsmuster und Strategien zur Resilienz des Fahrers go-mssqldb .

SQL Server-Fehlerstruktur

Wenn SQL Server einen Fehler zurückgibt, umwickelt der Treiber go-mssqldb ihn in eine mssql.Error Struktur. Verwenden Sie eine Typ-Assertion, um auf die strukturierten Fehlerfelder zuzugreifen:

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

Fehlerfelder

Feld Typ Description
Number int32 SQL Server-Fehlernummer. Wird sys.messages zugeordnet.
State uint8 Fehlerstatus. Bietet zusätzlichen Kontext für dieselbe Fehlernummer.
Class uint8 Schweregrad (0-25). Die Schweregrade 11-16 sind benutzerkorrigierbar. Schweregrad 17+ zeigt Ressourcen- oder Systemprobleme an.
Message string Von Menschen lesbarer Fehlertext vom Server.
ServerName string Name der SQL Server-Instanz, die den Fehler ausgelöst hat.
ProcName string Gespeicherte Prozedur oder Funktionsname, an der der Fehler aufgetreten ist. Leer für Ad-hoc-Anfragen.
LineNo int32 Zeilennummer im Transact-SQL-(T-SQL-)Batch oder in der gespeicherten Prozedur.

Schweregrade

Schweregradbereich Meaning Action
0-10 Informationsmeldungen Kein Fehler. Protokolliere, wenn es nützlich ist.
11-16 Vom Benutzer korrigierbare Fehler Beheben Sie die Abfrage, die Parameter oder die Berechtigungen.
17-19 Ressourcenfehler Wiederholen. Der Server könnte überlastet sein oder keine Ressourcen mehr haben.
20-25 Schwerwiegende Fehler Die Verbindung ist unterbrochen. Verbinden Sie sich wieder und versuchen Sie es erneut.

Fehler als vorübergehend oder dauerhaft klassifizieren

Transiente Fehler sind temporäre Zustände, die sich von selbst lösen, wie Netzwerkstörungen, Verbindungsdrosselung oder kurzfristige Ressourcenbeschränkungen. Dauerhafte Fehler erfordern Code- oder Konfigurationsänderungen.

Häufige Transientenfehlerzahlen

Verwenden Sie den folgenden gemeinsamen Katalog als kanonische Liste von transienten Verbindungs- und Anfragepfad-Transportfehlern:

Die folgenden Fehler sind vorübergehend, wenn sie während der Verbindungseinrichtung oder beim Senden einer Anforderung an den Server auftreten. Versuchen Sie es mit einem kurzen, begrenzten Backoff. Fehler, die über einige Wiederholungen bestehen, weisen in der Regel auf ein Konfigurationsproblem hin (falscher Server, fehlende Berechtigungen, erschöpftes Kontingent), das nicht behoben werden kann.

Fehler Nachricht 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.) Die TCP-Verbindung bricht während des Handshakes ab. Kein Anmeldefehler. Wenn sie weiterhin besteht, suchen Sie nach clientseitiger Netzwerkinstabilität oder einem Zwischengerät, das halb etablierte Verbindungen abbricht.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Transport- oder TLS-Fehler vor der Anmeldung. Der Server gibt sie häufig zurück, wenn sie die Verbindung nicht akzeptieren kann (Ressourcenausschöpfung, max. Verbindungen erreicht oder ein nicht unterstützter Client). Kein Anmeldefehler. Überprüfen Sie den Serverzustand und prüfen Sie dann die Anmeldezeitüberschreitung des Clients, die TLS-Einstellungen und die TLS-Version-Kompatibilität zwischen Client und Server.
4060 Cannot open database "%.*ls" requested by the login. The login failed. Die Anmeldung authentifiziert sich, kann die angeforderte Datenbank aber nicht öffnen. Vorübergehende Ursachen sind unter anderem, dass sich die Datenbank in einem Übergangszustand befindet (Failover, Wiederherstellung, Skalierung) oder automatisch angehalten wurde. Persistente Ursachen (Datenbank ist nicht vorhanden, Anmeldezugriff fehlt) werden nicht durch Wiederholung behoben. Überprüfen Sie den Datenbanknamen, die Anmeldezuordnung und den Datenbankstatus.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. Das Replikat ist für die Anmeldung nicht verfügbar, da Zeilenversionen für Transaktionen fehlen, die im Test-Flight ausgeführt wurden, als das Replikat wiederverwendet wurde. Führen Sie ein Rollback aus, oder führen Sie einen Commit für die aktiven Transaktionen auf der primären Seite durch, um das Problem zu beheben. Abhilfe schaffen Sie, indem Sie lange Schreibtransaktionen auf der Primärinstanz vermeiden.
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.) Die lokale Seite bricht die Verbindung ab. Überprüfen Sie die clientseitige Netzwerkintegrität und alle lokalen Firewalls oder VPN-Clients.
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.) Die Gegenseite sendet einen TCP-Reset. Häufige Ursachen: der Peerprozess ist abgestürzt, eine Firewall hat ein Reset-Paket gesendet, oder das Azure SQL-Gateway hat eine inaktive Verbindung geschlossen. Aktivieren Sie für Idle-Reset-Muster tcp keepalive auf dem Client, oder kürzen Sie das Leerlauftimeout für den Verbindungspool.
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. Die Datenbank überschreitet einen Grenzwert der Azure SQL-Ressourcensteuerung. Ressourcen-ID 1 gibt den Arbeitsgrenzwert an; Die Ressourcen-ID 2 gibt den Sitzungsgrenzwert an. Ermitteln Sie anhand der Meldung die Art der Begrenzung und verringern Sie dann die Parallelität, skalieren Sie die Datenbank hoch oder verkürzen Sie lang andauernde Vorgänge, die die Ressource belegen.
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. Die Datenbank überschreitet ihre Mindestgarantie, und der zugrunde liegende Server drosselt die Leistung. Der Wiederholungsvorgang ist in der Regel erfolgreich, wenn die Benachbarte Last fällt. Dauerhafte Vorkommen deuten darauf hin, dass Sie eine höhere Dienstebene oder eine weniger laute Umgebung benötigen.
40020, 40143, 40166, 40540 Gemeldet im Error code %d-Slot von Fehler 40197 während des Failovers. In eine 40197-Failovermeldung eingebettete Untercodes, die auf einigen Pfaden als Fehlernummer auf oberster Ebene erscheinen. Behandeln Sie sie genauso wie 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Ein Softwareupgrade, Hardwarefehler oder ein anderes Failoverereignis in Azure SQL. Durch erneutes Verbinden werden Sie zu einem fehlerfreien Replikat weitergeleitet. Der eingebettete Fehlercode identifiziert den Failovertyp. Wenn der Fehler weiterhin besteht, erfassen Sie die Sitzungsablaufverfolgungs-ID, und wenden Sie sich an den Support.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Drosselung der Azure SQL-Datenbank-Engine Die empfohlene Untergrenze für die Wartezeit beträgt 10 Sekunden. Eine dauerhafte Drosselung weist darauf hin, dass die Arbeitsauslastung die Ressourcenzuordnung der Datenbank überschritten hat; Skalieren Sie die Dienstebene, oder reduzieren Sie die Parallelität.
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'. Die Datenbank ist nicht verfügbar, in der Regel mitten im Failover oder kurz während eines Skalierungsvorgangs. Versuchen Sie es nach einer Wartezeit erneut; wenn das Problem nach einigen Minuten weiterhin besteht, notieren Sie die Session-Trace-ID und eröffnen Sie einen Supportfall.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. Der dedizierte SQL-Pool (Synapse) befindet sich in einem angehaltenen Zustand. Der Wiederholungsversuch ist erst erfolgreich, nachdem der Pool wiederaufgenommen wurde. Nehmen Sie den Pool explizit wieder auf, oder planen Sie die Workload so, dass sie erst nach der Wiederaufnahme des Pools ausgeführt wird.
42109 The SQL pool is warming up. Please try again. Der dedizierte SQL-Pool wird wieder aufgenommen. Wiederholen Sie einen Backoff, bis der Pool online ist. Das Aufwärmen dauert in der Regel ein paar Minuten.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. Der Server kann derzeit nicht genügend Ressourcen zuordnen, um die Anforderung zu erfüllen. Wiederholen Sie den Vorgang auf einem Backoff. Wenn der Fehler weiterhin besteht, skalieren Sie die Datenbank oder den elastischen Pool.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Parallelitätsgrenze auf Abonnementebene bei Verwaltungsvorgängen. Reduzieren Sie parallele Erstellungs-/Aktualisierungsaufrufe oder führen Sie sie gestaffelt aus.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Parallelitätsgrenzwert auf Abonnementebene für Vorgänge in Flight. Reduzieren Sie die Parallelität, oder warten Sie, bis die laufenden Vorgänge abgeschlossen sind.

Fehler auf Anweisungsebene befinden sich nicht in dieser Liste, da sie ausgelöst werden, nachdem die Verbindung hergestellt wurde und der Fehler die Sitzung nutzbar lässt. Die häufigsten Fehler bei Anweisungen, die erneut ausgeführt werden können, sind 1205 (Deadlock-Opfer) und 1222 (Zeitüberschreitung bei der Sperranforderung). Wiederholen Sie die gesamte Transaktion statt der einzelnen fehlerhaften Anweisung.

Fehlermeldungstext stammt aus Azure SQL vorübergehenden Verbindungsfehlern. Einzelne Treiber verwalten ihre eigenen integrierten Wiederholungslisten; in diesem Katalog wird beschrieben, welche Fehler in SQL Server, Azure SQL-Datenbank, Azure SQL Managed Instance, SQL-Datenbank in Microsoft Fabric und dedizierten SQL-Pools in Azure Synapse Analytics für Wiederholungen geeignet sind.

Die folgende isTransient Funktion zeigt ein Implementierungsmuster in Go für die Klassifizierung von Wiederholungsversuchen. Behandle den oben stehenden gemeinsamen Katalog als maßgebliche Quelle und richte deinen Codeabgleich danach aus.

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

Wenn du Konfigurations- und Quotenfehler bemerkst, korrigiere die zugrunde liegende Kapazität, Datenbank oder Netzwerkkonfiguration, bevor du es erneut versuchst. Beispiele sind:

  • 40544 (Datenbankgrößenquote)
  • 4060 (Datenbank kann nicht geöffnet werden)
  • 40615 (Firewall-Regel)

Netzwerkfehler erkennen

Fehler auf Netzwerkebene erzeugen keine mssql.Error Werte. Überprüfen Sie häufige Fehlertypen im Go-Netzwerk:

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
}

Retry mit exponentiellem Backoff implementieren

Versuchen Sie vorübergehende Fehler mit zunehmenden Verzögerungen zwischen den Versuchen erneut. Dieser Ansatz gibt dem Server Zeit zur Erholung und vermeidet eine Überforderung durch schnelle Rückversuche.

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

Deadlocks beheben

Deadlocks (Fehler 1205) sind der häufigste vorübergehende Fehler in Mehrbenutzeranwendungen. SQL Server beendet automatisch eine der konkurrierenden Sitzungen und gibt dem Opfer Fehler 1205 zurück.

Einen Deadlock erkennen

Überprüfen Sie, ob ein SQL Server-Fehler ein Deadlock ist (Fehler 1205).

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

Transaktionen nach einem Deadlock wiederholen

Wenn innerhalb einer Transaktion ein Deadlock auftritt, rollt der Server die gesamte Transaktion zurück. Sie müssen die vollständige Transaktion erneut versuchen, nicht nur die fehlgeschlagene Aussage:

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

Reduzieren Sie Deadlocks, indem Sie auf Tabellen in einer konsistenten Reihenfolge über alle Transaktionen hinweg zugreifen und die Transaktionen kurz halten.

Ein erneuter Versuch ist die korrekte Antwort im Anwendungscode, aber wiederholte Deadlocks bei derselben Abfrage deuten auf ein Designproblem hin. Verwenden Sie das SQL Server-Deadlockdiagramm (erfasst über Extended Events oder die Sitzung „system_health“), um die konkurrierenden Anweisungen und Sperrtypen zu identifizieren. Eine vollständige Anleitung zur Analyse und Vermeidung von Deadlocks finden Sie im Deadlocks-Leitfaden. Für Deadlock-Handlingsstrategien, die spezifisch für Transaktionen sind, siehe Deadlock-Handling.

Erschöpfung des Verbindungspools behandeln

Wenn alle Verbindungen im Pool belegt sind und MaxOpenConns erreicht ist, blockieren neue Aufrufer, bis eine Verbindung verfügbar wird oder das Zeitlimit des Kontexts abläuft. Diese Situation zeigt sich in Form von langsamen Anfragen oder Context-Deadline-Fehlern, nicht als explizite Fehler wegen Erschöpfung des Pools.

Pooldruck erkennen

Überwachen Sie die Pool-Statistiken und benachrichtigen Sie, wenn die Wartezahlen steigen.

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

Häufige Ursachen und Lösungen

Symptom Ursache Lösung
WaitCount nimmt stetig an MaxOpenConns ist zu niedrig Erhöhen Sie MaxOpenConns, damit es Ihrer Parallelität entspricht.
InUse entspricht MaxOpenConns über längere Zeiträume Verbindungen werden nicht an den Pool zurückgegeben Schließen Sie *sql.Rows, übernehmen oder setzen Sie *sql.Tx zurück und schließen Sie *sql.Conn umgehend.
OpenConnections wächst weiter Verbindungen gehen schneller verloren, als MaxIdleConns sie recyceln kann Legen Sie ConnMaxLifetime und ConnMaxIdleTime fest, um das maximale Alter der Verbindung zu begrenzen.
Kontext-Frist während der Abfragen überschritten Der Pool ist überlastet und Anrufer warten zu lange Erhöhe die Poolgröße, verkürze die Abfrageausführungszeit oder füge Abfrage-Timeouts hinzu.

Handhabung spezifischer SQL Server-Fehler

Verstöße gegen Einschränkungen

Verstöße gegen eindeutige Schlüssel und Fremdschlüssel sind permanente Fehler, die auf ein logisches Problem in der Anwendung hinweisen:

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-Muster mit Konflikterkennung

Verwenden Sie eine MERGE Anweisung, um eine Zeile atomar einzufügen oder zu aktualisieren:

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
}

Berechtigungsfehler

Erkennen Sie häufige Fehlernummern mit verweigerten Berechtigungen, um den Anrufern eine klare Nachricht zu geben:

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
}

Kümmere dich um SQL. ErrNoRows

sql.ErrNoRowsist kein SQL Server-Fehler. Die Methode QueryRowContext.Scan gibt sie zurück, wenn die Abfrage keine Zeilen zurückgibt. Behandle es explizit, um "nicht gefunden" von tatsächlichen Fehlern zu unterscheiden:

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
}

Fehler mit Kontext umwickeln

Fügen Sie den Fehlern Kontext hinzu, damit die Anrufer verstehen, wo der Fehler aufgetreten ist:

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
}

Die Verwendung von %w bewahrt die Fehlerkette, sodass Aufrufer weiterhin errors.As und errors.Is verwenden können, um den zugrunde liegenden Fehler zu untersuchen.

Fehlerbehandlungs-Checkliste

Area Recommendation
Typzusicherung Deklarieren var mssqlErr mssql.Errorund dann verwendenerrors.As(err, &mssqlErr), um auf SQL Server-Fehlerfelder zuzugreifen.
Transientendetektion Klassifizieren Sie Fehler nach Anzahl und Schwere der Fehler, bevor Sie entscheiden, ob Sie es erneut versuchen möchten.
Wiederholungslogik Verwenden Sie exponentielles Backoff mit Jitter. Legen Sie über den Kontext eine maximale Anzahl von Versuchen und ein gesamtes Zeitlimit fest.
Deadlocks Versuchen Sie die gesamte Transaktion erneut, nicht einzelne Kontoauszüge. Reduziere Deadlocks, indem du konsistent auf Tabellen zugreifst.
Erschöpfung des Pools Überwachen Sie db.Stats() und setzen Sie für alle Datenbankaufrufe Kontextfristen.
ErrNoRows Behandeln Sie sql.ErrNoRows ausdrücklich für QueryRowContext. Es ist kein Serverfehler.
Fehlerumwicklung Verwenden Sie fmt.Errorf mit %w, um Kontext hinzuzufügen und dabei die Fehlerkette beizubehalten.
Verstöße gegen Einschränkungen Überprüfen Sie die Fehlernummern 2627, 2601 (eindeutig) und 547 (Fremdschlüssel), um Konflikte elegant zu handhaben.