Punters a Go explicats sense drama
Punters a Go amb exemples reals: mutabilitat, mètodes amb receiver, rendiment, nil i errors comuns. Sense complicacions.

Si véns de Python o Java, portes anys treballant amb referències sense pensar-hi. Cada vegada que passes un objecte a una funció, estàs passant una referència a l’objecte original. El que modifiques dins, es modifica fora. No hi ha cap decisió conscient: el llenguatge ho fa per tu.
Go t’obliga a ser explícit. I això, lluny de ser un problema, és una de les millors decisions de disseny del llenguatge. Quan veus *User en una signatura, saps immediatament que aquella funció pot modificar l’struct original. Quan veus User a seques, saps que treballa amb una còpia. Sense ambigüitats, sense màgia oculta.
Els punters a Go no són els punters de C. No hi ha aritmètica de punters, no hi ha malloc, no hi ha free. Són una eina concreta per controlar quan vols compartir una referència i quan prefereixes treballar amb una còpia independent.
Què és un punter: * i & en 2 minuts
Un punter és una variable que emmagatzema l’adreça de memòria d’una altra variable. A Go s’utilitzen dos operadors:
&— obté l’adreça d’una variable (crea un punter cap a ella).*— accedeix al valor al qual apunta un punter (desreferència).
package main
import \"fmt\"
func main() {
nom := \"Roger\"
punter := &nom // punter és de tipus *string
fmt.Println(nom) // \"Roger\"
fmt.Println(punter) // 0xc0000140a0 (adreça de memòria)
fmt.Println(*punter) // \"Roger\" (desreferència)
*punter = \"Un altre nom\"
fmt.Println(nom) // \"Un altre nom\" — hem modificat l'original
}El tipus de punter en aquest cas és *string. L’asterisc davant del tipus indica que és un punter a aquell tipus. No hi ha res més que entendre aquí: & per obtenir l’adreça, * per llegir o escriure el que hi ha en aquella adreça.
Declaració explícita
També pots declarar un punter sense inicialitzar-lo:
var p *int // punter a int, valor per defecte: nil
fmt.Println(p) // <nil>Un punter no inicialitzat val nil. Accedir a *p quan p és nil provoca un panic. Hi tornarem més endavant.
Per què Go utilitza punters: semàntica de valor per defecte
En la majoria de llenguatges que uses cada dia, els objectes es passen per referència. A Go, tot es passa per valor. Quan crideues una funció amb un struct, Go copia l’struct sencer.
type Config struct {
MaxRetries int
Timeout int
}
func duplicarTimeout(c Config) {
c.Timeout *= 2
}
func main() {
cfg := Config{MaxRetries: 3, Timeout: 30}
duplicarTimeout(cfg)
fmt.Println(cfg.Timeout) // 30 — no ha canviat
}La funció duplicarTimeout rep una còpia de cfg. Modifica la còpia, l’original queda intacta. Si vols que la funció modifiqui l’original, necessites un punter:
func duplicarTimeout(c *Config) {
c.Timeout *= 2
}
func main() {
cfg := Config{MaxRetries: 3, Timeout: 30}
duplicarTimeout(&cfg)
fmt.Println(cfg.Timeout) // 60 — ara sí
}Aquesta semàntica de valor per defecte és deliberada. Et força a prendre la decisió conscient de quan alguna cosa és mutable des de fora. En un projecte gran, això es tradueix en menys bugs per mutació inesperada.
Value receivers vs pointer receivers
Quan defineixes mètodes en un struct, has de triar entre value receiver i pointer receiver. La diferència és fonamental i afecta directament com es comporta el teu codi.
Value receiver
type Rectangle struct {
Amplada float64
Altura float64
}
func (r Rectangle) Area() float64 {
return r.Amplada * r.Altura
}El mètode Area rep una còpia del Rectangle. No pot modificar-lo, només llegir-lo. Perfecte per a mètodes que calculen alguna cosa sense canviar estat.
Pointer receiver
func (r *Rectangle) Escalar(factor float64) {
r.Amplada *= factor
r.Altura *= factor
}El mètode Escalar rep un punter. Modifica l’struct original. Si usessis un value receiver aquí, els canvis es perdrien.
La regla pràctica
La convenció a Go és clara:
- Si el mètode modifica el receiver → pointer receiver. Sense discussió.
- Si l’struct és gran → pointer receiver, per evitar còpies innecessàries.
- Si algun mètode del tipus usa pointer receiver → tots haurien d’usar pointer receiver, per consistència.
- Si l’struct és petit i immutable → value receiver funciona bé.
// Consistència: si un usa pointer receiver, tots haurien de fer-ho
type Usuari struct {
ID int
Nom string
Email string
Config ConfigComplexa
}
func (u *Usuari) CanviarEmail(nou string) {
u.Email = nou
}
func (u *Usuari) NomComplet() string {
return u.Nom // Tot i que no modifica, usem pointer receiver per consistència
}Un detall que de vegades confon: Go et permet cridar mètodes amb pointer receiver en un valor (no punter) i viceversa. El compilador fa la conversió automàticament:
rect := Rectangle{Amplada: 10, Altura: 5}
rect.Escalar(2) // Go converteix automàticament a (&rect).Escalar(2)Però això només funciona quan el compilador pot prendre l’adreça de la variable. No funciona amb valors retornats directament de funcions si no es guarden en una variable abans.
Quan usar punters: mutabilitat, structs grans, estat compartit
No tots els escenaris necessiten punters. Aquí van els tres casos clars on sí que els necessites.
1. Mutabilitat
El cas més obvi. Si una funció o mètode necessita modificar un argument, necessites un punter.
func reiniciarComptador(c *Comptador) {
c.Valor = 0
c.UltimReinici = time.Now()
}2. Structs grans
Copiar un struct de 3 camps és barat. Copiar-ne un amb 20 camps, slices interns i maps ja no tant. Per a structs grans, un punter evita la còpia:
type InformeComplet struct {
Titol string
Seccions []Seccio
Metadades map[string]string
Contingut []byte
Historial []Revisio
// ... 15 camps més
}
// Punter per evitar copiar tota aquesta estructura
func processarInforme(informe *InformeComplet) error {
// ...
return nil
}3. Estat compartit
Quan múltiples goroutines o components necessiten accedir i modificar el mateix dada:
type Cache struct {
mu sync.RWMutex
dades map[string]string
}
func NovaCache() *Cache {
return &Cache{
dades: make(map[string]string),
}
}
func (c *Cache) Set(clau, valor string) {
c.mu.Lock()
defer c.mu.Unlock()
c.dades[clau] = valor
}
func (c *Cache) Get(clau string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.dades[clau]
return val, ok
}Aquí, Cache sempre es passa com a punter. Copiar un struct amb un sync.Mutex dins és un bug garantit — la còpia tindria el seu propi mutex independent, trencant tota la sincronització.
Quan NO usar punters
- Tipus bàsics (
int,string,bool): copia’ls sense por. Són petits i la còpia és pràcticament gratuïta. - Structs petits i immutables: si només té 2-3 camps i cap mètode el modifica, una còpia és més simple i segura.
- Quan vols garantir immutabilitat: passar una còpia assegura que ningú modifica l’original des d’un altre lloc.
Punters nil: l’error del bilió de dòlars (i com Go el mitiga)
Tony Hoare va anomenar la referència nul·la el seu “error de mil milions de dòlars”. Go no elimina el problema — els punters nil existeixen — però et dóna eines per mitigar-lo.
El panic clàssic
var u *Usuari
fmt.Println(u.Nom) // panic: runtime error: invalid memory address or nil pointer dereferenceAquest és probablement el panic més comú a Go. Intentar accedir a un camp o mètode d’un punter nil peta en temps d’execució.
Com protegir-te
1. Comprova nil abans d’usar:
func enviarNotificacio(u *Usuari) error {
if u == nil {
return fmt.Errorf(\"usuari és nil\")
}
// procedir amb seguretat
return enviarEmail(u.Email)
}2. Retorna errors en lloc de punters nil sense context:
// Malament: el caller no sap per què és nil
func buscarUsuari(id int) *Usuari {
// ...
return nil
}
// Bé: el caller sap exactament què ha passat
func buscarUsuari(id int) (*Usuari, error) {
u, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf(\"error buscant usuari %d: %w\", id, err)
}
if u == nil {
return nil, fmt.Errorf(\"usuari %d no trobat\", id)
}
return u, nil
}El patró (*T, error) és idiomàtic a Go i és la teva principal defensa contra nils inesperats. Si la teva funció pot fallar, retorna sempre un error.
3. Usa el zero value al teu favor:
En molts casos, pots evitar punters completament retornant el zero value de l’struct juntament amb un error:
func buscarConfig(nom string) (Config, error) {
cfg, ok := configs[nom]
if !ok {
return Config{}, fmt.Errorf(\"config %q no trobada\", nom)
}
return cfg, nil
}Aquí no hi ha punter, no hi ha risc de nil. El caller rep una còpia o un zero value. Més simple, més segur.
Punters i arguments de funció
Quan passes un punter a una funció, la funció rep una còpia del punter (que apunta a la mateixa adreça de memòria). Això té implicacions importants.
Modificar el que s’apunta funciona
func canviarNom(u *Usuari) {
u.Nom = \"Nou\" // modifica l'struct original
}Reassignar el punter no afecta el caller
func reemplaçar(u *Usuari) {
u = &Usuari{Nom: \"Altre\"} // només canvia la còpia local del punter
}
func main() {
u := &Usuari{Nom: \"Roger\"}
reemplaçar(u)
fmt.Println(u.Nom) // \"Roger\" — no ha canviat
}Dins de reemplaçar, u és una variable local que conté una còpia del punter. Reassignar-la no afecta el punter original a main. Només afecta la variable local.
Slices, maps i channels: ja són referències
Una dada que genera confusió: els slices, maps i channels ja contenen internament un punter a les dades subjacents. Passar-los a una funció no copia les dades, només la capçalera del slice/map/channel.
func afegirElement(s []int) []int {
return append(s, 42)
}
func modificarElement(s []int) {
s[0] = 999 // modifica l'array subjacent original
}No necessites *[]int per modificar els elements d’un slice. Però sí que el necessites si vols que la funció canviï el slice en si (la seva longitud o capacitat) de manera visible per al caller — per això append retorna el nou slice en lloc de modificar l’original directament.
Punters a punters: quan els necessites (gairebé mai)
Un punter a punter (**T) és exactament el que sona: un punter que apunta a un altre punter. A Go, és extremadament rar necessitar-los.
El cas legítim
L’únic escenari on té sentit és quan una funció necessita reassignar el punter del caller:
func inicialitzarSiNil(pp **Config) {
if *pp == nil {
*pp = &Config{
MaxRetries: 3,
Timeout: 30,
}
}
}
func main() {
var cfg *Config // nil
inicialitzarSiNil(&cfg)
fmt.Println(cfg.Timeout) // 30
}Per què gairebé mai el necessites
A la pràctica, hi ha formes més clares de resoldre-ho:
// Millor: retornar el punter directament
func inicialitzarSiNil(cfg *Config) *Config {
if cfg == nil {
return &Config{
MaxRetries: 3,
Timeout: 30,
}
}
return cfg
}
func main() {
var cfg *Config
cfg = inicialitzarSiNil(cfg)
fmt.Println(cfg.Timeout) // 30
}Aquest patró és molt més llegible i no requereix punters dobles. Si et trobes escrivint **T a Go, para i pensa si hi ha una alternativa més simple. Gairebé sempre n’hi ha.
Errors comuns amb punters
1. Punter a variable de bucle
Aquest és un clàssic que ha enxampat molta gent. En versions de Go anteriors a 1.22, la variable del bucle es reutilitzava en cada iteració:
// BUG a Go < 1.22
noms := []string{\"Ana\", \"Luis\", \"Marta\"}
punters := make([]*string, len(noms))
for i, nom := range noms {
punters[i] = &nom // tots apunten a la mateixa variable!
}
for _, p := range punters {
fmt.Println(*p) // \"Marta\", \"Marta\", \"Marta\"
}La variable nom es reutilitzava en cada iteració, de manera que tots els punters apuntaven a la mateixa adreça. Al final del bucle, aquella adreça conté “Marta”.
Solució clàssica (pre Go 1.22):
for i, nom := range noms {
n := nom // còpia local
punters[i] = &n
}A Go 1.22+ aquest problema està resolt: cada iteració crea una nova variable. Però si mantens codi que compila amb versions anteriors, segueix essent rellevant.
2. Nil dereference en cadenes d’accés
type Empresa struct {
Director *Persona
}
type Persona struct {
Adreça *Adreça
}
type Adreça struct {
Ciutat string
}
func obtenirCiutat(e *Empresa) string {
// PANIC si e, e.Director o e.Director.Adreça són nil
return e.Director.Adreça.Ciutat
}Cada accés a un punter en la cadena pot provocar un panic. La solució és comprovar cada nivell:
func obtenirCiutat(e *Empresa) string {
if e == nil || e.Director == nil || e.Director.Adreça == nil {
return \"\"
}
return e.Director.Adreça.Ciutat
}Sí, és verbós. Però és explícit. Go prefereix la verbositat a la màgia que amaga explosions en temps d’execució.
3. Copiar un struct amb mutex o recursos interns
type Pool struct {
mu sync.Mutex
conns []*Connection
}
func ferAlguna(p Pool) { // BUG: copia el mutex
p.mu.Lock()
defer p.mu.Unlock()
// ...
}Copiar un sync.Mutex crea un mutex independent. Les dues còpies poden bloquejar-se simultàniament, que és exactament el contrari del que volies. Sempre passa structs amb mutex com a punters. El compilador no t’avisa d’això — cal saber-ho.
Si uses go vet, detectarà alguns d’aquests casos, però no tots. El testing amb -race també ajuda a trobar aquests problemes.
Rendiment: quan copiar està bé i quan els punters ajuden
Hi ha una idea estesa que “punters = més ràpid”. No sempre és cert.
Quan copiar és millor
- Structs petits (< ~64 bytes, 3-4 camps): la còpia és tan ràpida que el overhead d’indireccions del punter pot ser pitjor.
- Dades de només lectura: les còpies són cache-friendly. El processador pot accedir a dades contigus en memòria més ràpid que perseguint punters.
- Concurrència: cada goroutine amb la seva pròpia còpia no necessita sincronització.
Quan els punters ajuden
- Structs grans: copiar 1 KB de dades en cada crida a funció sí que té cost.
- Dades compartides: si diverses goroutines necessiten llegir i escriure el mateix estat.
- Interfícies: quan assignes un struct gran a una interfície, Go necessita fer una còpia. Un punter evita aquesta còpia.
Mesurar, no assumir
Go et dona eines de benchmark integrades. Si dubtes de l’impacte en rendiment, mesura:
func BenchmarkValor(b *testing.B) {
cfg := Config{MaxRetries: 3, Timeout: 30}
for i := 0; i < b.N; i++ {
processarPerValor(cfg)
}
}
func BenchmarkPunter(b *testing.B) {
cfg := &Config{MaxRetries: 3, Timeout: 30}
for i := 0; i < b.N; i++ {
processarPerPunter(cfg)
}
}En structs petits, la diferència serà soroll estadístic. En structs de centenars de bytes, la diferència serà mesurable. La regla: no optimitzis abans de mesurar, i no usis punters “per rendiment” sense dades que ho recolzin.
Escape analysis
Go decideix en temps de compilació si una variable es queda a l’stack o es mou al heap. Quan prens l’adreça d’una variable local i la retornes com a punter, aquella variable “escapa” al heap:
func crearUsuari() *Usuari {
u := Usuari{Nom: \"Roger\"} // escapa al heap perquè retornem la seva adreça
return &u
}Les assignacions al heap són més cares que a l’stack i generen feina per al garbage collector. Pots veure les decisions del compilador amb:
go build -gcflags=\"-m\" ./...Això no vol dir que hagis d’evitar retornar punters. Vol dir que has de ser conscient que no és gratuït, i que de vegades retornar un valor (còpia) és més eficient que retornar un punter que força una assignació al heap.
Guia de decisió: quan usar punters
Després d’anys treballant amb Go, aquesta és la llista que faig servir mentalment abans de decidir si un paràmetre o receiver hauria de ser un punter:
Usa punter quan:
- El mètode o funció necessita modificar l’argument.
- L’struct conté un
sync.Mutexo altres camps que no s’han de copiar. - L’struct és gran (> 5-6 camps o conté slices/maps pesants).
- Necessites representar l’absència de valor (
nil). - Múltiples goroutines necessiten accedir a la mateixa dada.
- Algun altre mètode del tipus ja usa pointer receiver (consistència).
Usa valor quan:
- L’struct és petit i immutable.
- Vols garantir que ningú modifica l’original.
- És un tipus bàsic (
int,string,bool,float64). - És un slice, map o channel (ja contenen referències internes).
- És una funció pura sense efectes secundaris.
Regla d’or: comença amb valors. Canvia a punters quan tinguis una raó concreta. Si la raó és “per rendiment” i no has fet un benchmark, probablement no necessites el punter.
El valor per defecte és valor, i això és bo
Els punters a Go no són el monstre que semblen si véns de llenguatges amb recolector de brossa i pas per referència automàtic. Són una eina explícita que et dona control sobre alguna cosa que altres llenguatges amaguen: quan treballes amb una còpia i quan amb la referència original.
La semàntica de valor per defecte de Go és, en la meva opinió, una de les seves majors fortaleses. T’obliga a pensar en la mutabilitat de cada funció, cada mètode, cada struct. Això fa que el codi sigui més predictible i més fàcil de raonar, especialment en projectes amb concurrència.
No converteixis tot a punters per defecte. No evitis els punters per por. Usa les regles d’aquesta guia, mesura quan tinguis dubtes, i escriu codi que qualsevol persona de l’equip pugui llegir i entendre sense necessitat de rastrejar mutacions ocultes.
Si vols aprofundir en la base dels punters, comença per entendre bé els structs a Go i com funciona el maneig d’errors — tots dos temes estan directament connectats amb les decisions que prens sobre punters cada dia.


