Effective Go explicat: com escriure Go idiomàtic sense complicar-te
Els principis de Effective Go traduïts a exemples moderns i pràctics. Com escriure Go idiomàtic per a backend real.

Effective Go es va publicar el 2009. Parts del document han envellit, hi ha seccions que assumeixen un món sense mòduls, sense generics, sense context.Context. Però crec que la filosofia que transmet segueix sent la millor guia per escriure Go que no lluiti contra el llenguatge. El problema, almenys com jo ho veig, és que molta gent el llegeix com una referència tècnica, quan en realitat és un manifest de disseny: et diu quin estil de codi intenta evitar Go i per què.
He tornat a llegir-lo diverses vegades al llarg dels anys. I és curiós, perquè cada vegada que ho faig, entenc alguna cosa que abans em semblava arbitrària. Aquest article és la meva traducció d’aquells principis a exemples moderns, aplicats al tipus de backend que escric avui. No és un resum literal del document. És el que em queda després d’aplicar-lo en projectes reals, amb els encerts i els errors que això implica.
Formatting: go fmt no és opcional
La primera secció de Effective Go parla de format. I la conclusió és radical: no hi ha debat sobre estil a Go. go fmt decideix, tu acceptes. Tabs, no espais. Claus a la mateixa línia. Fi de la discussió.
Això sembla trivial, i al principi jo també ho pensava. Però crec que és una de les decisions de disseny més intel·ligents del llenguatge. En altres ecosistemes gastes hores configurant linters, debatent si els imports van ordenats per tipus o per longitud, si les funcions duen una línia en blanc després del { o no. A Go, això no existeix. Tot el codi del món té el mateix format.
// Abans de go fmt (això no compila, però com a exemple visual)
func handler(w http.ResponseWriter,r *http.Request){
if r.Method!=\"POST\" {
http.Error(w,\"method not allowed\",405)
return
}
// ...
}
// Després de go fmt
func handler(w http.ResponseWriter, r *http.Request) {
if r.Method != \"POST\" {
http.Error(w, \"method not allowed\", 405)
return
}
// ...
}El benefici real no és estètic. És que qualsevol diff en un PR mostra només canvis de lògica, mai canvis de format. Les code reviews van més ràpid. Els merges tenen menys conflictes. I els desenvolupadors nous no necessiten llegir un document d’estil de 40 pàgines.
La meva regla: executa gofmt o goimports en guardar. Configura-ho al teu editor i oblida’t. Si algú de l’equip proposa “el seu propi estil”, la resposta és sempre la mateixa: no.
Naming: noms curts, paquets amb significat, zero getters
El naming a Go és radicalment diferent al de Java o C#. Si vens d’aquells mons, els noms curts et semblaran críptics al principi. A mi em va passar. Però tenen una raó que s’entén amb el temps.
Variables curtes en scope curt
A Go, una variable que viu tres línies no necessita dir-se userAccountBalance. Li poses b i tothom ho entén en context.
// Això és Go idiomàtic
for i, v := range items {
if v.IsActive() {
process(v)
}
}
// Això NO és Go idiomàtic
for index, currentItem := range itemsList {
if currentItem.IsActive() {
processItem(currentItem)
}
}La regla és simple: com més curt és el scope, més curt pot ser el nom. Un paràmetre de funció que s’usa una vegada pot ser r per a un *http.Request. Un camp de struct que s’exporta i viu per sempre necessita un nom complet i descriptiu.
Noms de paquets
El nom del paquet forma part de l’identificador quan el fas servir des de fora. Per això http.Server és millor que httpserver.HTTPServer. I json.Marshal és millor que jsonutil.MarshalJSON.
// Bé: el paquet dóna context
user.New(\"roger\", \"roger@example.com\")
order.Create(cart)
// Malament: redundància entre paquet i funció
user.NewUser(\"roger\", \"roger@example.com\")
order.CreateOrder(cart)Sense getters
Go no fa servir getters amb prefix Get. Si tens un camp name, el getter es diu Name(), no GetName(). El setter sí que porta prefix: SetName().
type Config struct {
timeout time.Duration
}
// Getter: sense \"Get\"
func (c *Config) Timeout() time.Duration {
return c.timeout
}
// Setter: amb \"Set\"
func (c *Config) SetTimeout(d time.Duration) {
c.timeout = d
}Això no és un caprici. És que config.Timeout() es llegeix com a anglès natural. config.GetTimeout() afegeix soroll sense aportar informació.
Control flow: retorns primerencs, evitar anidament
Effective Go insisteix en un patró que, almenys en la meva experiència, marca la diferència entre codi Go llegible i codi Go que sembla escrit en Java: l’early return.
La idea, que sona òbvia però costa interioritzar, és que el camí principal (happy path) del codi hauria d’estar al nivell d’indentació més baix possible. Els errors, les validacions, els casos especials es gestionen primer i surten de la funció com més aviat millor.
// MALAMENT: anidament innecessari
func processOrder(o *Order) error {
if o != nil {
if o.IsValid() {
if o.HasStock() {
// lògica principal aquí, a 3 nivells d'indentació
return nil
} else {
return fmt.Errorf(\"no stock for order %s\", o.ID)
}
} else {
return fmt.Errorf(\"invalid order %s\", o.ID)
}
} else {
return errors.New(\"order is nil\")
}
}
// BÉ: early return, happy path a l'esquerra
func processOrder(o *Order) error {
if o == nil {
return errors.New(\"order is nil\")
}
if !o.IsValid() {
return fmt.Errorf(\"invalid order %s\", o.ID)
}
if !o.HasStock() {
return fmt.Errorf(\"no stock for order %s\", o.ID)
}
// lògica principal aquí, sense anidament
return nil
}La versió amb early return és més llarga en línies però molt més fàcil de llegir. No has de fer l’exercici mental de “en quin nivell d’if estic?”. Cada condició d’error està aïllada, és fàcil de trobar i de modificar.
Ho aplico de forma gairebé religiosa, ho reconec. Si veig un else després d’un return, l’elimino. Si veig més de dos nivells d’indentació, busco com aplanar. De vegades em passo de purista, però en general crec que compensa.
Error handling: comprova immediatament, embolcalla amb context
I aquí és l’elefant a l’habitació. La gestió d’errors a Go és la part que genera més queixes, i sent honestos, alguna raó hi ha. El famós if err != nil que es repeteix fins a la sacietat.
Però Effective Go ho planteja d’una forma que, amb el temps, m’ha anat convencent: els errors són valors. Es retornen, es comproven, es propaguen. No s’ignoren.
// El patró bàsic
result, err := doSomething()
if err != nil {
return fmt.Errorf(\"doing something: %w\", err)
}El que Effective Go no cobreix (perquè es va escriure abans de Go 1.13) és el wrapping d’errors amb %w. Avui és essencial. Cada vegada que propagues un error, li afegeixes context sobre on ha ocorregut.
func GetUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRowContext(ctx, \"SELECT id, name, email FROM users WHERE id = $1\", id)
var u User
if err := row.Scan(&u.ID, &u.Name, &u.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf(\"user %s not found: %w\", id, ErrNotFound)
}
return nil, fmt.Errorf(\"scanning user %s: %w\", id, err)
}
return &u, nil
}Tres regles que intento seguir sempre (tot i que no sempre ho aconsegueixo a la primera):
- Comprova l’error immediatament després de la crida. No facis tres operacions i llavors comproves. Cada crida que pot fallar es comprova a la línia següent.
- Embolcalla amb context.
\"scanning user\"és infinitament més útil en un log que unsql: no rowssolt. T’estalvia hores de debugging. - No facis servir
_per ignorar errors tret que tinguis una raó documentada. Si creus que un error no pot ocórrer, pensa-ho una altra vegada.
Si vols aprofundir en patrons d’errors, tinc un article complet sobre errors a Go amb errors sentinella, tipus personalitzats i errors.Is/errors.As.
Interfícies: petites, definides al consumidor
Les interfícies a Go són implícites. No has de declarar implements. Si el teu tipus té els mètodes que la interfície requereix, la implementa. Punt.
Effective Go recomana interfícies petites. I amb el temps, crec que aquesta és una de les idees més potents del llenguatge. Una interfície amb un sol mètode és perfectament normal a Go. I una interfície amb deu mètodes és gairebé sempre un senyal que alguna cosa va malament.
// Interfície estàndard de la llibreria: un sol mètode
type Reader interface {
Read(p []byte) (n int, err error)
}
// Interfície del teu domini: també un sol mètode
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}Defineix-les on es consumeixen, no on s’implementen
Aquest és el principi que més costa interioritzar si vens de Java. A Java, defineixes la interfície junt amb la implementació. A Go, defineixes la interfície on la necessites.
// Al paquet \"order\" (consumidor)
package order
type PaymentProcessor interface {
Charge(ctx context.Context, amount int64, currency string) error
}
type Service struct {
payments PaymentProcessor
}
func NewService(p PaymentProcessor) *Service {
return &Service{payments: p}
}// Al paquet \"stripe\" (implementació)
package stripe
type Client struct {
apiKey string
}
func (c *Client) Charge(ctx context.Context, amount int64, currency string) error {
// crida a l'API de Stripe
return nil
}El paquet stripe no importa el paquet order. No sap que existeix una interfície PaymentProcessor. Simplement té un mètode Charge que coincideix. L’acoblament va en una sola direcció.
Això et dóna una flexibilitat enorme per a testing (mocks trivials), per canviar implementacions i per mantenir els paquets desacoblats. Ho explico en detall a l’article sobre interfícies a Go.
Concurrència: no comparteixis memòria, comunica
La frase més citada de l’ecosistema Go: “Don’t communicate by sharing memory; share memory by communicating.”
Effective Go dedica bastant espai a goroutines i channels. La idea central és que en comptes de protegir dades compartides amb mutexos (cosa propensa a errors i difícil de razonar), prefereixis enviar dades entre goroutines a través de channels. Dit així sona senzill, però la realitat té matisos.
// Patró: fan-out de tasques amb channel de resultats
func processItems(ctx context.Context, items []Item) ([]Result, error) {
results := make(chan Result, len(items))
errs := make(chan error, len(items))
for _, item := range items {
go func(it Item) {
r, err := process(ctx, it)
if err != nil {
errs <- err
return
}
results <- r
}(item)
}
var out []Result
for range items {
select {
case r := <-results:
out = append(out, r)
case err := <-errs:
return nil, fmt.Errorf(\"processing items: %w\", err)
case <-ctx.Done():
return nil, ctx.Err()
}
}
return out, nil
}De fet, hi ha matisos importants que Effective Go no cobreix amb la profunditat que necessites avui:
- No llancis goroutines sense control. Sempre hauries de saber quan acaben.
sync.WaitGroupo un channel de senyalització. - Fes servir
context.Contextper a cancel·lació. Tota goroutine que faci I/O hauria de rebre un context i respectar-lo. - Els channels no són la solució per a tot. Si necessites un comptador atòmic, fes servir
sync/atomic. Si necessites protegir un mapa d’accés concurrent,sync.RWMutexés més clar que un channel.
La regla pràctica: fes servir channels quan necessites comunicar dades entre goroutines. Fes servir mutexos quan necessites protegir estat compartit simple. No forces un channel on un mutex és més clar.
Composició sobre herència: embedding, no extensió
Go no té herència. No hi ha classes, no hi ha extends, no hi ha jerarquies de tipus. Això és intencional, i al principi pot resultar desconcertant si vens de llenguatges OOP. Effective Go ho defensa amb el concepte d’embedding.
L’embedding et permet incloure un tipus dins d’un altre, promocionant els seus mètodes automàticament. Però no és herència, tot i que a primera vista ho pugui semblar. No hi ha polimorfisme de subtipus. És composició pura.
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf(\"[%s] %s\n\", l.prefix, msg)
}
// Server fa embedding de Logger
type Server struct {
Logger
addr string
}
func main() {
s := Server{
Logger: Logger{prefix: \"HTTP\"},
addr: \":8080\",
}
// Log() es promociona des de Logger
s.Log(\"starting server\")
}El cas d’ús més habitual en backend és fer embedding de sync.Mutex per protegir un struct:
type SafeCounter struct {
sync.Mutex
counts map[string]int
}
func (c *SafeCounter) Increment(key string) {
c.Lock()
defer c.Unlock()
c.counts[key]++
}El que cal evitar: fer servir embedding com si fos herència. Si faig embedding de Database a UserService per “heretar” mètodes d’accés a dades, estic creant acoblament innecessari. L’embedding hauria de promocionar comportament que té sentit en la interfície pública del tipus contenidor.
Disseny de paquets: paquets cohesionats i enfocats
Effective Go no dedica una secció específica al disseny de paquets, però crec que la filosofia es desprèn de tot el document si llegeixes entre línies. Un paquet a Go hauria de fer una cosa bé.
// MALAMENT: paquet \"util\" que fa de tot
util/
strings.go
http.go
time.go
crypto.go
math.go
// BÉ: paquets enfocats
httputil/
middleware.go
response.go
order/
service.go
repository.go
model.go
user/
service.go
repository.go
handler.goRegles que aplico:
- Si el nom del paquet és
util,common,helpersoshared, probablement necessites dividir-lo. Aquests noms són símptomes que no has pensat en les responsabilitats. - Evita dependències circulars. Go no les permet a nivell de compilació, però això t’obliga a pensar en la direcció de les dependències des del principi.
- Un paquet no hauria de tenir més d’un nivell de subdirectoris tret que sigui una llibreria gran. Per a la majoria d’aplicacions backend, una estructura plana funciona millor.
- Els paquets
internal/són els teus amics. Tot el que no vulguis exposar fora del teu mòdul va allà.
Si vols veure com organitzo projectes reals a Go, ho explico a l’article sobre estructura de projecte.
Comentaris: explica el perquè, no el què
Effective Go té una posició clara sobre els comentaris: els bons comentaris expliquen per què, no què. El codi ja diu què fa. Si necessites un comentari per explicar què fa el codi, probablement el codi és massa complicat.
// MALAMENT: el comentari repeteix el que diu el codi
// Incrementa el comptador
counter++
// MALAMENT: el comentari descriu la implementació òbvia
// Itera sobre els usuaris i filtra els actius
for _, u := range users {
if u.Active {
active = append(active, u)
}
}
// BÉ: el comentari explica una decisió no òbvia
// Fem servir un buffer de 100 perquè el productor pot generar ràfegues
// de fins a 80 esdeveniments per segon i el consumidor processa ~50/s.
ch := make(chan Event, 100)
// BÉ: el comentari explica un workaround
// El driver de PostgreSQL retorna un error genèric quan la connexió
// es tanca per timeout del servidor. Ho reintentem una vegada abans de
// propagar l'error al caller.
result, err := retryOnce(func() (*Result, error) {
return db.QueryContext(ctx, query, args...)
})Comentaris de documentació
A Go, els comentaris que precedeixen una declaració exportada són documentació. godoc els extreu automàticament. Haurien de començar amb el nom de l’element que documenten.
// OrderService gestiona la lògica de negoci de comandes.
// Coordina entre el repositori de comandes, el servei de pagaments
// i les notificacions a l'usuari.
type OrderService struct {
// ...
}
// Create valida i persisteix una nova comanda.
// Retorna ErrInvalidOrder si la comanda no supera les validacions
// i ErrPaymentFailed si el cobrament no es pot completar.
func (s *OrderService) Create(ctx context.Context, o *Order) error {
// ...
}El que Effective Go no cobreix
El document es va escriure el 2009 i no s’ha actualitzat per reflectir canvis importants del llenguatge. I aquí és el problema: hi ha tres àrees que necessites aprendre per separat, perquè Effective Go simplement no les cobreix:
Generics (Go 1.18+)
Els genèrics canvien la forma d’escriure funcions i tipus reutilitzables. Effective Go no en diu res perquè no existien.
// Abans dels generics: una funció per tipus
func ContainsString(slice []string, target string) bool { ... }
func ContainsInt(slice []int, target int) bool { ... }
// Amb generics: una funció per a tots
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}La meva posició, que reconec pot ser conservadora: fes servir generics quan l’alternativa és duplicar codi o fer servir interface{}. No els facis servir per a abstraccions prematures. La majoria de codi backend no necessita generics.
Modules (Go 1.11+)
Effective Go assumeix GOPATH. Avui fem servir mòduls. go.mod i go.sum gestionen dependències de forma determinista. Si estàs començant amb Go avui, ni necessites saber què era GOPATH.
// go.mod típic
module github.com/rogerbcn/myservice
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/jackc/pgx/v5 v5.6.0
)Context (Go 1.7+)
context.Context és avui omnipresent en codi Go de backend. Es fa servir per a cancel·lació, timeouts i propagació de valors entre capes. Effective Go no el menciona.
func (s *Service) GetOrder(ctx context.Context, id string) (*Order, error) {
// El context es propaga a la query de base de dades
row := s.db.QueryRowContext(ctx, \"SELECT * FROM orders WHERE id = $1\", id)
// Si el context es cancel·la (timeout del HTTP handler, per exemple),
// la query es cancel·la automàticament
var o Order
if err := row.Scan(&o.ID, &o.Total, &o.Status); err != nil {
return nil, fmt.Errorf(\"getting order %s: %w\", id, err)
}
return &o, nil
}La regla: context.Context sempre és el primer paràmetre d’una funció. Mai el guardis en un struct. Propaga’l sempre cap avall.
Les 10 regles que aplico cada dia
Després de diversos anys escrivint Go per a backend, aquests són els principis de Effective Go destil·lats en les regles que intento aplicar cada dia. No són originals ni revolucionàries, però m’han donat bons resultats:
Executa
go fmten guardar. Sense excepcions. El format no és negociable.Noms curts per a scopes curts.
rper a un request,ctxper a un context,errper a un error. Noms llargs només per a coses que viuen molt o s’exporten.Early return sempre. Si pots sortir de la funció abans, fes-ho. El happy path va al nivell més baix d’indentació.
Comprova errors immediatament. Mai acumulis operacions sense comprovar. Embolcalla amb
fmt.Errorf(\"context: %w\", err).Interfícies d’un mètode. Si la teva interfície té més de tres mètodes, probablement necessites dividir-la. Defineix les interfícies on es consumeixen.
No llancis goroutines sense saber quan acaben. Fes servir
sync.WaitGroup, channels amb senyal, oerrgroup.Group.Composició, no herència. Fes embedding de tipus quan té sentit per a la interfície pública. No ho facis servir com a substitut de l’herència.
Paquets amb un sol propòsit. Si el nom és
utilocommon, repensa l’estructura.Comenta el perquè, no el què. Els comentaris de documentació comencen amb el nom de l’element. Els comentaris inline expliquen decisions, no sintaxi.
context.Contextcom a primer paràmetre. Propaga’l sempre. Mai el guardis en un struct.
Cap d’aquestes regles és original, com deia. Totes vénen, directa o indirectament, de Effective Go i de la cultura que va generar. El document original mereix una lectura completa almenys una vegada. Però si et quedes només amb aquestes deu regles i les apliques de forma consistent, crec que el teu codi Go serà més llegible, més mantenible i més idiomàtic que la majoria del que hi ha per aquí.
I al final, el que diferencia el Go idiomàtic del Go “funcional però estrany” no és conèixer trucs avançats. És respectar les convencions del llenguatge de forma sistemàtica. Effective Go et diu exactament quines són aquestes convencions. La resta és pràctica. I temps. I equivocar-se prou per entendre per què existeixen aquelles convencions.


