Errors a Go: per què no hi ha excepcions i com escriure codi clar
Gestió d'errors a Go: per què són explícits, com usar wrapping, errors centinella i personalitzats. Sense excepcions, amb claredat.

La primera vegada que vaig veure if err != nil repetit vint vegades en un fitxer, vaig pensar que la gestió d’errors a Go era una bogeria. Venia de Java i Kotlin, on un try-catch t’ho solucionava tot, i de Python, on les excepcions volen per l’aire i ja les agafaràs en algun middleware. Veure que Go t’obligava a comprovar cada error, a cada línia, em semblava un retrocés de vint anys.
Ara penso que és una de les millors decisions de disseny del llenguatge.
No perquè sigui elegant. No ho és. Sinó perquè et força a fer visible el flux real del programa. Cada punt on alguna cosa pot fallar és allà, davant teu, sense amagar-se darrere d’una pila de crides que ningú va inspeccionar. I quan debugues un problema en producció a les tres de la matinada, aquesta visibilitat val més que tota l’elegància del món.
Aquest article va d’això: de com funciona la gestió d’errors a Go, per què és així i com escriure codi que sigui clar sense tornar-te boig amb la verbositat.
Per què Go no té excepcions
L’absència d’excepcions a Go no és un descuit. És una decisió deliberada i documentada. Els creadors del llenguatge (Rob Pike, Ken Thompson, Robert Griesemer) van considerar que les excepcions creen fluxos de control ocults que fan el codi més difícil de raonar.
A Java o Python, quan una funció llança una excepció, aquesta excepció pot propagar-se a través de deu nivells de la pila de crides fins que algú la capturi. O ningú la capturi i el programa peti. El problema és que entre el punt de llançament i el punt de captura, hi ha codi que no sap que alguna cosa ha fallat. Recursos que no s’alliberen, estats que queden inconsistents, transaccions que queden a mitges.
Go pren una posició radical: si alguna cosa pot fallar, la funció que la crida ho ha de saber immediatament. No hi ha propagació implícita. No hi ha throws a la signatura. No hi ha blocs try-catch que embolcallen quinze línies de codi heterogeni. Hi ha un valor de retorn que diu “això ha fallat” i tu decideixes què fer-ne.
file, err := os.Open(\"config.yaml\")
if err != nil {
// Aquí decideixes: retornes l'error? Fas servir un valor per defecte? El logeges?
return fmt.Errorf(\"no s'ha pogut obrir la configuració: %w\", err)
}
defer file.Close()Això és verbós. Absolutament. Però té una propietat que les excepcions no tenen: el flux d’error és local. No cal rastrejar la pila de crides per saber què passa quan os.Open falla. És allà, a les tres línies següents.
Si véns d’un llenguatge amb excepcions, això et costarà al principi. És normal. Però et demano que li donis una oportunitat real abans de descartar-ho. A Effective Go explicat parlo més sobre la filosofia general del llenguatge, i la gestió d’errors és potser on es nota més.
La interfície error: simplicitat per disseny
A Go, un error no és una classe especial, ni un tipus màgic del runtime. És una interfície amb un sol mètode:
type error interface {
Error() string
}Qualsevol tipus que implementi el mètode Error() string és un error. Això és tot. No hi ha jerarquies d’excepcions, no hi ha Throwable, no hi ha BaseException. Un error és qualsevol cosa que pugui descriure’s a si mateixa com a text.
Això té conseqüències importants. La primera és que pots crear errors trivials amb errors.New o fmt.Errorf:
import \"errors\"
var ErrNotFound = errors.New(\"recurs no trobat\")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf(\"ID invàlid: %d\", id)
}
// ...
}La segona és que pots crear tipus d’error complexos quan els necessitis, amb camps addicionals, context estructurat i el que calgui. Però no estàs obligat a fer-ho. La interfície és tan mínima que la barrera d’entrada per crear i gestionar errors és pràcticament zero.
Això encaixa amb la filosofia de Go: les abstraccions simples que composen bé són més valuoses que les abstraccions complexes que cobreixen tots els casos.
El patró bàsic: if err != nil
Aquest és el patró que escriuràs cent vegades al dia a Go. I no és una exageració:
result, err := doSomething()
if err != nil {
return err
}
// Usar result amb la certesa que no hi ha errorLa funció retorna dos valors: el resultat i un error. Si l’error no és nil, alguna cosa ha fallat. Si és nil, pots usar el resultat amb confiança.
El que sembla repetitiu és en realitat una garantia: cada punt de fallada té un tractament explícit. No hi ha errors silenciosos. No hi ha excepcions que es propaguen sense control. No hi ha “ja el pillaré més amunt”.
Vegem un exemple més realista. Una funció que llegeix un fitxer de configuració, el parseja com a JSON i retorna una estructura:
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf(\"llegint configuració: %w\", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf(\"parsejant configuració: %w\", err)
}
if cfg.Port == 0 {
return nil, errors.New(\"el port no pot ser zero\")
}
return &cfg, nil
}Tres operacions que poden fallar, tres comprovacions explícites. Cadascuna amb un missatge que et diu exactament què passava quan ha fallat. Quan vegis llegint configuració: open config.yaml: no such file or directory als logs, sabràs exactament on buscar.
Un detall important: fixa’t que reutilitzo la variable err dins del if. A Go és idiomàtic declarar err en l’àmbit del if quan només la necessites per a la comprovació:
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf(\"parsejant configuració: %w\", err)
}Això manté l’àmbit de err limitat al bloc if, que és més net quan tens múltiples comprovacions seguides.
Wrapping d’errors amb fmt.Errorf i %w
Un dels avenços més importants en la gestió d’errors de Go va arribar a Go 1.13 amb el wrapping d’errors. Abans, si volies afegir context a un error, perdies la informació de l’error original:
// Abans de Go 1.13: es perd l'error original
return fmt.Errorf(\"fallada en connectar: %v\", err)Amb %v crees un error nou el missatge del qual inclou el text de l’error original, però la cadena d’errors es trenca. No pots inspeccionar quin tipus d’error era l’original.
El verb %w soluciona això:
// Amb wrapping: conserves l'error original
return fmt.Errorf(\"fallada en connectar a la base de dades: %w\", err)Ara l’error resultant conté l’error original embolcallat. Pots inspeccionar-lo amb errors.Is o errors.As, recórrer la cadena d’errors i prendre decisions basades en l’error arrel.
La regla és senzilla: usa %w quan vulguis que qui cridi la teva funció pugui inspeccionar l’error original. Usa %v quan vulguis encapsular l’error i ocultar els detalls d’implementació.
Un exemple pràctic. Imagina un servei que accedeix a una base de dades:
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf(\"obtenint usuari %d: %w\", id, err)
}
return user, nil
}Aquí fem servir %w perquè volem que la capa superior pugui distingir si l’error és un “no trobat” o una fallada de connexió. Però si estiguessis en una capa d’API pública i no volguessis exposar detalls interns de la base de dades, faries servir %v per crear un error opac.
El wrapping pot encadenar-se. Si el repositori també va embolcallar l’error amb %w, acabes amb una cadena com:
obtenint usuari 42: consultant base de dades: dial tcp 127.0.0.1:5432: connection refusedCada capa afegeix el seu context, i l’error arrel segueix sent accessible mitjançant les funcions del paquet errors.
errors.Is i errors.As: inspeccionant tipus d’error
Amb el wrapping ve la necessitat d’inspeccionar errors embolcallats. Per a això existeixen errors.Is i errors.As.
errors.Is: comprovar identitat
errors.Is recorre la cadena d’errors embolcallats i comprova si algun coincideix amb un error específic:
import (
\"errors\"
\"os\"
)
_, err := os.Open(\"config.yaml\")
if errors.Is(err, os.ErrNotExist) {
fmt.Println(\"El fitxer no existeix, fent servir configuració per defecte\")
} else if err != nil {
return fmt.Errorf(\"error inesperat obrint config: %w\", err)
}El crucial aquí és que errors.Is funciona a través de capes de wrapping. Si algú va embolcallar os.ErrNotExist amb tres capes de fmt.Errorf(\"...: %w\", err), errors.Is encara el troba. Per això sempre has de fer servir errors.Is en lloc de comparar directament amb ==:
// MAL: no funciona amb errors embolcallats
if err == os.ErrNotExist {
// BÉ: funciona a través de wrapping
if errors.Is(err, os.ErrNotExist) {errors.As: comprovar tipus
errors.As és l’equivalent per a tipus d’error personalitzats. Recorre la cadena d’errors i comprova si algun és del tipus que busques:
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf(\"Error a la ruta: %s, operació: %s\n\", pathErr.Path, pathErr.Op)
}errors.As no només comprova el tipus, sinó que assigna el valor al punter que li passes. És com una type assertion però que funciona a través de capes de wrapping.
Un patró habitual en APIs HTTP és definir un tipus d’error propi i fer servir errors.As al handler per decidir el codi de resposta:
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Al handler
func handleRequest(w http.ResponseWriter, r *http.Request) {
result, err := service.DoSomething(r.Context())
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
http.Error(w, appErr.Message, appErr.Code)
return
}
http.Error(w, \"Error intern\", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}Errors centinella: valors d’error a nivell de paquet
Els errors centinella (sentinel errors) són variables d’error declarades a nivell de paquet que representen condicions d’error específiques i conegudes. Els has vist a la llibreria estàndard:
var (
ErrNotFound = errors.New(\"not found\")
ErrUnauthorized = errors.New(\"unauthorized\")
ErrConflict = errors.New(\"conflict\")
)La convenció a Go és clara: els errors centinella comencen amb Err (excepte io.EOF, que és una excepció històrica). Són valors, no tipus. Els compares amb errors.Is, no amb errors.As.
Són útils quan el teu paquet necessita exposar condicions d’error que els consumidors voldran comprovar:
package user
var (
ErrNotFound = errors.New(\"usuari no trobat\")
ErrAlreadyExists = errors.New(\"l'usuari ja existeix\")
ErrInvalidEmail = errors.New(\"email invàlid\")
)
func (s *Service) Create(ctx context.Context, u *User) error {
existing, err := s.repo.FindByEmail(ctx, u.Email)
if err != nil && !errors.Is(err, ErrNotFound) {
return fmt.Errorf(\"comprovant email existent: %w\", err)
}
if existing != nil {
return ErrAlreadyExists
}
// ...
}I a la capa que consumeix aquest servei:
err := userService.Create(ctx, newUser)
if errors.Is(err, user.ErrAlreadyExists) {
http.Error(w, \"L'email ja està registrat\", http.StatusConflict)
return
}
if err != nil {
http.Error(w, \"Error intern\", http.StatusInternalServerError)
return
}Quan usar errors centinella
Usa errors centinella quan:
- La condició d’error és previsible i coneguda (no trobat, ja existeix, no autoritzat).
- Els consumidors del teu paquet necessiten prendre decisions basades en el tipus d’error.
- L’error no necessita context addicional més enllà de la seva identitat.
No uses errors centinella per a:
- Errors que només logejaràs sense prendre decisions.
- Errors amb context dinàmic (com un ID d’usuari o un nom de fitxer).
- Errors interns que no haurien d’escapar del paquet.
Un error centinella és part de l’API pública del teu paquet. Tracta’l com a tal. Canviar-lo o eliminar-lo trenca els consumidors.
Tipus d’error personalitzats: implementant la interfície error
Quan necessites més informació que un simple string, pots crear el teu propi tipus d’error. Només necessites implementar el mètode Error() string:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf(\"validació fallida al camp '%s': %s\", e.Field, e.Message)
}
func ValidateUser(u *User) error {
if u.Name == \"\" {
return &ValidationError{Field: \"name\", Message: \"no pot estar buit\"}
}
if !strings.Contains(u.Email, \"@\") {
return &ValidationError{Field: \"email\", Message: \"format invàlid\"}
}
return nil
}El consumidor pot inspeccionar els camps de l’error:
err := ValidateUser(user)
if err != nil {
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf(\"Camp '%s' invàlid: %s\n\", valErr.Field, valErr.Message)
}
}Errors amb múltiples camps
Per a APIs, un patró que funciona bé és un tipus d’error que inclogui codi HTTP, missatge per a l’usuari i l’error intern:
type APIError struct {
StatusCode int `json:\"-\"`
Message string `json:\"message\"`
Detail string `json:\"detail,omitempty\"`
Internal error `json:\"-\"`
}
func (e *APIError) Error() string {
if e.Internal != nil {
return fmt.Sprintf(\"%s: %v\", e.Message, e.Internal)
}
return e.Message
}
func (e *APIError) Unwrap() error {
return e.Internal
}
// Constructors per a errors comuns
func NewNotFoundError(resource string, id any) *APIError {
return &APIError{
StatusCode: http.StatusNotFound,
Message: fmt.Sprintf(\"%s no trobat\", resource),
Detail: fmt.Sprintf(\"ID: %v\", id),
}
}
func NewInternalError(err error) *APIError {
return &APIError{
StatusCode: http.StatusInternalServerError,
Message: \"Error intern del servidor\",
Internal: err,
}
}Fixa’t en el mètode Unwrap() error. Això permet que errors.Is i errors.As recorrin la cadena d’errors a través del teu tipus personalitzat. Si el teu tipus d’error embolcalla un altre error, sempre implementa Unwrap.
Errors comuns: el que no hauries de fer
Després de treballar amb Go un temps, comences a reconèixer patrons de gestió d’errors que semblen raonables però acaben causant problemes. Aquests són els més freqüents.
Ignorar errors
El pitjor error. I el compilador de Go t’avisa si ignores un valor de retorn, però hi ha maneres de silenciar-lo:
// MAL: ignorar l'error explícitament
result, _ := doSomething()
// MAL: no comprovar l'error de Close
file.Close()
// BÉ: gestionar l'error, fins i tot si és només logejar-lo
result, err := doSomething()
if err != nil {
log.Printf(\"fallada a doSomething: %v\", err)
// decidir què fer
}
// BÉ: comprovar l'error de Close en defer
defer func() {
if err := file.Close(); err != nil {
log.Printf(\"error tancant fitxer: %v\", err)
}
}()El _ per descartar errors només és acceptable quan estàs absolutament segur que l’error no pot ocórrer o no t’afecta. A la pràctica, gairebé mai és el cas.
Over-wrapping: afegir context redundant
Afegir context als errors és bo. Afegir massa context crea missatges il·legibles:
// MAL: cada capa repeteix informació
return fmt.Errorf(\"error a GetUser: fallada en obtenir usuari: %w\", err)
// Resultat: \"error a GetUser: fallada en obtenir usuari: consultant DB: dial tcp...\"
// BÉ: afegeix només el context nou
return fmt.Errorf(\"obtenint usuari %d: %w\", id, err)
// Resultat: \"obtenint usuari 42: consultant DB: dial tcp...\"El nom de la funció ja és a l’stack trace si el necessites. El que aporta valor és el context que la funció coneix: IDs, noms de fitxers, operacions específiques. No repeteixis el nom de la funció ni facis servir frases genèriques com “error a” o “fallada en”.
Usar panic com a sistema d’excepcions
panic existeix a Go, però no és un sistema d’excepcions. És per a situacions irrecuperables on el programa no pot continuar:
// MAL: usar panic per a errors de negoci
func GetUser(id int) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // NO facis això
}
return user
}
// BÉ: retornar l'error
func GetUser(id int) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf(\"buscant usuari %d: %w\", id, err)
}
return user, nil
}Els únics usos legítims de panic són:
- Errors de programació que indiquen un bug (índex fora de rang, nil pointer en un lloc impossible).
- Inicialització del programa que falla (no pots connectar a la base de dades en arrencar).
- Tests, on
panicequival a unt.Fatal.
Si estàs fent servir panic i recover com a throw i catch, estàs lluitant contra el llenguatge. Per entendre més sobre defer, panic i recover, et recomano consultar la documentació oficial de Go.
No comprovar errors en goroutines
Aquest és subtil però perillós. Si llances una goroutine i el codi de dins falla, l’error es perd silenciosament:
// MAL: error perdut
go func() {
result, err := doExpensiveWork()
if err != nil {
log.Printf(\"error: %v\", err) // Qui veu aquest log?
return
}
processResult(result)
}()
// BÉ: comunicar l'error per un canal
errCh := make(chan error, 1)
go func() {
result, err := doExpensiveWork()
if err != nil {
errCh <- fmt.Errorf(\"treball costós: %w\", err)
return
}
processResult(result)
errCh <- nil
}()
if err := <-errCh; err != nil {
// Gestionar l'error
}Si treballes amb múltiples goroutines, errgroup del paquet golang.org/x/sync/errgroup és l’eina correcta:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf(\"fallada en operacions paral·leles: %w\", err)
}Patrons pràctics per a APIs: errors en handlers i serveis
Si estàs construint una API REST amb Go, la gestió d’errors és on més notaràs la diferència amb altres llenguatges. En lloc d’un middleware global que captura excepcions, necessites una estratègia deliberada.
Patró 1: handler amb switch d’errors
L’enfocament més directe. El handler crida el servei i decideix el codi HTTP basant-se en l’error:
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue(\"id\"))
if err != nil {
http.Error(w, \"ID invàlid\", http.StatusBadRequest)
return
}
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, \"Usuari no trobat\", http.StatusNotFound)
case errors.Is(err, ErrUnauthorized):
http.Error(w, \"No autoritzat\", http.StatusUnauthorized)
default:
log.Printf(\"error obtenint usuari %d: %v\", id, err)
http.Error(w, \"Error intern\", http.StatusInternalServerError)
}
return
}
w.Header().Set(\"Content-Type\", \"application/json\")
json.NewEncoder(w).Encode(user)
}Simple i explícit. Però si tens vint handlers, repeteixes el mateix switch a cadascun.
Patró 2: handler wrapper amb tipus d’error
Un enfocament més escalable és definir un tipus de handler que retorna error i un middleware que el tradueix:
type AppHandler func(w http.ResponseWriter, r *http.Request) error
func HandleErrors(h AppHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err == nil {
return
}
var apiErr *APIError
if errors.As(err, &apiErr) {
w.Header().Set(\"Content-Type\", \"application/json\")
w.WriteHeader(apiErr.StatusCode)
json.NewEncoder(w).Encode(apiErr)
return
}
log.Printf(\"error no gestionat: %v\", err)
http.Error(w, \"Error intern\", http.StatusInternalServerError)
}
}
// Ús
mux.HandleFunc(\"GET /users/{id}\", HandleErrors(func(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.Atoi(r.PathValue(\"id\"))
if err != nil {
return &APIError{StatusCode: 400, Message: \"ID invàlid\"}
}
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
return err // El wrapper decideix el codi HTTP
}
w.Header().Set(\"Content-Type\", \"application/json\")
return json.NewEncoder(w).Encode(user)
}))Aquest patró centralitza la lògica de traducció d’errors a HTTP sense perdre l’explicitesa de la gestió d’errors a cada handler.
Patró 3: errors en capes de servei
A la capa de servei, la regla és: afegeix context i propaga. No tradueixis errors a codis HTTP aquí. Això és responsabilitat del handler.
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf(\"consultant usuari %d: %w\", id, err)
}
if !user.Active {
return nil, ErrUnauthorized
}
return user, nil
}Fixa’t en alguna cosa important: la capa de servei transforma sql.ErrNoRows (un detall d’implementació de la base de dades) en ErrNotFound (un concepte de domini). Això desacobla la teva lògica de negoci de la tecnologia de persistència. Si demà canvies PostgreSQL per MongoDB, els handlers no necessiten canviar.
Quan NO complicar la gestió d’errors
Després de tot l’anterior, és temptador muntar una arquitectura d’errors elaborada amb tipus personalitzats, wrapping a cada capa i un sistema de codis d’error digne d’una RFC. No ho facis.
La majoria d’aplicacions en Go necessiten molt poc:
- Errors centinella per a dues o tres condicions conegudes (
ErrNotFound,ErrAlreadyExists). - Wrapping amb
%wper mantenir la cadena de context. - Un tipus d’error personalitzat si estàs construint una API i necessites mapejar errors a codis HTTP.
I ja. Res més.
No crees una jerarquia d’errors com a Java amb IOException, FileNotFoundException, SocketException. Go no està dissenyat per a això. Un error és un valor que descriu què ha fallat. Com més simple sigui el teu sistema d’errors, més fàcil serà treballar-hi.
Per a scripts, CLIs i eines internes, moltes vegades fmt.Errorf amb %w és tot el que necessites. No cal crear errors centinella si ningú els inspeccionarà. No cal crear tipus personalitzats si el string de l’error és context suficient.
La regla que segueixo: comença amb fmt.Errorf i %w. Afegeix errors centinella quan un consumidor necessiti prendre decisions. Afegeix tipus personalitzats quan necessitis camps estructurats. I quan escrius tests a Go, la simplicitat dels errors com a valors fa que provar condicions d’error sigui molt més directe que mockejar excepcions.
Comparació amb excepcions (Java/Python/Kotlin)
Vinc de Kotlin i Java. Treballo amb Python cada dia. Conec els tres sistemes d’excepcions i puc dir amb coneixement de causa que l’enfocament de Go no és ni millor ni pitjor. És diferent, i té trade-offs clars.
Java: excepcions checked i unchecked
Java té el sistema d’excepcions més formal dels tres. Les checked exceptions t’obliguen a gestionar-les o declarar-les a la signatura del mètode:
public User getUser(int id) throws UserNotFoundException, DatabaseException {
// ...
}En teoria, això dóna garanties similars a Go: saps què pot fallar. A la pràctica, la majoria d’equips acaba embolcallant tot en RuntimeException per no haver de declarar excepcions a cada mètode de la cadena. Les checked exceptions van ser una bona idea que l’ecosistema va rebutjar.
Python: excepcions com a flux de control
Python fa servir excepcions per a tot. No només per a errors:
try:
value = my_dict[\"key\"]
except KeyError:
value = \"default\"Això és idiomàtic a Python (“easier to ask forgiveness than permission”). Però significa que qualsevol funció pot llançar qualsevol excepció en qualsevol moment, i l’única manera de saber-ho és llegir la documentació (que pot no existir) o el codi font.
Kotlin: excepcions unchecked amb Result
Kotlin va eliminar les checked exceptions i confia que el programador gestioni els errors. Té un tipus Result<T> que s’acosta a l’enfocament de Go:
fun getUser(id: Int): Result<User> {
return runCatching { repository.findById(id) }
}
// Ús
getUser(42)
.onSuccess { user -> println(user) }
.onFailure { error -> println(\"Error: $error\") }Però Result és opcional. La majoria del codi Kotlin segueix fent servir excepcions.
El trade-off real
| Aspecte | Go | Java (checked) | Python / Kotlin |
|---|---|---|---|
| Visibilitat de l’error | Sempre visible | Visible a la signatura | Invisible sense documentació |
| Verbositat | Alta | Mitjana-alta | Baixa |
| Propagació | Explícita | Semi-explícita | Implícita |
| Risc d’error silenciat | Baix (_ és visible) | Mitjà (catch buit) | Alt (bare except) |
| Composició | Valors normals | Mecanisme especial | Mecanisme especial |
| Stack trace automàtic | No (cal wrapping) | Sí | Sí |
L’avantatge principal de Go és que els errors són valors. No són un mecanisme especial del llenguatge amb regles pròpies. Són valors de retorn normals que pots emmagatzemar en variables, passar a funcions, posar en slices, testar amb igualtat. Això fa que la gestió d’errors sigui codi normal, no un sistema paral·lel amb la seva pròpia sintaxi.
El desavantatge principal és la verbositat i la manca de stack traces automàtics. A Java o Python, quan una excepció peta, tens tota la pila de crides. A Go, si no embolcalles els teus errors amb context a cada capa, acabes amb un missatge críptic que no et diu on es va originar el problema.
Per això el wrapping amb %w no és opcional a la pràctica. És obligatori si vols errors útils.
El cost de la claredat
La gestió d’errors a Go és verbosa. Això és innegable. Véns de Kotlin amb les seves sealed classes o de Python amb el seu try/except, i el primer mes amb Go sents que escrius més codi del necessari només per gestionar casos de fallada.
Però hi ha alguna cosa que canvia amb el temps. Comences a llegir codi Go d’altres i entens exactament què passa quan alguna cosa falla. No hi ha fluxos ocults, no hi ha excepcions que salten tres capes amunt sense que ningú les esperi, no hi ha catch buits amagats en un middleware que algú va escriure fa dos anys. Tot és allà, davant teu, a cada if err != nil.
Aquesta visibilitat té un cost en escriure. Però té un valor enorme en mantenir. I mantenir és el que fem el 80% del temps.
Si véns d’un altre llenguatge, el meu consell és aquest: no intentis replicar excepcions amb panic/recover. No muntis jerarquies d’errors com si estiguessis a Java. Comença amb fmt.Errorf i %w, afegeix errors centinella quan un consumidor necessiti prendre decisions, i crea tipus personalitzats només quan necessitis camps estructurats. Res més.
La gestió d’errors a Go sembla repetitiva fins que entens que et força a fer visible el flux real del programa. Cada if err != nil és una decisió conscient sobre què fer quan alguna cosa falla. I això, en producció, és exactament el que vols.


