Context en Go: timeouts, cancel·lacions i peticions robustes
context.Context en Go explicat: cancel·lacions, timeouts, propagació en HTTP, base de dades i workers. Peça clau per a serveis reals.

Context en Go em va confondre durant setmanes. El veia a totes les signatures de funció, el passava mecànicament com a primer argument, i no entenia per què existia. Semblava una formalitat del llenguatge, alguna cosa que havies de posar perquè sí. Aleshores vaig construir un servei que per cada petició cridava tres APIs externes i feia una consulta a PostgreSQL, i de cop tot va encaixar. Si una d’aquelles crides trigava 30 segons, les altres tres seguien esperant. Si el client tancava la connexió, el servidor seguia processant una resposta que ningú anava a rebre. Sense context, el teu servei no té manera de dir “para, això ja no importa”.
No és una peça secundària. És una de les abstraccions més importants de Go per a qualsevol cosa que vagi més enllà d’un script.
Què és context.Context: la interfície
context.Context és una interfície amb quatre mètodes:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline()retorna el moment en què el context expirarà, si en té un de definit.Done()retorna un canal que es tanca quan el context es cancel·la o expira. És la peça clau: pots ferselectsobre aquest canal per reaccionar a la cancel·lació.Err()retornanilmentre el context estigui actiu,context.Canceledsi va ser cancel·lat manualment, ocontext.DeadlineExceededsi el timeout ha expirat.Value(key)retorna valors associats al context. Utilitza’l amb cautela (més endavant explico per què).
El que fa especial context.Context és que és immutable i jeràrquic. Mai no modifiques un context existent: en crees un de nou derivat de l’anterior. Si cancel·les un context pare, tots els fills es cancel·len automàticament. Aquesta propietat és la que permet propagar senyals de cancel·lació a través de tota la cadena de crides de la teva aplicació.
context.Background() i context.TODO()
Aquests són els dos contextos arrel que ofereix la llibreria estàndard:
ctx := context.Background()
ctx := context.TODO()context.Background() és el context buit per defecte. No té deadline, no es pot cancel·lar, no té valors. L’uses com a punt de partida quan no hi ha cap altre context disponible: a main(), a la inicialització de la teva aplicació, o en arrencar un worker de fons.
context.TODO() és funcionalment idèntic a Background(), però amb una intenció diferent: marca un lloc on saps que hi hauria d’haver un context real però encara no l’has implementat. És un recordatori al codi, no una solució permanent.
func main() {
ctx := context.Background()
server := NewServer(ctx)
server.Start()
}A la pràctica, si veus context.TODO() en un codebase productiu, és un senyal que algú ha deixat alguna cosa a mitges. Tracta’l com a deute tècnic.
context.WithCancel: cancel·lació manual
context.WithCancel crea un context derivat que pots cancel·lar explícitament cridant una funció:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
// treball llarg
select {
case <-ctx.Done():
fmt.Println(\"cancel·lat:\", ctx.Err())
return
case result := <-doWork():
fmt.Println(\"resultat:\", result)
}
}()
// En algun punt decideixes cancel·lar
cancel()La funció cancel és idempotent: pots cridar-la múltiples vegades sense problema. I sempre has de cridar-la, fins i tot si el treball acaba abans. Si no la crides, el context i els seus recursos interns queden en memòria fins que el pare es cancel·li o el programa acabi. El defer cancel() immediatament després de crear el context és un patró obligatori.
Un cas real on necessites cancel·lació manual: tens diverses goroutines fent la mateixa consulta a diferents rèpliques de base de dades. La primera que respongui guanya, i cancel·les les altres.
func queryFastest(ctx context.Context, replicas []string, query string) (Result, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan Result, len(replicas))
for _, replica := range replicas {
go func(addr string) {
result, err := queryReplica(ctx, addr, query)
if err == nil {
results <- result
}
}(replica)
}
select {
case r := <-results:
return r, nil
case <-ctx.Done():
return Result{}, ctx.Err()
}
}Quan la primera goroutine envia el seu resultat i la funció retorna, defer cancel() cancel·la el context. Les goroutines que encara estaven esperant resposta de les seves rèpliques veuen el canal ctx.Done() tancar-se i poden alliberar recursos.
context.WithTimeout: deadline automàtic
context.WithTimeout és probablement la variant que més usaràs. Crea un context que es cancel·la automàticament després d’una durada:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := callExternalAPI(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println(\"l'API ha trigat més de 5 segons\")
}
return err
}La diferència amb WithCancel és que no necessites decidir quan cancel·lar. Defines el temps màxim i Go s’encarrega de la resta. Però continua sent obligatori cridar cancel amb defer. Si l’operació acaba en 100ms però el timeout era de 5 segons, sense defer cancel() el timer intern continua viu durant els 4.9 segons restants, consumint recursos.
El que molts no entenen al principi: el timeout s’aplica a tota la cadena d’operacions que usin aquest context. Si passes un context amb timeout de 5 segons a una funció que fa tres crides HTTP seqüencials, les tres comparteixen aquests 5 segons. No són 5 segons per a cadascuna.
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Les tres crides comparteixen els 5 segons
user, err := fetchUser(ctx, userID) // triga 2s
orders, err := fetchOrders(ctx, userID) // triga 2s
recommendations, err := fetchRecs(ctx, user) // li queden ~1s, probablement falliAixò és intencional. El timeout representa el pressupost de temps total per a l’operació, no per a cada pas individual. Si necessites timeouts individuals, crea contextos derivats:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) // global
defer cancel()
userCtx, userCancel := context.WithTimeout(ctx, 3*time.Second) // màx 3s per a user
defer userCancel()
user, err := fetchUser(userCtx, userID)
ordersCtx, ordersCancel := context.WithTimeout(ctx, 3*time.Second) // màx 3s per a orders
defer ordersCancel()
orders, err := fetchOrders(ordersCtx, userID)context.WithDeadline: límit de temps absolut
context.WithDeadline funciona igual que WithTimeout, però en lloc d’una durada, especifiques un moment exacte:
deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()A la pràctica, WithTimeout és sucre sintàctic sobre WithDeadline. Internament, context.WithTimeout(parent, d) és equivalent a context.WithDeadline(parent, time.Now().Add(d)).
Quan usaries WithDeadline directament? Quan el deadline ve de fora. Per exemple, si reps una capçalera HTTP que et diu “aquesta petició s’ha de completar abans de les 14:30:05 UTC”, uses WithDeadline amb aquell valor absolut.
Hi ha un detall important: un fill no pot estendre el deadline del seu pare. Si el pare expira en 5 segons i crees un fill amb WithTimeout(parent, 10*time.Second), el fill continuarà expirant en 5 segons. El context més restrictiu sempre guanya.
parent, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Això NO dona 10 segons. El fill hereta el deadline del pare (5s).
child, childCancel := context.WithTimeout(parent, 10*time.Second)
defer childCancel()Pots comprovar el deadline efectiu:
if deadline, ok := ctx.Deadline(); ok {
remaining := time.Until(deadline)
fmt.Printf(\"queden %v\n\", remaining)
}Propagant context a través de la teva aplicació
La regla fonamental: context sempre és el primer paràmetre d’una funció i es diu ctx. No és una convenció estètica, és l’estàndard de tot l’ecosistema Go.
// Correcte
func GetUser(ctx context.Context, id int64) (*User, error)
// Incorrecte
func GetUser(id int64, ctx context.Context) (*User, error)
// Incorrecte: mai guardis context en un struct
type Service struct {
ctx context.Context // NO facis això
}Guardar un context en un struct és un error que sembla còmode però trenca el model de propagació. Un context està lligat a una operació concreta (una petició HTTP, una execució d’un job). Si el guardes en un struct, aquell struct queda lligat a una operació que ja ha acabat, o pitjor, comparteixes un context entre operacions diferents.
La propagació correcta és lineal: cada capa de la teva aplicació rep el context i el passa a la següent.
// Handler → Service → Repository
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := h.service.FindUser(ctx, userID)
// ...
}
func (s *Service) FindUser(ctx context.Context, id int64) (*User, error) {
return s.repo.GetByID(ctx, id)
}
func (r *Repository) GetByID(ctx context.Context, id int64) (*User, error) {
row := r.db.QueryRowContext(ctx, \"SELECT id, name FROM users WHERE id = $1\", id)
// ...
}Si el client tanca la connexió HTTP, r.Context() es cancel·la, la cancel·lació es propaga al servei, del servei al repositori, i la consulta a la base de dades s’aborta. Tot sense que hagis d’escriure lògica explícita de cancel·lació a cada capa. El context ho fa per tu.
Context en HTTP handlers: context per petició
Cada petició HTTP en Go porta el seu propi context. L’obtens amb r.Context():
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Aquest context es cancel·la automàticament quan:
// 1. El client tanca la connexió
// 2. El servidor cancel·la la petició (per exemple, per timeout del servidor)
result, err := processRequest(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// El client se n'ha anat, no té sentit escriure resposta
return
}
http.Error(w, \"error intern\", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}Si uses un framework o middleware que afegeix timeouts, aquests s’apliquen al context de la petició. És habitual tenir un middleware que embolcalla cada petició amb un timeout màxim:
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}Amb r.WithContext(ctx) crees una còpia de la petició amb el nou context. Totes les funcions downstream que usin r.Context() veuran el timeout aplicat.
Un detall que genera bugs subtils: si el teu handler llança una goroutine per a treball en segon pla, no usis r.Context() per a aquella goroutine. Quan la petició HTTP acaba, el context es cancel·la, i la teva goroutine de fons també es cancel·la.
func handler(w http.ResponseWriter, r *http.Request) {
// MALAMENT: aquesta goroutine es cancel·la quan acaba la petició HTTP
go sendAnalytics(r.Context(), event)
// BÉ: context independent per a treball en segon pla
go sendAnalytics(context.Background(), event)
w.WriteHeader(http.StatusOK)
}Context amb consultes a base de dades: pgx i database/sql
La llibreria estàndard database/sql suporta context en totes les seves operacions. Si uses pgx (el driver PostgreSQL més usat en Go), el suport és fins i tot millor.
Amb database/sql:
func (r *Repository) GetUser(ctx context.Context, id int64) (*User, error) {
query := \"SELECT id, name, email FROM users WHERE id = $1\"
var user User
err := r.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf(\"timeout consultant usuari %d: %w\", id, err)
}
return nil, fmt.Errorf(\"error consultant usuari %d: %w\", id, err)
}
return &user, nil
}Amb pgx directament:
func (r *Repository) GetUser(ctx context.Context, id int64) (*User, error) {
query := \"SELECT id, name, email FROM users WHERE id = $1\"
var user User
err := r.pool.QueryRow(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, fmt.Errorf(\"error consultant usuari %d: %w\", id, err)
}
return &user, nil
}pgx usa el context com a primer paràmetre en lloc de tenir mètodes separats *Context. És un disseny més net.
La clau aquí: quan el context es cancel·la, la base de dades rep un senyal per abortar la consulta. No és només que el teu codi Go deixi d’esperar la resposta; PostgreSQL realment cancel·la la query al servidor. Això és fonamental per a consultes pesades. Sense context, una query que triga 60 segons continua consumint recursos al servidor de base de dades encara que el client ja se n’hagi anat.
Transaccions amb context:
func (r *Repository) TransferFunds(ctx context.Context, from, to int64, amount float64) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, \"UPDATE accounts SET balance = balance - $1 WHERE id = $2\", amount, from)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, \"UPDATE accounts SET balance = balance + $1 WHERE id = $2\", amount, to)
if err != nil {
return err
}
return tx.Commit()
}Si el context es cancel·la entre els dos UPDATE, la transacció s’aborta i fa rollback. No et quedes amb un estat inconsistent. Això és alguna cosa que en altres llenguatges has d’implementar manualment; en Go, propagar el context t’ho dona de franc.
Context en goroutines i workers
Els worker pools en Go necessiten context per a dues coses: saber quan parar i respectar els timeouts del sistema.
Un worker bàsic que respecta la cancel·lació:
func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result) {
for {
select {
case <-ctx.Done():
fmt.Printf(\"worker %d: parant (%v)\n\", id, ctx.Err())
return
case job, ok := <-jobs:
if !ok {
return // canal tancat, no hi ha més feina
}
result := process(ctx, job)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}Un pool de workers amb cancel·lació graceful:
func RunWorkerPool(ctx context.Context, numWorkers int, jobs <-chan Job) []Result {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan Result, numWorkers)
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(ctx, id, jobs, results)
}(i)
}
// Tancar results quan tots els workers acabin
go func() {
wg.Wait()
close(results)
}()
var collected []Result
for r := range results {
collected = append(collected, r)
}
return collected
}El patró és sempre el mateix: el select amb ctx.Done() en qualsevol punt on el worker pugui bloquejar-se. Si només el poses en rebre el job però no en enviar el resultat, el worker pot quedar-se bloquejat a results <- result si ningú llegeix el canal.
Per a tasques periòdiques (cron-like), el context controla quan parar el loop:
func periodicTask(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := doWork(ctx); err != nil {
log.Printf(\"error en tasca periòdica: %v\", err)
}
}
}
}Sense context, no tens una manera neta de dir-li a aquest loop que pari. Acabes usant flags booleans atòmics o canals manuals, que és exactament el que context abstreu.
Context values: quan usar-los (poques vegades) i quan no
context.WithValue permet adjuntar parells clau-valor a un context:
type contextKey string
const requestIDKey contextKey = \"requestID\"
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func GetRequestID(ctx context.Context) string {
id, ok := ctx.Value(requestIDKey).(string)
if !ok {
return \"\"
}
return id
}El tipus contextKey privat és obligatori. Si uses string directament com a clau, qualsevol paquet que usi el mateix string pot col·lisionar. Amb un tipus propi no exportat, només el teu paquet pot accedir al valor.
Quan té sentit usar context.WithValue:
- Request IDs i trace IDs per a logging i observabilitat.
- Informació d’autenticació (l’usuari autenticat de la petició).
- Metadades que creuen fronteres entre paquets i que no pertanyen a la signatura de la funció.
Quan no usar-lo:
- Paràmetres de negoci. Si una funció necessita un
userID, posa’l com a paràmetre explícit. No l’amaguis al context. - Dependències. Mai posis un logger, una connexió a base de dades o un servei al context.
- Qualsevol cosa que la funció necessiti per funcionar correctament. Si sense aquell valor la funció falla, hauria de ser un paràmetre obligatori amb tipus concret.
La regla pràctica: si treus el valor del context i la funció hauria de seguir compilant i funcionant (potser amb menys informació als logs), aleshores està bé al context. Si sense el valor la funció no pot fer la seva feina, és un paràmetre de la funció.
Un middleware típic que injecta valors al context:
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get(\"X-Request-ID\")
if id == \"\" {
id = uuid.New().String()
}
ctx := WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}I downstream:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
reqID := GetRequestID(r.Context())
log.Printf(\"[%s] processant GetUser\", reqID)
// ...
}Errors comuns: ignorar context i no propagar-lo
He vist aquests patrons més vegades de les que m’agradaria:
1. Rebre context i no usar-lo
// MALAMENT: accepta ctx però usa http.DefaultClient (que ignora el context)
func fetchData(ctx context.Context, url string) ([]byte, error) {
resp, err := http.Get(url) // http.Get no usa el teu ctx
return io.ReadAll(resp.Body)
}
// BÉ: usa el context a la petició
func fetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}http.Get crea una petició sense context. Si uses http.Get en una funció que rep ctx, estàs mentint a la signatura: promets que respectes la cancel·lació però no ho fas. Sempre usa http.NewRequestWithContext.
2. No propagar context entre capes
// MALAMENT: el service crea el seu propi context, ignorant el del handler
func (s *Service) Process(ctx context.Context, data Data) error {
dbCtx := context.Background() // Per què? Això ignora el timeout del handler
return s.repo.Save(dbCtx, data)
}
// BÉ: propaga el context del handler
func (s *Service) Process(ctx context.Context, data Data) error {
return s.repo.Save(ctx, data)
}Si crees un context.Background() nou al mig de la cadena, trenques tota la propagació. El handler pot tenir un timeout de 10 segons, però el teu repositori no ho sap perquè li has passat un context sense deadline.
3. No comprovar la cancel·lació en loops
// MALAMENT: si ctx es cancel·la, aquest loop continua processant milers d'items
func processAll(ctx context.Context, items []Item) error {
for _, item := range items {
if err := process(item); err != nil {
return err
}
}
return nil
}
// BÉ: comprova la cancel·lació periòdicament
func processAll(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := process(item); err != nil {
return err
}
}
return nil
}El select amb default és no-bloquejant: comprova si el context està cancel·lat i, si no, continua immediatament. El cost és mínim però t’estalvia processar milers d’items innecessaris quan la petició ja no importa.
4. Oblidar defer cancel()
// MALAMENT: leak del timer intern
func doSomething() {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
callAPI(ctx)
}
// BÉ
func doSomething() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
callAPI(ctx)
}El linter go vet t’avisarà d’això. Si no tens linting configurat, configura’l. És massa fàcil oblidar el cancel.
Exemple pràctic: HTTP handler → service → repository amb context
Ajuntem-ho tot en un exemple realista. Un endpoint que busca un usuari, consulta les seves comandes, i retorna una resposta combinada. El handler aplica un timeout global, i cada operació respecta la cancel·lació.
Comencem amb el repositori:
package repository
import (
\"context\"
\"database/sql\"
\"fmt\"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*User, error) {
query := \"SELECT id, name, email FROM users WHERE id = $1\"
var user User
err := r.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, fmt.Errorf(\"user %d: %w\", id, err)
}
return &user, nil
}
type OrderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) GetByUserID(ctx context.Context, userID int64) ([]Order, error) {
query := \"SELECT id, product, amount FROM orders WHERE user_id = $1 ORDER BY id DESC LIMIT 10\"
rows, err := r.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf(\"orders for user %d: %w\", userID, err)
}
defer rows.Close()
var orders []Order
for rows.Next() {
var o Order
if err := rows.Scan(&o.ID, &o.Product, &o.Amount); err != nil {
return nil, err
}
orders = append(orders, o)
}
return orders, rows.Err()
}El servei orquestra les dues consultes. Si alguna falla o el context es cancel·la, retorna error:
package service
import (
\"context\"
\"fmt\"
)
type UserService struct {
users *repository.UserRepository
orders *repository.OrderRepository
}
func NewUserService(users *repository.UserRepository, orders *repository.OrderRepository) *UserService {
return &UserService{users: users, orders: orders}
}
func (s *UserService) GetUserWithOrders(ctx context.Context, userID int64) (*UserProfile, error) {
user, err := s.users.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf(\"fetching user: %w\", err)
}
orders, err := s.orders.GetByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf(\"fetching orders: %w\", err)
}
return &UserProfile{
User: *user,
Orders: orders,
}, nil
}I el handler HTTP, que aplica un timeout de 5 segons a tota l’operació:
package handler
import (
\"context\"
\"encoding/json\"
\"errors\"
\"log\"
\"net/http\"
\"strconv\"
\"time\"
)
type UserHandler struct {
service *service.UserService
}
func NewUserHandler(service *service.UserService) *UserHandler {
return &UserHandler{service: service}
}
func (h *UserHandler) GetUserProfile(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
reqID := GetRequestID(ctx) // del middleware
userID, err := strconv.ParseInt(r.PathValue(\"id\"), 10, 64)
if err != nil {
http.Error(w, \"id invàlid\", http.StatusBadRequest)
return
}
profile, err := h.service.GetUserWithOrders(ctx, userID)
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
log.Printf(\"[%s] timeout obtenint perfil d'usuari %d\", reqID, userID)
http.Error(w, \"timeout\", http.StatusGatewayTimeout)
case errors.Is(err, context.Canceled):
// El client s'ha desconnectat, no cal respondre
log.Printf(\"[%s] client desconnectat per a l'usuari %d\", reqID, userID)
default:
log.Printf(\"[%s] error obtenint perfil d'usuari %d: %v\", reqID, userID, err)
http.Error(w, \"error intern\", http.StatusInternalServerError)
}
return
}
w.Header().Set(\"Content-Type\", \"application/json\")
json.NewEncoder(w).Encode(profile)
}Observa com funciona el flux:
- El handler crea un context amb timeout de 5 segons, derivat del context de la petició HTTP.
- Aquell mateix context es passa al servei, que el passa als repositoris.
- Els repositoris el passen a
database/sql, que l’usa per limitar les queries. - Si la base de dades triga més de 5 segons en total (sumant ambdues queries), el context expira i es retorna un 504.
- Si el client tanca la connexió,
r.Context()es cancel·la, la qual cosa cancel·la el context fill amb timeout, la qual cosa cancel·la les queries.
Cada capa fa una sola cosa: rebre el context, usar-lo, propagar-lo. No hi ha flags booleans de “shouldStop”, no hi ha canals manuals de “done”. El context ho gestiona tot.
Quan no necessites context
No tot necessita un context. Funcions pures que fan càlculs, transformacions de dades en memòria, validacions… si l’operació no pot bloquejar-se i no interactua amb I/O, no necessita ctx context.Context. Afegir-lo per sistema només contamina la teva API.
La pregunta és simple: pot aquesta funció bloquejar-se esperant alguna cosa externa (xarxa, disc, canal)? Si sí, necessita context. Si no, no el necessita.
// No necessita context: és un càlcul en memòria
func CalculateDiscount(price float64, percentage int) float64 {
return price * (1 - float64(percentage)/100)
}
// Sí necessita context: fa I/O
func FetchPrice(ctx context.Context, productID string) (float64, error) {
// ...
}La cerimònia que et salva en producció
Quan vaig començar amb Go, el context em semblava excessivament cerimonial. Sempre primer paràmetre, sempre anomenat ctx, sempre defer cancel() en crear contextos amb WithCancel, WithTimeout o WithDeadline. Mai guardar-lo en un struct, propagar-lo a totes les operacions d’I/O, comprovar ctx.Done() en loops i goroutines. I context.WithValue només per a metadades transversals com request ID o trace ID, mai per a paràmetres de negoci.
Aquella opinió va canviar quan vaig tenir un servei amb 20 endpoints, cridant a 5 APIs externes amb una base de dades al darrere. Cada petició té un mecanisme automàtic de “para-ho tot si això ja no importa”, i això és exactament el que necessites quan un client es desconnecta a meitat d’una cadena d’operacions. Sense context, hauries hagut d’inventar flags booleans, canals manuals de done, o simplement deixar que les operacions acabessin malgastant recursos. És una d’aquelles decisions de disseny de Go que no aprecies fins que et salva d’un incident a les tres de la matinada.
Per veure-ho en acció amb APIs reals, revisa l’article sobre com construir una API REST amb Go. Si vols aprofundir en com el context interactua amb la concurrència en Go o amb PostgreSQL amb Go, aquells articles cobreixen els detalls específics de cada cas.


