Defer, panic i recover a Go: quan usar-los i quan evitar-los
Defer per a neteja, panic per a fallades excepcionals i recover per a contenció. Quan tenen sentit i quan no.

Defer, panic i recover són tres mecanismes que els nouvinguts a Go ignoren o en fan un abús. Les dues coses estan malament. Si els ignores, escrius codi fràgil que filtra recursos. Si en fas un abús, escrius Go amb mentalitat de Java o Python, convertint panic/recover en un try/catch disfressat que el llenguatge mai va pretendre oferir.
La qüestió és que aquests tres mecanismes estan dissenyats per a coses molt concretes. defer és per a neteja garantida. panic és per a fallades que no haurien d’ocórrer. recover és per a contenció en fronteres. Fora d’aquests contextos, usar-los és un error de disseny.
Si véns de llenguatges amb excepcions, necessites recalibrar el teu instint. A Go, el camí correcte per a la majoria d’errors és retornar-los com a valors. Si no tens això clar, et recomano llegir primer errors a Go abans de continuar aquí.
Defer: neteja garantida
defer ajorna l’execució d’una funció fins que la funció que la conté retorni. No importa com retorni: per un return normal, per un panic, per arribar al final del cos. La funció diferida sempre s’executa.
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf(\"obrir config: %w\", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf(\"llegir config: %w\", err)
}
return data, nil
}El defer f.Close() s’executa quan readConfig retorna, ja sigui amb dades o amb error. No has de recordar tancar el fitxer abans de cada return. No pots oblidar-te’n. Aquesta és la gràcia.
Això és especialment valuós quan una funció té múltiples punts de retorn. Sense defer, hauries de tancar el recurs abans de cada return, la qual cosa és una recepta per a bugs. Amb defer, ho declares una vegada just després d’adquirir el recurs i t’oblides.
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Primer punt de retorn
header, err := readHeader(f)
if err != nil {
return fmt.Errorf(\"header invàlid: %w\", err)
}
// Segon punt de retorn
if header.Version < 3 {
return fmt.Errorf(\"versió %d no suportada\", header.Version)
}
// Tercer punt de retorn
return processBody(f, header)
}Tres punts de retorn, un sol defer. No hi ha manera que el fitxer quedi obert.
Ordre d’execució: pila LIFO
Quan uses diversos defer en la mateixa funció, s’executen en ordre invers: últim en entrar, primer en sortir. És una pila.
func exemple() {
fmt.Println(\"inici\")
defer fmt.Println(\"primer diferit\")
defer fmt.Println(\"segon diferit\")
defer fmt.Println(\"tercer diferit\")
fmt.Println(\"fi\")
}La sortida:
inici
fi
tercer diferit
segon diferit
primer diferitAixò no és arbitrari. Té sentit pràctic: si adquireixes recursos en ordre A, B, C, normalment vols alliberar-los en ordre C, B, A. És el mateix patró que un destructor en C++ o un bloc finally aniuat.
func transaccio(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // S'executa segon si hi ha error
conn, err := acquireExternalConnection()
if err != nil {
return err
}
defer conn.Release() // S'executa primer
// ... operacions ...
return tx.Commit() // Rollback és no-op després de Commit
}El conn.Release() s’executa abans que tx.Rollback(), que és exactament el que vols: primer alliberes la connexió externa, llavors desfàs la transacció si ha estat necessari.
Patrons comuns amb defer
Tancar fitxers i connexions
El cas més bàsic i el més freqüent. Qualsevol recurs que implementa io.Closer es tanca amb defer.
resp, err := http.Get(\"https://api.example.com/data\")
if err != nil {
return err
}
defer resp.Body.Close()Un matís important: el defer va després de comprovar l’error. Si http.Get falla, resp pot ser nil i cridar resp.Body.Close() provocaria un panic. Segueix sempre el patró: adquirir, comprovar error, diferir tancament.
Alliberar locks
Els mutex i altres mecanismes de sincronització segueixen el mateix patró.
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}Aquí el defer és imprescindible. Sense ell, qualsevol panic dins de la secció crítica deixaria el mutex bloquejat, causant un deadlock que és extremadament difícil de diagnosticar en producció.
Mesurar durada
Un patró elegant per instrumentar funcions.
func measureTime(name string) func() {
start := time.Now()
return func() {
fmt.Printf(\"%s va trigar %v\n\", name, time.Since(start))
}
}
func operacioLenta() {
defer measureTime(\"operacioLenta\")()
// Nota els parèntesis dobles: measureTime s'executa immediatament,
// i la funció que retorna es difereix.
time.Sleep(2 * time.Second)
}Això funciona perquè measureTime(\"operacioLenta\") s’avalua en el moment del defer (capturant el temps d’inici), però la funció que retorna s’executa en sortir d’operacioLenta (calculant la durada).
Recover de panics (ho veurem en detall més endavant)
defer func() {
if r := recover(); r != nil {
log.Printf(\"panic recuperat: %v\", r)
}
}()Trampes de defer que et mossegaran
Els arguments s’avaluen immediatament
Els arguments de la funció diferida s’avaluen en el moment en què s’executa la sentència defer, no quan s’executa la funció diferida.
func exemple() {
x := 0
defer fmt.Println(\"x val:\", x) // x s'avalua ARA, val 0
x = 42
fmt.Println(\"x modificat a:\", x)
}Sortida:
x modificat a: 42
x val: 0No imprimeix 42. Imprimeix 0, perquè aquest era el valor de x quan el defer es va registrar. Si necessites capturar el valor final, usa un closure:
func exemple() {
x := 0
defer func() {
fmt.Println(\"x val:\", x) // Ara sí, captura la variable
}()
x = 42
}Ara imprimeix 42, perquè el closure captura la variable, no el seu valor.
Defer en bucles: compte amb l’acumulació
Un error clàssic de principiant:
func processarFitxers(paths []string) error {
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // MALAMENT: s'acumulen fins que processarFitxers retorni
// ... processar fitxer ...
}
return nil
}Si paths té 10.000 elements, tindràs 10.000 fitxers oberts simultàniament abans que es tanqui cap. Els defer no s’executen al final de cada iteració del bucle, sinó al final de la funció.
La solució és extreure el cos del bucle a una funció separada:
func processarFitxers(paths []string) error {
for _, path := range paths {
if err := processarUnFitxer(path); err != nil {
return err
}
}
return nil
}
func processarUnFitxer(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// ... processar fitxer ...
return nil
}Ara el defer s’executa al final de cada crida a processarUnFitxer. Net, correcte, sense filtracions.
Defer i valors de retorn amb nom
defer pot modificar els valors de retorn amb nom. Això és útil però fàcil de malinterpretar.
func llegirContingut(path string) (content string, err error) {
f, err := os.Open(path)
if err != nil {
return \"\", err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr // Modifica l'error de retorn
}
}()
data, err := io.ReadAll(f)
if err != nil {
return \"\", err
}
return string(data), nil
}El defer aquí modifica err (el valor de retorn amb nom) si f.Close() falla i no hi havia un error previ. Aquest és el patró correcte per no perdre errors de tancament, però requereix que entenguis que els closures en defer accedeixen als valors de retorn amb nom per referència.
Panic: quan el programa no pot continuar
panic atura l’execució normal de la goroutine actual. Executa els defer pendents i llavors propaga el panic cap amunt en la pila de crides fins que arriba al cim de la goroutine, on acaba el programa amb un stack trace.
func dividir(a, b int) int {
if b == 0 {
panic(\"divisió per zero\")
}
return a / b
}Un panic és sorollós. Imprimeix un stack trace complet. Mata el procés si no es recupera. I això és intencional. És un senyal que alguna cosa està fonamentalment malament.
La llibreria estàndard de Go usa panic internament en situacions com:
- Accés fora dels límits d’un slice
- Assertion de tipus incorrecta sense la forma de dos valors
- Enviament a un channel tancat
- Ús d’un mutex després de copiar-lo
Tots aquests són errors del programador, no errors operacionals. Aquesta distinció és clau.
Quan panic és apropiat
Errors d’inicialització irrecuperables
Si el teu programa no pot arrencar sense una configuració vàlida, un panic a l’inici és raonable.
func mustLoadConfig(path string) Config {
data, err := os.ReadFile(path)
if err != nil {
panic(fmt.Sprintf(\"no es pot llegir la config %s: %v\", path, err))
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
panic(fmt.Sprintf(\"config invàlida a %s: %v\", path, err))
}
return cfg
}El prefix must és una convenció a Go. Funcions com template.Must, regexp.MustCompile o sql.MustOpen fan panic si fallen. La idea és que només les uses en la inicialització del programa, on una fallada significa que no hi ha res a fer.
Invariants del programador
Si una condició hauria de ser impossible segons la lògica del programa i es viola, un panic és apropiat.
func estatSeguent(actual Estat) Estat {
switch actual {
case Pendent:
return EnProcés
case EnProcés:
return Completat
case Completat:
return Arxivat
default:
panic(fmt.Sprintf(\"estat desconegut: %d\", actual))
}
}Si arribes al default, és un bug al teu codi, no un error de l’usuari ni una fallada de xarxa. Un panic amb un missatge clar és millor que retornar un error que ningú gestionarà correctament perquè “no hauria de passar”.
Detecció de bugs en desenvolupament
De vegades uses panic com una assertion durant el desenvolupament per detectar problemes ràpidament.
func newBuffer(size int) *Buffer {
if size <= 0 {
panic(\"newBuffer: size ha de ser positiu\")
}
return &Buffer{data: make([]byte, 0, size)}
}Això és acceptable si newBuffer només es crida amb valors controlats pel programador, no amb input de l’usuari.
Quan panic NO és apropiat
Aquí és on la majoria de gent s’equivoca. Venim de llenguatges on throw new Exception(\"alguna cosa ha anat malament\") és normal, i la temptació d’usar panic de la mateixa forma és enorme. Resisteix.
Errors de validació
// MALAMENT
func crearUsuari(nom string) Usuari {
if nom == \"\" {
panic(\"nom buit\") // NO
}
return Usuari{Nom: nom}
}
// BÉ
func crearUsuari(nom string) (Usuari, error) {
if nom == \"\" {
return Usuari{}, errors.New(\"el nom no pot estar buit\")
}
return Usuari{Nom: nom}, nil
}Un nom buit no és una fallada irrecuperable. És un input invàlid. Retorna un error.
Dades no trobades
// MALAMENT
func buscarProducte(id int) Producte {
p, ok := productes[id]
if !ok {
panic(fmt.Sprintf(\"producte %d no trobat\", id))
}
return p
}
// BÉ
func buscarProducte(id int) (Producte, error) {
p, ok := productes[id]
if !ok {
return Producte{}, fmt.Errorf(\"producte %d no trobat\", id)
}
return p, nil
}Que un producte no existeixi és un escenari operacional completament normal. Ni se t’acudeixi fer panic per això.
Fallades de xarxa, I/O, base de dades
// MALAMENT
func obtenirDades(url string) []byte {
resp, err := http.Get(url)
if err != nil {
panic(err) // NO, les xarxes fallen constantment
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return data
}Les xarxes fallen. Els discs fallen. Les bases de dades es saturen. Aquests són errors esperats que el teu programa ha de gestionar amb gràcia, no petar.
Timeouts i cancel·lacions
// MALAMENT
func ferPeticio(ctx context.Context) Resultat {
resultat, err := servei.Cridar(ctx)
if err != nil {
panic(err) // NO
}
return resultat
}Un timeout és una decisió operacional. Un context cancel·lat és un flux normal. Si vols aprofundir en com gestionar contextos correctament, he escrit sobre context a Go.
La regla és simple: si l’error pot ocórrer en producció com a part del funcionament normal del sistema, mai usis panic.
Recover: capturar panics en les fronteres
recover és una funció builtin que captura un panic en curs i retorna el valor que es va passar a panic. Només funciona dins d’una funció diferida. Fora d’un defer, sempre retorna nil.
func operacioSegura() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf(\"panic recuperat: %v\", r)
}
}()
// Codi que podria fer panic
funcioPerillosa()
return nil
}recover no és un mecanisme general de gestió d’errors. És un mecanisme de contenció per evitar que un panic descontrolat mati el teu procés sencer. La distinció és important.
On té sentit recover
recover té sentit en fronteres:
Fronteres de goroutines: Si llances goroutines que processen feina, un panic en una goroutine mata tot el programa. Un
recoveren la goroutine pot registrar l’error i continuar processant el següent treball.Fronteres de request: En un servidor HTTP, un panic en un handler no hauria d’enfonsar el servidor sencer.
Fronteres de plugin: Si el teu programa carrega codi de tercers o executa lògica configurable, un
recoverprotegeix el teu procés principal.Fronteres de llibreria pública: Si internament uses panic per simplificar el flux, la teva API pública no l’ha d’exposar.
El patró del servidor HTTP: middleware de recover
Aquest és probablement l’ús més comú i més legítim de recover en producció. Un servidor HTTP que serveix milers de requests no pot permetre’s que un bug en un handler mati el procés sencer.
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Registrar el stack trace per a debugging
stack := debug.Stack()
log.Printf(
\"PANIC a %s %s: %v\n%s\",
r.Method, r.URL.Path, rec, stack,
)
// Retornar 500 al client
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(\"Internal Server Error\"))
}
}()
next.ServeHTTP(w, r)
})
}I ho uses així:
func main() {
mux := http.NewServeMux()
mux.HandleFunc(\"GET /users/{id}\", getUser)
mux.HandleFunc(\"POST /orders\", createOrder)
handler := recoveryMiddleware(mux)
log.Fatal(http.ListenAndServe(\":8080\", handler))
}Ara, si getUser té un bug que provoca un panic (accés a un slice fora de rang, nil pointer dereference, el que sigui), el middleware captura el panic, registra el stack trace perquè puguis debuggar, retorna un 500 al client i el servidor segueix funcionant per a la resta de peticions.
Frameworks com Gin ja inclouen aquest middleware per defecte. Si estàs construint amb la llibreria estàndard, has d’afegir-lo tu. Més detalls sobre això en middlewares a Go i API REST amb Go.
Recover en goroutines de treball
Un altre patró important: workers que processen tasques d’una cua.
func worker(id int, tasques <-chan Tasca) {
for tasca := range tasques {
processarAmbRecover(id, tasca)
}
}
func processarAmbRecover(workerID int, tasca Tasca) {
defer func() {
if r := recover(); r != nil {
log.Printf(
\"Worker %d: panic processant tasca %s: %v\n%s\",
workerID, tasca.ID, r, debug.Stack(),
)
// Opcionalment: enviar a una dead-letter queue
}
}()
tasca.Processar()
}Sense el recover, un panic a tasca.Processar() mataria la goroutine del worker, deixant feina sense processar. Amb el recover, el worker registra l’error i continua amb la tasca següent.
Fixa’t que processarAmbRecover és una funció separada. No pots posar el defer/recover dins de la funció anònima de la goroutine i esperar que es recuperi de panics en funcions que crides des d’allà. Bé, sí que pots, però és més net separar-ho perquè l’àmbit sigui clar.
Anti-pattern: usar panic/recover com a try/catch
Aquest és l’error més greu i el més comú entre programadors que venen de Java, C# o Python. Vegem-ho explícitament perquè quedi clar.
L’anti-pattern
// NO FACIS AIXÒ
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf(\"%s: %s\", e.Field, e.Message)
}
func validarComanda(c Comanda) {
if c.Total <= 0 {
panic(ValidationError{Field: \"total\", Message: \"ha de ser positiu\"})
}
if c.ClientID == \"\" {
panic(ValidationError{Field: \"client_id\", Message: \"és obligatori\"})
}
}
func crearComanda(c Comanda) error {
defer func() {
if r := recover(); r != nil {
if ve, ok := r.(ValidationError); ok {
// \"Capturem\" la validació com si fos un catch
log.Printf(\"validació fallida: %v\", ve)
}
}
}()
validarComanda(c)
// ... guardar a la base de dades ...
return nil
}Això funciona. Però és terrible. Estàs usant panic com a throw i recover com a catch. Estàs ocultant el flux d’errors. Estàs fent impossible que el caller sàpiga què pot fallar mirant la signatura de la funció. Estàs trencant el contracte social de Go.
La versió correcta
func validarComanda(c Comanda) error {
if c.Total <= 0 {
return fmt.Errorf(\"total ha de ser positiu, té %f\", c.Total)
}
if c.ClientID == \"\" {
return errors.New(\"client_id és obligatori\")
}
return nil
}
func crearComanda(c Comanda) error {
if err := validarComanda(c); err != nil {
return fmt.Errorf(\"comanda invàlida: %w\", err)
}
// ... guardar a la base de dades ...
return nil
}Més línies, sí. Però cada funció declara exactament què pot fallar. El caller sap que crearComanda pot retornar un error. Sense sorpreses, sense màgia, sense stack unwinding invisible.
Per què l’anti-pattern és perjudicial
Rendiment:
panic/recoverés significativament més lent que retornar un error. No és gratuït; implica unwinding de l’stack.Llegibilitat: Quan llegeixes una funció que retorna
error, saps immediatament que pot fallar. Quan llegeixes una funció que fapanic, no tens ni idea de si algú la està recuperant a dalt o si anirà a matar el procés.Composició: Els errors a Go es poden embolcallar amb
%w, inspeccionar amberrors.Isierrors.As, i gestionar de forma granular. Un panic és una destral: ho captures tot o no captures res.Convenció: Tot l’ecosistema de Go espera errors com a valors. Si la teva llibreria fa panic per errors operacionals, ningú la voldrà usar.
Si et trobes escrivint
recoverfora d’un middleware o un boundary, probablement estàs fent alguna cosa malament.
Cas especial: panic intern amb recover a l’API pública
Hi ha un cas on usar panic/recover internament és acceptable i la pròpia llibreria estàndard ho fa: quan tens recursió profunda o un parser complex i necessites sortir de molts nivells d’anidament de cop.
El paquet encoding/json fa això internament. Usa panic per saltar des del fons de l’arbre de serialització quan troba un error irrecuperable, i el captura amb recover en la funció pública per retornar un error net.
// Exemple simplificat del patró intern
type parseError struct {
msg string
}
func (p *Parser) parse() (Node, error) {
defer func() {
if r := recover(); r != nil {
if pe, ok := r.(parseError); ok {
// Convertir panic intern en error públic
err = fmt.Errorf(\"error de parseig: %s\", pe.msg)
} else {
panic(r) // Re-llançar panics que no són nostres
}
}
}()
return p.parseExpressio(), nil
}
func (p *Parser) parseExpressio() Node {
// Recursió profunda...
if p.tokenActual.Tipus == TokenInvalid {
panic(parseError{msg: \"token inesperat\"}) // Salta al recover de parse()
}
// ...
}Les regles per a aquest patró:
- Mai creuïs la frontera de l’API pública. El caller només veu
error. - Usa un tipus de panic privat i específic per distingir-lo de panics reals.
- Si captures alguna cosa que no és el teu tipus de panic, re-llança’l amb
panic(r).
Això és acceptable perquè és un detall d’implementació. Però si estàs temptat d’usar-ho, pregunta’t primer si pots passar un error a través de les crides recursives. En la majoria de casos, pots.
Regles pràctiques
Per tancar, les regles que aplico al meu codi i que recomano:
Defer
- Sempre difereix el tancament de recursos just després d’adquirir-los (i comprovar l’error).
- Mai posis
deferdins d’un bucle si el bucle pot tenir moltes iteracions. Extreu una funció. - Recorda que els arguments s’avaluen en el moment del
defer, no en executar-se. Si necessites el valor final, usa un closure. - Usa
deferamb valors de retorn amb nom per capturar errors de tancament.
Panic
- Només per a errors del programador: invariants violades, estats impossibles, inicialització fallida.
- Funcions
Must*que fan panic són acceptables si només s’usen durant la inicialització del programa. - Mai per a errors operacionals: validació, I/O, xarxa, base de dades, dades no trobades.
- Si dubtes entre panic i error, la resposta és error. Sempre.
Recover
- Només en fronteres: middleware HTTP, goroutines de treball, fronteres de llibreria pública.
- Sempre registra el stack trace amb
debug.Stack(). Un panic recuperat sense log és un bug invisible. - Si captures un panic que no és teu, re-llança’l.
- Mai com a mecanisme general de gestió d’errors.
Go té un sistema de gestió d’errors. Són els valors
error. Defer, panic i recover són eines complementàries per a escenaris específics. Usa-les com el que són.
Si vols veure com s’apliquen aquests conceptes en una API real, fes un cop d’ull a API REST amb Go. I si necessites repassar els fonaments de la gestió d’errors a Go, que és el que usaràs el 99% del temps, comença per errors a Go.


