Felhanteringsmönster för Go API som standardiserar typade fel, HTTP-statuskoder, request IDs och säkra meddelanden utan att läcka intern information.

När varje endpoint rapporterar fel på olika sätt slutar klienter lita på ditt API. En route returnerar { "error": "not found" }, en annan returnerar { "message": "missing" }, och en tredje skickar ren text. Även om betydelsen är nära måste klientkoden nu gissa vad som hänt.
Kostnaden syns snabbt. Team bygger sprött parsflöde och lägger till specialfall per endpoint. Omskrivningar blir riskabla eftersom klienten inte kan skilja mellan "försök igen senare" och "din input är fel". Supportärenden ökar eftersom klienten bara ser ett vagt meddelande, och ditt team kan inte enkelt matcha det mot en serverloggrad.
Ett vanligt scenario: en mobilapp anropar tre endpoints under signup. Den första returnerar HTTP 400 med en fältorienterad felkarta, den andra returnerar HTTP 500 med en stacktrace-sträng, och den tredje returnerar HTTP 200 med { "ok": false }. Applaget släpper tre olika felhanterare, och backendteamet får fortfarande rapporter som "signup misslyckas ibland" utan tydlig startpunkt.
Målet är ett förutsägbart kontrakt. Klienter ska kunna pålitligt läsa vad som hände: om det är deras fel eller ditt, om retry är meningsfullt, och ett request ID de kan klistra in i support.
Omfångsnot: detta fokuserar på JSON HTTP-API:er (inte gRPC), men samma idéer gäller överallt där du returnerar fel till andra system.
Välj ett tydligt kontrakt för fel och se till att varje endpoint följer det. "Konsekvent" betyder samma JSON-form, samma betydelse för fälten och samma beteende oavsett vilken handler som fallerar. När du gjort det slutar klienter gissa och börjar hantera fel.
Ett användbart kontrakt hjälper klienter att bestämma vad som kommer härnäst. För de flesta appar bör varje felrespons svara på tre frågor:
En praktisk uppsättning regler:
Bestäm i förväg vad som aldrig får visas i responser. Vanliga "aldrig"-saker inkluderar SQL-fragment, stacktraces, interna hostnamn, hemligheter och råa felsträngar från beroenden.
Behåll en tydlig uppdelning: ett kort användarorienterat meddelande (säkert, artigt, handlingsbart) och interna detaljer (fullt fel, stack och kontext) som hålls i loggar. Till exempel är "Kunde inte spara dina ändringar. Försök igen." säkert. "pq: duplicate key value violates unique constraint users_email_key" är det inte.
När varje endpoint följer samma kontrakt kan klienter bygga en enda felhanterare och återanvända den överallt.
Klienter kan bara hantera fel rent om varje endpoint svarar i samma form. Välj en JSON-kuvertstruktur och håll den stabil.
Ett praktiskt standardval är ett error-objekt plus en toppnivå request_id:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
},
"request_id": "req_01HV..."
}
HTTP-statusen ger den breda kategorin (400, 401, 409, 500). Den maskinläsbara error.code anger det specifika fallet som klienten kan branch:a på. Den separationen är viktig eftersom många olika problem delar samma status. En mobilapp kan till exempel visa olika UI för EMAIL_TAKEN vs WEAK_PASSWORD, även om båda är 400.
Behåll error.message som säker och begriplig för människor. Den ska hjälpa användaren att åtgärda problemet, men aldrig läcka intern information (SQL, stacktraces, leverantörsnamn, filvägar).
Valbara fält är användbara om de förblir förutsägbara:
details.fields som en karta från fält till meddelande.details.retry_after_seconds.details.docs_hint som ren text (inte en URL).För bakåtkompatibilitet, behandla error.code-värden som en del av ditt API-kontrakt. Lägg till nya koder utan att ändra gamla betydelser. Lägg bara till valfria fält och anta att klienter ignorerar fält de inte känner igen.
Felhantering blir rörig när varje handler hittar på sitt eget sätt att signalera misslyckande. Ett litet set typade fel löser det: handlers returnerar kända feltyper och ett enhetligt lager översätter dem till konsekventa responser.
Ett praktiskt startset täcker de flesta endpoints:
Nyckeln är stabilitet på toppnivå, även om grundorsaken ändras. Du kan wrappa lägre nivåers fel (SQL, nätverk, JSON-parsning) samtidigt som du returnerar samma publika typ som middleware kan upptäcka.
type NotFoundError struct {
Resource string
ID string
Err error // private cause
}
func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }
I din handler, returnera NotFoundError{Resource: "user", ID: id, Err: err} istället för att läcka sql.ErrNoRows direkt.
För att kontrollera fel, föredra errors.As för egna typer och errors.Is för sentinel-fel. Sentinel-fel (som var ErrUnauthorized = errors.New("unauthorized")) fungerar för enkla fall, men egna typer vinner när du behöver säker kontext (som vilken resurs som saknas) utan att ändra ditt publika responskontrakt.
Var strikt med vad du bifogar:
Err, stackinfo, råa SQL-fel, tokens, användardata.Den uppdelningen låter dig hjälpa klienter utan att exponera intern information.
När du har typade fel är nästa jobb tråkigt men nödvändigt: samma feltyp ska alltid ge samma HTTP-status. Klienter bygger logik kring det.
En praktisk mappning som fungerar för de flesta API:er:
| Feltyp (exempel) | Status | När man använder det |
|---|---|---|
| BadRequest (felaktig JSON, saknat query-param) | 400 | Begäran är inte giltig på ett grundläggande protokoll- eller formatlager. |
| Unauthenticated (inget/ogiltigt token) | 401 | Klienten behöver autentisera sig. |
| Forbidden (ingen behörighet) | 403 | Auth är giltig men åtkomst är inte tillåten. |
| NotFound (resurs-ID finns inte) | 404 | Den efterfrågade resursen finns inte (eller du väljer att dölja existens). |
| Conflict (unik restriktion, versionskonflikt) | 409 | Begäran är välformad men kolliderar med aktuellt tillstånd. |
| ValidationFailed (fältregler) | 422 | Formen är okej men affärsvalidering misslyckas (e-postformat, minlängd). |
| RateLimited | 429 | För många förfrågningar på kort tid. |
| Internal (okänt fel) | 500 | Bug eller oväntat fel. |
| Unavailable (beroende nere, timeout, underhåll) | 503 | Tillfälligt serverfel. |
Två distinktioner som undviker mycket förvirring:
Retry-vägledning är viktig:
Ett request ID är ett kort unikt värde som identifierar ett API-anrop end-to-end. Om klienter kan se det i varje respons blir support enkelt: "Skicka request ID" räcker ofta för att hitta exakt logg och exakt fel.
Den här vanan lönar sig för både framgång och felresponser.
Använd en tydlig regel: om klienten skickar ett request ID, behåll det. Om inte, skapa ett.
X-Request-Id).Placera request ID på tre ställen:
request_id i ditt standardformat)För batch-endpoints eller bakgrundsjobb behåll ett parent request ID. Exempel: en klient laddar upp 200 rader, 12 misslyckas i validering och du köar upp arbete. Returnera ett request_id för hela anropet och inkludera ett parent_request_id på varje jobb och varje per-item-fel. Då kan du spåra "en uppladdning" även när den sprids till många uppgifter.
Klienter behöver en klar, stabil felrespons. Dina loggar behöver den röriga sanningen. Håll dessa två världar separata: returnera ett säkert meddelande och en publik felkod till klienten, medan du loggar intern orsak, stack och kontext på servern.
Logga en strukturerad händelse för varje felrespons, sökbar via request_id.
Fält som är värda att hålla konsekventa:
Spara interna detaljer endast i serverloggar (eller en intern felstore). Klienten ska aldrig se råa databasfel, frågetext, stacktraces eller leverantörsmeddelanden. Om du kör flera tjänster kan ett internt fält som source (api, db, auth, upstream) snabba upp triage.
Bevaka bullriga endpoints och rate-limited fel. Om en endpoint kan producera samma 429 eller 400 tusentals gånger per minut, undvik loggspam: sampelrepetitioner eller sänk allvarlighetsgrad för förväntade fel samtidigt som du räknar dem i metrics.
Metrics fångar problem tidigare än loggar. Spåra räkningar grupperade efter HTTP-status och felkod, och larma på plötsliga toppar. Om RATE_LIMITED hoppar 10x efter en deploy ser du det snabbt även om loggarna sampelas.
Det enklaste sättet att göra fel konsekventa är att sluta hantera dem "överallt" och leda dem genom en liten pipeline. Den pipeline bestämmer vad klienten ser och vad du sparar i loggar.
Börja med ett litet set felkoder som klienter kan lita på (till exempel: INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL). Wrappa dem i ett typat fel som exponerar endast säkra, publika fält (code, safe message, valbara detaljer som vilket fält som är fel). Behåll interna orsaker privata.
Implementera sedan en översättarfunktion som konverterar vilket fel som helst till (statusCode, responseBody). Här mappar typade fel till HTTP-statuskoder och okända fel blir ett säkert 500-svar.
Lägg därefter till middleware som:
request_idEn panic ska aldrig dumpa stacktraces till klienten. Returnera en normal 500-respons med ett generiskt meddelande och logga hela paniken med samma request_id.
Ändra slutligen dina handlers så att de returnerar ett error istället för att skriva responsen direkt. En wrapper kan kalla handler:n, köra översättaren och skriva JSON i standardformatet.
En kompakt checklista:
Golden-tester är viktiga eftersom de låser kontraktet. Om någon senare ändrar ett meddelande eller en statuskod misslyckas tester innan klienter överraskas.
Föreställ dig en endpoint: en klient skapar en kundpost.
POST /v1/customers med JSON som { "email": "[email protected]", "name": "Pat" }. Servern returnerar alltid samma felform och inkluderar alltid en request_id.
E-posten saknas eller är felaktigt formaterad. Klienten kan markera fältet.
{
"request_id": "req_01HV9N2K6Q7A3W1J9K8B",
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields need attention.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
}
}
E-posten finns redan. Klienten kan föreslå inloggning eller välja en annan e-post.
{
"request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
"error": {
"code": "ALREADY_EXISTS",
"message": "A customer with this email already exists."
}
}
Ett beroende är nere. Klienten kan försöka igen med backoff och visa ett lugnt meddelande.
{
"request_id": "req_01HV9N3X8P2J7T4N6C1D",
"error": {
"code": "TEMPORARILY_UNAVAILABLE",
"message": "We could not save your request right now. Please try again."
}
}
Med ett kontrakt reagerar klienten konsekvent:
details.fieldsrequest_id som ett support-IDFör support är samma request_id den snabbaste vägen till den verkliga orsaken i interna loggar, utan att exponera stacktraces eller databaserrors.
Det snabbaste sättet att irritera API-klienter är att få dem att gissa. Om en endpoint returnerar { "error": "..." } och en annan returnerar { "message": "..." } förvandlas varje klient till en hög specialfall och buggar göms i veckor.
Några misstag som återkommer:
code som klienter kan bygga logik på.request_id endast vid fel, så du inte kan korrelera en användarrapport med det lyckade anrop som utlöst en senare händelse.Att läcka intern info är den enklaste fallgropen. En handler returnerar err.Error() för enkelhetens skull och plötsligt finns constraint-namn eller tredjepartsmeddelanden i produktionssvaren. Håll klientmeddelandet säkert och kort, och lägg detaljerna i loggar.
Att förlita sig på text är en annan långsam brand. Om klienten måste parsa engelska meningar som "email already exists" kan du inte ändra formuleringen utan att bryta logik. Stabil felkoder låter dig justera meddelanden, översätta dem och behålla beteendet.
Behandla felkoder som en del av ditt publika kontrakt. Om du måste ändra en, lägg till en ny kod och behåll den gamla fungerande en tid, även om båda mappar till samma HTTP-status.
Slutligen, inkludera samma request_id-fält i varje respons, framgång eller fel. När en användare säger "det fungerade, sen slutade det" sparar det där ID:t ofta en timmes gissande.
Innan release, gör en snabb genomgång för konsekvens:
error.code, error.message, request_id).VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED). Lägg till tester så handlers inte av misstag returnerar okända koder.request_id och logga den för varje request, inklusive panics och timeouts.Efter det, spot-testa några endpoints manuellt. Framkalla ett valideringsfel, en saknad post och ett oväntat fel. Om responserna ser olika ut över endpoints (fält byter, statuskoder driver iväg, meddelanden läcker) åtgärda det delade pipelinen innan du lägger till fler funktioner.
En praktisk regel: om ett meddelande skulle hjälpa en angripare eller förvirra en normal användare, hör det hemma i loggarna, inte i responsen.
Skriv ner det felkontrakt du vill att varje endpoint ska följa, även om ditt API redan är live. Ett delat kontrakt (status, stabil felkod, säkert meddelande och request_id) är det snabbaste sättet att göra fel förutsägbara för klienter.
Migrera sedan gradvis. Behåll dina nuvarande handlers, men skicka deras fel genom en mapper som översätter interna fel till ditt publika responsformat. Det förbättrar konsekvens utan stor riskfylld omskrivning och förhindrar att nya endpoints uppfinner nya format.
Behåll en liten katalog med felkoder och behandla den som en del av ditt API. När någon vill lägga till en ny kod, gör en snabb granskning: är det verkligen nytt, är namnet tydligt och mappar det till rätt HTTP-status?
Lägg till ett fåtal tester som fångar drift:
request_id.error.code finns och kommer från katalogen.error.message förblir säkert och innehåller aldrig interna detaljer.Om du bygger en Go-backend från grunden kan det hjälpa att låsa kontraktet tidigt. Till exempel inkluderar Koder.ai (koder.ai) ett planeringsläge där du kan definiera konventioner som ett felschema och en felkodskatalog i förväg, och sedan hålla handlers i linje när API:t växer.
Använd en och samma JSON-struktur för alla felresponser, över alla endpoints. Ett praktiskt standardval är en toppnivå request_id plus ett error-objekt med code, message och valbara details så klienter på ett tillförlitligt sätt kan parsas och agera.
Returnera error.message som en kort, användarsäker mening och spara den verkliga orsaken i serverloggar. Returnera aldrig råa databasfel, stacktraces, interna hostnamn eller leverantörsmeddelanden, även om det verkar hjälpsamt under utveckling.
Använd en stabil error.code för maskinell logik och låt HTTP-statusen beskriva den breda kategorin. Klienter bör grenstyra på error.code (t.ex. ALREADY_EXISTS) och använda statusen som vägledning (t.ex. 409 som konflikt).
Använd 400 när begäran inte kan tolkas pålitligt (felaktig JSON, fel typer). Använd 422 när begäran är korrekt formad men bryter mot affärsregler (ogiltigt e-postformat, för kort lösenord).
Använd 409 när input är giltig men inte kan appliceras på grund av konflikt med nuvarande tillstånd (e-post redan tagen, versionskonflikt). Använd 422 för fältvalidering där ändring av värdet löser problemet utan att serverns tillstånd behöver ändras.
Skapa ett litet set typade fel (validering, inte hittad, konflikt, obehörig, intern) och låt handlers returnera dem. Använd sedan en gemensam translator för att mappa dessa typer till statuskoder och den standardiserade JSON-responsen.
Returnera alltid en request_id i varje svar, både vid framgång och fel, och logga den på varje serverloggrad. Om en klient rapporterar ett problem räcker ofta det ID:t för att hitta den exakta felsökspaden i loggarna.
Returnera 200 endast när operationen lyckades, och använd 4xx/5xx för fel. Att dölja fel bakom 200 tvingar klienter att tolka body-fält och skapar inkonsekvent beteende över endpoints.
Avstå från att göra automatiska retries för 400, 401, 403, 404, 409 och 422 eftersom retries vanligtvis inte hjälper utan ändringar. Tillåt retry för 503, och ibland 429 efter väntetid; om ni stödjer idempotensnycklar blir retries säkrare även för POST vid tillfälliga fel.
Lås kontraktet med några “golden” tester som kontrollerar status, error.code och närvaro av request_id. Lägg till nya felkoder utan att ändra gamla betydelser, och lägg endast till valfria fält så äldre klienter fortsätter fungera.