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.

Cover for Go i Gin: quan fer servir un framework i quan quedar-te amb la biblioteca estàndard

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 comprovar r.Method dins 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() i c.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 servir context.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. A net/http necessites un ResponseWriter wrapper 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):

Aspectenet/httpGin
Routing overhead~200 ns/op~150 ns/op
JSON encodingIgual (encoding/json)Igual (encoding/json)
Memòria per request~2-3 KB~3-5 KB (Context de Gin)
Throughput sota càrregaExcel·lentExcel·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:

  1. Comença amb net/http pur. Munta una API amb quatre o cinc endpoints, un parell de middlewares i gestió d’errors.
  2. Identifica el boilerplate. Si escrius la mateixa validació a cada handler, si necessites wrappejar el ResponseWriter per a logging, si la composició de middleware es torna incòmoda, pren nota.
  3. 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.
  4. Prova Gin. Si necessites binding declaratiu, validació integrada i un sistema de middleware més complet, Gin és l’opció més madura.
  5. 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:

EscenariRecomanació
Servei intern, <10 endpointsnet/http
API pública, 20+ endpointsGin o Chi
Biblioteca que exposa HTTPnet/http (sempre)
Equip nou a Gonet/http per aprendre, llavors avaluar
Equip gran, rotació freqüentGin (convencions imposades > convencions implícites)
Microservei cloud-native lleugernet/http o Chi
API amb validació complexa d’entradaGin

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.

Articles relacionats

OshyTech

Enginyeria backend i de dades orientada a sistemes escalables, automatització i IA.

Navegació

Copyright 2026 OshyTech. Tots els drets reservats