Go i Gin: quan fer servir un framework i quan quedar-te amb la biblioteca estàndard
Comparació entre net/http i Gin a Go. Routing, middlewares, validació i quan val la pena afegir un framework.

A Python tries Flask o FastAPI. A Java, Spring Boot. A Node, Express o Fastify. Arribes a Go i la primera pregunta és diferent: necessites un framework? La resposta no és òbvia, i equivocar-te pot portar-te a afegir dependències innecessàries o a reinventar la roda amb la biblioteca estàndard. Vaig a comparar net/http i Gin amb codi real perquè tinguis criteri i decideixis amb dades, no amb inèrcia d’altres ecosistemes.
Go té una biblioteca estàndard potent. Des de Go 1.22, el ServeMux suporta patrons de rutes amb mètodes HTTP i path parameters. Això va canviar la conversa. Abans, fer servir net/http pur per a una API REST era incòmode. Ara, per a molts casos, és suficient. Gin continua aportant valor, però en un rang de problemes més concret que fa dos anys.
net/http a Go 1.22+: el nou ServeMux
Abans de Go 1.22, el router estàndard era bàsic. No distingia mètodes HTTP i no suportava path parameters. Si volies GET /users/{id}, havies de parsejar la URL a mà o fer servir una biblioteca externa. Això empenyia molta gent cap a frameworks sense plantejar-se si realment els necessitaven.
Go 1.22 va canviar això. El nou ServeMux suporta:
- Mètodes HTTP a la ruta:
\"GET /users/{id}\"en comptes de comprovarr.Methoddins el handler. - Path parameters:
r.PathValue(\"id\")extreu el valor directament. - Wildcards:
\"GET /files/{path...}\"captura la resta de la ruta. - Precedència explícita: les rutes més específiques guanyen sobre les més generals.
package main
import (
\"encoding/json\"
\"log\"
\"net/http\"
\"strconv\"
)
type Task struct {
ID int `json:\"id\"`
Title string `json:\"title\"`
Done bool `json:\"done\"`
}
var tasks = map[int]Task{
1: {ID: 1, Title: \"Revisar PR\", Done: false},
2: {ID: 2, Title: \"Deploy a staging\", Done: true},
}
func getTasks(w http.ResponseWriter, r *http.Request) {
result := make([]Task, 0, len(tasks))
for _, t := range tasks {
result = append(result, t)
}
w.Header().Set(\"Content-Type\", \"application/json\")
json.NewEncoder(w).Encode(result)
}
func getTask(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue(\"id\"))
if err != nil {
http.Error(w, \"Invalid task ID\", http.StatusBadRequest)
return
}
task, exists := tasks[id]
if !exists {
http.Error(w, \"Task not found\", http.StatusNotFound)
return
}
w.Header().Set(\"Content-Type\", \"application/json\")
json.NewEncoder(w).Encode(task)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc(\"GET /tasks\", getTasks)
mux.HandleFunc(\"GET /tasks/{id}\", getTask)
log.Println(\"Servidor en :8080\")
log.Fatal(http.ListenAndServe(\":8080\", mux))
}Això és un servidor HTTP funcional amb routing per mètode i path parameters. Zero dependències externes. Sense framework. El codi és explícit, fàcil de seguir i production-ready.
Si véns de construir una API REST amb Go, aquest patró et resultarà familiar. La biblioteca estàndard post-1.22 cobreix la majoria de necessitats de routing sense necessitat de res més.
Què aporta Gin a sobre de net/http
Gin és el framework web més popular de Go. Té més de 80.000 estrelles a GitHub i un ecosistema ampli. Però la pregunta rellevant no és si és popular, sinó quins problemes concrets resol que net/http no resol.
Router basat en radix tree
El router de Gin fa servir un radix tree (httprouter per sota), que és més eficient que el pattern matching del ServeMux estàndard. A la pràctica, la diferència de rendiment en routing és irrellevant per a la majoria d’APIs. On sí importa és en l’API del router:
r := gin.Default()
r.GET(\"/tasks\", getTasks)
r.GET(\"/tasks/:id\", getTask)
r.POST(\"/tasks\", createTask)
r.PUT(\"/tasks/:id\", updateTask)
r.DELETE(\"/tasks/:id\", deleteTask)
// Grups de rutes
api := r.Group(\"/api/v1\")
{
api.GET(\"/users\", getUsers)
api.GET(\"/users/:id\", getUser)
}Els grups de rutes (r.Group) són útils quan tens un prefix compartit o vols aplicar middleware a un conjunt d’endpoints. A net/http, pots fer quelcom similar amb http.StripPrefix o composant handlers manualment, però és més verbós.
Binding i validació de paràmetres
Aquí és on Gin comença a aportar valor real. Gin pot bindear automàticament query params, path params, headers i body JSON a structs amb validació integrada:
type CreateTaskRequest struct {
Title string `json:\"title\" binding:\"required,min=1,max=200\"`
Description string `json:\"description\" binding:\"max=1000\"`
Priority int `json:\"priority\" binding:\"required,min=1,max=5\"`
}
func createTask(c *gin.Context) {
var req CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{\"error\": err.Error()})
return
}
// req està validat, pots fer-lo servir directament
c.JSON(http.StatusCreated, gin.H{\"title\": req.Title, \"priority\": req.Priority})
}El tag binding fa servir go-playground/validator per sota. Pots validar required, min, max, email, url, expressions regulars i molt més. Tot declaratiu al struct.
A net/http pur, la validació és manual:
func createTask(w http.ResponseWriter, r *http.Request) {
var req CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, \"Invalid JSON\", http.StatusBadRequest)
return
}
if req.Title == \"\" {
http.Error(w, \"Title is required\", http.StatusBadRequest)
return
}
if len(req.Title) > 200 {
http.Error(w, \"Title too long\", http.StatusBadRequest)
return
}
if req.Priority < 1 || req.Priority > 5 {
http.Error(w, \"Priority must be between 1 and 5\", http.StatusBadRequest)
return
}
// ...
}Funciona perfectament. Però quan tens vint endpoints amb structs d’entrada diferents, la validació manual es converteix en boilerplate repetitiu que a més és fàcil de deixar incomplet. El binding de Gin elimina aquesta classe d’errors.
Cadena de middlewares
Gin té un sistema de middlewares amb un ordre d’execució clar i la capacitat d’avortar la cadena en qualsevol punt:
r := gin.New()
// Middlewares globals
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(corsMiddleware())
// Middleware per grup
admin := r.Group(\"/admin\")
admin.Use(authRequired())
{
admin.GET(\"/stats\", getStats)
admin.DELETE(\"/users/:id\", deleteUser)
}El mètode c.Abort() atura la cadena, c.Next() passa al següent middleware i c.Set()/c.Get() permet compartir dades entre middlewares i handlers dins el mateix request.
Respostes JSON simplificades
Gin proporciona helpers per a les respostes més comunes:
// Gin
c.JSON(http.StatusOK, gin.H{\"message\": \"ok\"})
c.JSON(http.StatusOK, user)
c.String(http.StatusOK, \"Hello %s\", name)
c.XML(http.StatusOK, data)
// net/http equivalent
w.Header().Set(\"Content-Type\", \"application/json\")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{\"message\": \"ok\"})Menys línies, sí. Però si només fas servir JSON (que és el 95% de les APIs modernes), pots escriure un helper de tres línies per a net/http i oblidar-te’n:
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set(\"Content-Type\", \"application/json\")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}Comparació costat a costat: el mateix endpoint
Anem a implementar el mateix endpoint complet en ambdós: un POST /tasks que rep JSON, valida els camps, crea la tasca i retorna la resposta.
net/http
package main
import (
\"encoding/json\"
\"fmt\"
\"net/http\"
\"sync\"
)
type Task struct {
ID int `json:\"id\"`
Title string `json:\"title\"`
Description string `json:\"description\"`
Priority int `json:\"priority\"`
}
type CreateTaskRequest struct {
Title string `json:\"title\"`
Description string `json:\"description\"`
Priority int `json:\"priority\"`
}
var (
tasks = make(map[int]Task)
nextID = 1
tasksMu sync.Mutex
)
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set(\"Content-Type\", \"application/json\")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{\"error\": msg})
}
func createTask(w http.ResponseWriter, r *http.Request) {
var req CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, \"Invalid JSON body\")
return
}
// Validació manual
if req.Title == \"\" {
writeError(w, http.StatusBadRequest, \"title is required\")
return
}
if len(req.Title) > 200 {
writeError(w, http.StatusBadRequest, \"title must be 200 characters or less\")
return
}
if req.Priority < 1 || req.Priority > 5 {
writeError(w, http.StatusBadRequest, \"priority must be between 1 and 5\")
return
}
tasksMu.Lock()
task := Task{
ID: nextID,
Title: req.Title,
Description: req.Description,
Priority: req.Priority,
}
tasks[nextID] = task
nextID++
tasksMu.Unlock()
writeJSON(w, http.StatusCreated, task)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc(\"POST /tasks\", createTask)
http.ListenAndServe(\":8080\", mux)
}Gin
package main
import (
\"net/http\"
\"sync\"
\"github.com/gin-gonic/gin\"
)
type Task struct {
ID int `json:\"id\"`
Title string `json:\"title\"`
Description string `json:\"description\"`
Priority int `json:\"priority\"`
}
type CreateTaskRequest struct {
Title string `json:\"title\" binding:\"required,max=200\"`
Description string `json:\"description\"`
Priority int `json:\"priority\" binding:\"required,min=1,max=5\"`
}
var (
tasks = make(map[int]Task)
nextID = 1
tasksMu sync.Mutex
)
func createTask(c *gin.Context) {
var req CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{\"error\": err.Error()})
return
}
tasksMu.Lock()
task := Task{
ID: nextID,
Title: req.Title,
Description: req.Description,
Priority: req.Priority,
}
tasks[nextID] = task
nextID++
tasksMu.Unlock()
c.JSON(http.StatusCreated, task)
}
func main() {
r := gin.Default()
r.POST(\"/tasks\", createTask)
r.Run(\":8080\")
}La diferència principal no és la quantitat de línies. És que a la versió Gin, la validació està declarada al struct i s’executa automàticament en fer ShouldBindJSON. A net/http, la validació és codi imperatiu que has d’escriure, mantenir i assegurar-te que no et deixes cap camp. Amb un endpoint és manejable. Amb trenta, la diferència és real.
Middlewares: net/http vs Gin
Els middlewares són una peça central de qualsevol API. Logging, autenticació, CORS, rate limiting, recovery de panics. Ambdues opcions suporten middlewares, però amb ergonomia diferent.
Middleware a net/http
A net/http, un middleware és una funció que rep un http.Handler i retorna un altre http.Handler. És composició de funcions pura:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf(\"%s %s %v\", r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(\"Authorization\")
if token == \"\" {
http.Error(w, \"Unauthorized\", http.StatusUnauthorized)
return // No cridem next: la cadena s'atura
}
// Validar token...
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc(\"GET /tasks\", getTasks)
mux.HandleFunc(\"POST /tasks\", createTask)
// Composar middlewares (s'executen de fora cap a dins)
handler := loggingMiddleware(authMiddleware(mux))
http.ListenAndServe(\":8080\", handler)
}El patró és elegant i no necessita framework. Però la composició niada es complica quan tens cinc o sis middlewares. I si vols aplicar middleware a rutes específiques (no globals), has de composar manualment.
Per passar dades entre middlewares, fas servir context.WithValue:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(\"Authorization\")
userID, err := validateToken(token)
if err != nil {
http.Error(w, \"Unauthorized\", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), \"userID\", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getProfile(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(\"userID\").(int) // Type assertion necessària
// ...
}Funciona, però el context.Value no té type safety. Necessites type assertions i les claus són any, cosa que obre la porta a errors subtils. La convenció és fer servir tipus custom com a claus per evitar col·lisions, però és responsabilitat del desenvolupador.
Si vols aprofundir en patrons de middlewares a Go, tinc un article on ho exploro en detall.
Middleware a Gin
Gin té un model de middleware més estructurat:
func loggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // Executar la resta de la cadena
log.Printf(\"%s %s %d %v\",
c.Request.Method,
c.Request.URL.Path,
c.Writer.Status(),
time.Since(start),
)
}
}
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader(\"Authorization\")
if token == \"\" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})
return
}
userID, err := validateToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"invalid token\"})
return
}
c.Set(\"userID\", userID)
c.Next()
}
}
func main() {
r := gin.New()
r.Use(loggingMiddleware())
r.Use(gin.Recovery())
public := r.Group(\"/\")
{
public.GET(\"/health\", healthCheck)
}
protected := r.Group(\"/api\")
protected.Use(authMiddleware())
{
protected.GET(\"/profile\", getProfile)
protected.GET(\"/tasks\", getTasks)
}
r.Run(\":8080\")
}
func getProfile(c *gin.Context) {
userID, _ := c.Get(\"userID\")
// ...
}Els avantatges de Gin aquí són clars:
c.Next()ic.Abort(): control explícit del flux de la cadena. Pots executar lògica abans i després del handler.c.Set()/c.Get(): compartir dades entre middlewares sense fer servircontext.Value. Continua sense tenir type safety complet, però l’API és més directa.- Grups amb middleware:
r.Group(\"/api\").Use(authMiddleware())aplica middleware només a un subconjunt de rutes de forma declarativa. c.Writer.Status(): accés al status code de la resposta al middleware de logging. Anet/httpnecessites unResponseWriterwrapper per capturar això.
La clau: a net/http, un middleware que necessita llegir el status code de la resposta requereix wrappejar el ResponseWriter amb un struct custom. A Gin ja ho tens disponible.
// net/http: ResponseWriter wrapper per capturar l'status
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.statusCode = code
rr.ResponseWriter.WriteHeader(code)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rr := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
start := time.Now()
next.ServeHTTP(rr, r)
log.Printf(\"%s %s %d %v\", r.Method, r.URL.Path, rr.statusCode, time.Since(start))
})
}No és difícil, però és boilerplate que Gin t’estalvia. I si no ho implementes, el teu middleware de logging no pot registrar el status code. A Gin és gratuït.
JSON: serialització, deserialització i errors
El maneig de JSON és el pa de cada dia d’una API REST. Ambdues opcions cobreixen el cas, però amb diferències en ergonomia.
net/http
// Deserialitzar
func createTask(w http.ResponseWriter, r *http.Request) {
var req CreateTaskRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Rebutjar camps no esperats
if err := decoder.Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, \"Invalid JSON: \"+err.Error())
return
}
// Validar manualment...
}
// Serialitzar
func getTask(w http.ResponseWriter, r *http.Request) {
// ...
w.Header().Set(\"Content-Type\", \"application/json\")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(task); err != nil {
log.Printf(\"Error encoding response: %v\", err)
}
}El detall de DisallowUnknownFields() és important. Per defecte, encoding/json ignora camps desconeguts al body. Això pot ser un problema si un client envia un camp amb un typo (priorty en comptes de priority) i ningú se n’adona.
Gin
// Deserialitzar amb binding
func createTask(c *gin.Context) {
var req CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{\"error\": err.Error()})
return
}
// req ja està validat pels tags binding
}
// Serialitzar
func getTask(c *gin.Context) {
// ...
c.JSON(http.StatusOK, task)
}Gin simplifica la serialització amb c.JSON(). Una línia, content-type inclòs. Per a deserialització, ShouldBindJSON combina decode i validació.
Binding de diferents fonts
On Gin aporta valor real és quan necessites extreure dades de múltiples fonts en un mateix request:
type SearchTasksRequest struct {
Status string `form:\"status\" binding:\"omitempty,oneof=pending done\"`
Priority int `form:\"priority\" binding:\"omitempty,min=1,max=5\"`
Page int `form:\"page\" binding:\"min=1\"`
Limit int `form:\"limit\" binding:\"min=1,max=100\"`
}
func searchTasks(c *gin.Context) {
var req SearchTasksRequest
req.Page = 1 // Valor per defecte
req.Limit = 20
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{\"error\": err.Error()})
return
}
// req.Status, req.Priority, req.Page, req.Limit ja estan parsejats i validats
}ShouldBindQuery parseja query parameters. ShouldBindUri parseja path parameters. ShouldBindHeader parseja headers. Tot amb el mateix sistema de validació declarativa. A net/http hauríes de fer r.URL.Query().Get(\"status\") per a cada camp, convertir tipus manualment i validar cadascun.
// net/http equivalent per a query params
func searchTasks(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get(\"status\")
if status != \"\" && status != \"pending\" && status != \"done\" {
writeError(w, http.StatusBadRequest, \"status must be pending or done\")
return
}
page := 1
if p := r.URL.Query().Get(\"page\"); p != \"\" {
var err error
page, err = strconv.Atoi(p)
if err != nil || page < 1 {
writeError(w, http.StatusBadRequest, \"invalid page\")
return
}
}
limit := 20
if l := r.URL.Query().Get(\"limit\"); l != \"\" {
var err error
limit, err = strconv.Atoi(l)
if err != nil || limit < 1 || limit > 100 {
writeError(w, http.StatusBadRequest, \"limit must be between 1 and 100\")
return
}
}
// ... fer servir status, page, limit
}La diferència és evident. No és que no es pugui fer amb net/http. És que amb vint endpoints amb filtres diferents, el binding declaratiu de Gin t’estalvia temps i errors.
Rendiment: tots dos són ràpids
Aquest punt és curt perquè la conclusió és simple: no tries entre net/http i Gin per rendiment. Tots dos són ràpids.
El router de Gin (basat en httprouter) és marginalment més eficient per a routing pur perquè fa servir un radix tree en lloc del pattern matching del ServeMux. En benchmarks sintètics, la diferència existeix. En una API real on el coll d’ampolla és la base de dades, una crida a un altre servei o la serialització de la resposta, la diferència de routing és irrellevant.
Nombres reals (orientatius, depenen del maquinari i la càrrega):
| Aspecte | net/http | Gin |
|---|---|---|
| Routing overhead | ~200 ns/op | ~150 ns/op |
| JSON encoding | Igual (encoding/json) | Igual (encoding/json) |
| Memòria per request | ~2-3 KB | ~3-5 KB (Context de Gin) |
| Throughput sota càrrega | Excel·lent | Excel·lent |
Gin afegeix una mica d’overhead pel seu Context i la cadena de middlewares, però estem parlant de microsegons. Si el rendiment de routing és el teu coll d’ampolla, tens problemes majors que l’elecció de framework.
La decisió entre net/http i Gin hauria de ser d’ergonomia i mantenibilitat, no de rendiment. Tots dos gestionen milers de peticions per segon sense esforç.
Quan net/http és suficient
La biblioteca estàndard és suficient (i preferible) en aquests escenaris:
Serveis interns amb pocs endpoints
Si el teu servei té cinc endpoints, un health check i un parell de middlewares, net/http és tot el que necessites. Afegir Gin seria incloure una dependència externa per estalviar-te unes poques línies de validació.
func main() {
mux := http.NewServeMux()
mux.HandleFunc(\"GET /health\", healthCheck)
mux.HandleFunc(\"GET /api/status\", getStatus)
mux.HandleFunc(\"POST /api/process\", processJob)
handler := withLogging(withAuth(mux))
http.ListenAndServe(\":8080\", handler)
}Cinc línies de setup. Zero dependències externes. Production-ready.
Biblioteques i eines reutilitzables
Si estàs escrivint una biblioteca Go que exposa un endpoint HTTP (un exporter de Prometheus, un webhook handler, un SDK), fer servir net/http és la decisió correcta. No vols forçar els teus usuaris a dependre de Gin. La interfície http.Handler de la biblioteca estàndard és universal.
Projectes on la mida del binari importa
Gin porta consigo diverses dependències (go-playground/validator, bytedance/sonic o encoding/json, ugorji/go…). Per a eines CLI o serveis embedded on cada megabyte compta, la biblioteca estàndard és més lleugera.
Aprendre Go
Si estàs aprenent Go, comença amb net/http. Entendre com funciona el Handler, el ResponseWriter, el Request i la composició de middlewares et farà millor desenvolupador Go. Gin abstreu coses que convé entendre abans de deixar que un framework les faci per tu.
Quan Gin val la pena
Gin aporta valor real en aquests casos:
APIs amb molts endpoints i validació complexa
Si la teva API té trenta endpoints, cadascun amb el seu struct d’entrada, filtres per query params, paginació i validació, el binding declaratiu de Gin t’estalvia centenars de línies de validació manual i redueix la superfície d’error.
Equips grans o amb rotació
El sistema de grups, middleware i binding de Gin estableix una convenció que és més fàcil de seguir que la composició manual de net/http. Quan algú nou s’incorpora a l’equip, sap on buscar les rutes, on estan els middlewares i com es valida l’entrada. És estructura imposada pel framework en comptes de convenció que cada persona pot implementar diferent.
APIs amb middleware complex
Si necessites middlewares que llegeixin el status code de la resposta, que executeixin lògica abans i després del handler, que avortin la cadena amb una resposta JSON estructurada, Gin et dona això out of the box. A net/http, pots fer-ho, però acabaràs escrivint abstraccions que s’assemblen sospitosament al que Gin ja fa.
Quan véns d’altres frameworks
Si el teu equip ve d’Express, Flask o Spring Boot, l’API de Gin els resultarà familiar. Grups de rutes, middleware chain, binding de paràmetres. La corba d’aprenentatge és menor que la composició funcional pura de net/http, que requereix entendre les interfícies del paquet estàndard.
Altres frameworks: Chi, Echo, Fiber
Gin no és l’única opció. Hi ha altres frameworks Go que val la pena conèixer.
Chi
Chi és un router lleuger compatible amb net/http. Això significa que els teus handlers continuen sent func(w http.ResponseWriter, r *http.Request), no funcions amb un context custom. Si vols millor routing i middleware però sense sortir de la interfície estàndard, Chi és l’opció més pragmàtica.
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route(\"/api/tasks\", func(r chi.Router) {
r.Get(\"/\", listTasks)
r.Post(\"/\", createTask)
r.Route(\"/{id}\", func(r chi.Router) {
r.Get(\"/\", getTask)
r.Put(\"/\", updateTask)
})
})Chi és la meva recomanació si vols un pas intermedi entre net/http pur i un framework complet com Gin. Obtens routing amb grups, un sistema de middleware madur i compatibilitat total amb l’ecosistema estàndard.
Echo
Echo és similar a Gin en filosofia: context custom, binding, validació, middlewares inclosos. El rendiment és comparable. L’elecció entre Gin i Echo és principalment de preferència d’API i ecosistema. Gin té més comunitat i més middleware de tercers. Echo té una documentació excel·lent.
Fiber
Fiber està basat en fasthttp, no en net/http. És el més ràpid en benchmarks sintètics, però aquesta velocitat ve amb un cost: no és compatible amb l’ecosistema estàndard de Go. Qualsevol middleware escrit per a net/http no funciona amb Fiber sense adaptadors. Qualsevol biblioteca que esperi un http.Request necessita conversió. Tret que el teu cas d’ús requereixi exprimir cada nanosegon de routing, el tradeoff no val la pena.
La meva recomanació: comença amb net/http, mou-te a Gin quan sentis el dolor
No és una resposta covarda. És l’aproximació que millor funciona a Go.
Go no és Java, on sense Spring Boot estàs perdut. Go no és Python, on sense FastAPI o Flask no tens estructura. La biblioteca estàndard de Go és un servidor HTTP de producció. Comença amb ella. Escriu els teus primers handlers, els teus primers middlewares, la teva validació manual. Entén com funciona http.Handler, com es composen funcions, com es fa servir el context.
Quan comencis a sentir el dolor —validació repetitiva, routing limitat, middleware boilerplate—, aleshores avalua Gin o Chi. En aquell punt sabràs exactament quin problema et resol el framework i quanta de la màgia estàs disposat a acceptar.
La seqüència que recomano:
- Comença amb
net/httppur. Munta una API amb quatre o cinc endpoints, un parell de middlewares i gestió d’errors. - Identifica el boilerplate. Si escrius la mateixa validació a cada handler, si necessites wrappejar el
ResponseWriterper a logging, si la composició de middleware es torna incòmoda, pren nota. - Prova Chi. Si el que et molesta és només el routing i la composició de middlewares, Chi et resol això sense sortir de l’estàndard.
- Prova Gin. Si necessites binding declaratiu, validació integrada i un sistema de middleware més complet, Gin és l’opció més madura.
- No miris enrere sense motiu. Un cop tries, queda’t fins que tinguis un motiu real per canviar.
Per a projectes nous amb un equip que ja coneix Go, el meu criteri és:
| Escenari | Recomanació |
|---|---|
| Servei intern, <10 endpoints | net/http |
| API pública, 20+ endpoints | Gin o Chi |
| Biblioteca que exposa HTTP | net/http (sempre) |
| Equip nou a Go | net/http per aprendre, llavors avaluar |
| Equip gran, rotació freqüent | Gin (convencions imposades > convencions implícites) |
| Microservei cloud-native lleuger | net/http o Chi |
| API amb validació complexa d’entrada | Gin |
Si vols veure això en pràctica amb un projecte complet, comença per construir una API REST amb Go fent servir net/http i després aplica el que hem vist aquí per decidir si Gin aporta valor en el teu cas. I si t’interessa com estructurar el testing, a testing a Go cobreixo els patrons que funcionen tant amb net/http com amb Gin.
Tria amb criteri, no amb dogma
A Go, la pregunta “necessito un framework?” té una resposta que en altres llenguatges no existeix: probablement no, almenys al principi. La biblioteca estàndard post-Go 1.22 és un servidor HTTP complet amb routing per mètode, path parameters i una interfície de composició que permet construir APIs de producció sense dependències externes.
Gin aporta valor real en escenaris concrets: validació declarativa amb binding de structs, sistema de middleware ergonòmic amb accés al status code, grups de rutes amb middleware selectiu i una API familiar per a equips que vénen de frameworks en altres llenguatges. No és que Gin sigui millor o pitjor que net/http. És que resol problemes diferents.
La pitjor decisió és triar Gin “perquè en altres llenguatges sempre faig servir un framework”. La segona pitjor és rebutjar Gin “perquè a Go no es fan servir frameworks” quan la teva API té quaranta endpoints i estàs escrivint la mateixa validació manual a cada handler. Tria amb criteri, no amb dogma.


