Com començar amb Go si ja saps programar
Guia pràctica per començar amb Go si véns de Python, Java o Kotlin. Instal·lació, estructura, mòduls, funcions, structs i testing.

Si ja programes en Python, Java o Kotlin, no necessites un altre tutorial de “Hola Món”. El que necessites és entendre les opinions que Go pren per tu i per què les pren. Go és un llenguatge que t’obliga a fer les coses d’una manera concreta, i si intentes lluitar contra això —com vaig fer jo al principi— l’experiència és dolenta. Però si ho acceptes, tot encaixa amb una velocitat que sorprèn.
Go no té classes, no té excepcions, no té genèrics complexos, no té herència. I això no és una limitació accidental: és una decisió de disseny deliberada. Cadascuna d’aquestes absències existeix per eliminar una categoria sencera de problemes que Google trobava en mantenir codi a escala. Crec que la teva feina en començar no és preguntar “on és l’herència?” sinó entendre quina alternativa proposa Go i per què funciona. Encara que reconec que costa acceptar-ho al principi.
Aquest article és la guia que m’hauria estalviat temps quan vaig començar amb Go venint de Kotlin i Java. Va directe al que importa.
Instal·lació: cinc minuts i llest
Go té una de les instal·lacions més netes de qualsevol llenguatge. No hi ha gestors de versions obligatoris, no hi ha conflictes amb la versió del sistema, no hi ha drama.
macOS
// Si fas servir Homebrew:
// brew install goO descarrega el .pkg des de go.dev/dl i instal·la’l directament. Homebrew és més còmode per actualitzar, però l’instal·lador oficial funciona igual de bé.
Linux
# Descarrega la darrera versió (ajusta la versió al moment)
wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gzAfegeix això al teu .bashrc o .zshrc:
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/binWindows
Descarrega el .msi des de go.dev/dl i executa l’instal·lador. Les variables d’entorn es configuren automàticament.
Verifica la instal·lació
go version
# go version go1.23.0 darwin/arm64Si veus la versió, estàs llest. I sent honestos, això ja és un alleujament. No necessites un SDK manager, no necessites triar entre JDK 17, 21 o 22, no necessites pyenv per evitar conflictes. Una versió, un binari, funciona.
El teu primer projecte: go mod, main.go, go run
A Go no crees un projecte amb un generador ni amb un framework CLI. Crees un directori, inicialitzes un mòdul i escrius codi.
mkdir el-meu-primer-projecte
cd el-meu-primer-projecte
go mod init github.com/el-teu-usuari/el-meu-primer-projecteAquest go mod init crea un fitxer go.mod que és l’equivalent al pom.xml de Maven, al build.gradle.kts de Gradle o al requirements.txt de Python. Però més simple:
module github.com/el-teu-usuari/el-meu-primer-projecte
go 1.23.0Això és tot. No hi ha fitxers XML de 200 línies. No hi ha plugins. No hi ha seccions de configuració que mai toques però que es trenquen si les esborres.
Ara crea el fitxer main.go:
package main
import \"fmt\"
func main() {
fmt.Println(\"Funciona\")
}I executa’l:
go run main.go
# FuncionaO compila’l:
go build -o la-meva-app
./la-meva-app
# Funcionago run compila i executa en un pas. go build genera un binari estàtic que pots copiar a qualsevol màquina amb la mateixa arquitectura i executar sense dependències. No necessites JVM, no necessites intèrpret de Python, no necessites Docker perquè funcioni. Un sol fitxer binari.
Si véns de Java o Kotlin, aquest detall canviarà la teva manera de desplegar aplicacions. Si véns de Python, deixaràs de lluitar amb entorns virtuals i versions de dependències en producció.
Paquets i visibilitat: la convenció que sorprèn tothom
A Go, la visibilitat d’un identificador es decideix per la primera lletra del seu nom. Majúscula: exportat (públic). Minúscula: no exportat (privat al paquet).
package usuari
// Exportat: accessible des d'altres paquets
type Usuari struct {
Nom string
Email string
edat int // no exportat: només visible dins del paquet \"usuari\"
}
// Exportat
func NouUsuari(nom, email string, edat int) Usuari {
return Usuari{
Nom: nom,
Email: email,
edat: edat,
}
}
// No exportat
func validarEmail(email string) bool {
return len(email) > 0 // simplificat
}No hi ha paraules clau public, private, protected, internal. No hi ha __init__.py. No hi ha export ni module.exports. La convenció és el mecanisme.
La primera vegada que ho veus sembla estrany. Tècnicament no estava equivocat en pensar que era una convenció rara. Però després d’una setmana, et sembla gairebé absurd que altres llenguatges necessitin una paraula clau per a alguna cosa que es resol amb una majúscula. Obres un fitxer, veus una funció amb minúscula i saps immediatament que és interna. No cal buscar el modificador d’accés.
Estructura de paquets
Cada directori és un paquet. No hi ha un directori src obligatori. L’estructura més bàsica és:
el-meu-projecte/
├── go.mod
├── main.go
├── usuari/
│ └── usuari.go
└── repositori/
└── repositori.goPer importar un paquet del teu propi projecte:
package main
import (
\"fmt\"
\"github.com/el-teu-usuari/el-meu-primer-projecte/usuari\"
)
func main() {
u := usuari.NouUsuari(\"Roger\", \"roger@example.com\", 30)
fmt.Println(u.Nom)
}Fixa’t que l’import fa servir la ruta completa del mòdul + directori. No hi ha àlies màgics ni convencions de barrel files. Si necessites aprofundir en com organitzar alguna cosa més seriosa, tinc un article dedicat a estructura de projecte i un altre a mòduls a Go.
Variables, tipus i zero values
Go és un llenguatge tipat estàticament, però no cal que declaris el tipus sempre. Té inferència de tipus que funciona bé en la majoria de casos.
Declaració explícita
var nom string = \"Roger\"
var edat int = 30
var actiu bool = trueDeclaració curta (la que faràs servir el 90% del temps)
nom := \"Roger\"
edat := 30
actiu := trueL’operador := declara i inicialitza. Només funciona dins de funcions, no a nivell de paquet. És l’equivalent a val a Kotlin amb inferència de tipus.
Zero values: tot té un valor per defecte
A Go, les variables declarades sense inicialitzar no són null ni undefined. Tenen un zero value determinat pel seu tipus:
var s string // \"\"
var n int // 0
var f float64 // 0.0
var b bool // false
var p *int // nil (només els punters)Això elimina una categoria sencera de NullPointerException. Si declares un string, sempre té un valor: la cadena buida. Sense sorpreses. Bé, gairebé: els punters sí que poden ser nil, així que el problema no desapareix del tot, però es redueix molt. Si véns de Java, pensa que Go fa per defecte el que tu feies manualment en inicialitzar camps al constructor. Si véns de Kotlin, és com si tot tingués un valor per defecte sense que ho hagis de declarar.
Tipus bàsics
// Enters
var a int // mida depèn de la plataforma (32 o 64 bits)
var b int64 // 64 bits explícitament
var c uint // enter sense signe
// Decimals
var d float64 // el més utilitzat
var e float32
// Text
var f string // immutable, UTF-8 per defecte
// Booleà
var g bool
// Byte i Rune
var h byte // àlies de uint8
var i rune // àlies de int32 (un caràcter Unicode)Go no fa conversions implícites entre tipus numèrics. Si tens un int i necessites un int64, ho converteixes explícitament:
var x int = 42
var y int64 = int64(x)Això molesta els primers dies. Després agradeix que el compilador t’obligui a ser explícit en lloc d’amagar conversions que perden precisió.
Funcions: retorns múltiples i returns amb nom
Les funcions a Go tenen una característica que en altres llenguatges necessites llibreries o workarounds per aconseguir: retorns múltiples natius.
Funció bàsica
func sumar(a, b int) int {
return a + b
}Si els paràmetres són del mateix tipus, pots agrupar-los. a, b int en lloc de a int, b int.
Retorns múltiples
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf(\"no es pot dividir per zero\")
}
return a / b, nil
}I en cridar-la:
resultat, err := dividir(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Println(resultat)Aquest patró valor, error és el cor de Go. No hi ha try/catch, no hi ha excepcions, no hi ha throw. Cada funció que pot fallar retorna un error com a segon valor, i el codi que la crida decideix què fer-ne. Immediatament. No tres nivells més amunt en un catch genèric. Aquí és on la filosofia de Go es fa tangible.
Si véns de Java, al principi sents que escrius més codi. I és cert, no ho negaré. Però també és cert que mai perds de vista on pot fallar alguna cosa. No hi ha excepcions que es propaguen silenciosament fins a un handler genèric que registra “alguna cosa ha anat malament” i se l’empassa, l’error.
Named returns
Go permet nomenar els valors de retorn:
func analitzarConfig(ruta string) (config Config, err error) {
data, err := os.ReadFile(ruta)
if err != nil {
return // retorna els zero values de config i l'error actual
}
err = json.Unmarshal(data, &config)
return // retorna config poblat i err (nil si tot ha anat bé)
}Els named returns s’inicialitzen amb el seu zero value i pots fer servir return sense arguments. Són útils en funcions curtes, però en funcions llargues poden fer el codi més difícil de seguir. La meva recomanació: fes-los servir quan la funció té 15 línies o menys. En funcions més grans, sigues explícit amb el que retornes.
Structs i mètodes
Go no té classes. Té structs amb mètodes associats. Si véns de l’orientació a objectes, pensa en els structs com a classes sense herència, sense constructors màgics i —sent honestos— sense la meitat de la complexitat que probablement mai necessitaves.
Definir un struct
type Tasca struct {
ID int
Titol string
Completada bool
}Crear instàncies
// Forma literal (la més comuna)
t := Tasca{
ID: 1,
Titol: \"Escriure article de Go\",
Completada: false,
}
// Els camps no esmentats prenen el seu zero value
t2 := Tasca{Titol: \"Revisar codi\"}
// t2.ID == 0, t2.Completada == false
// Punter al struct
t3 := &Tasca{
ID: 3,
Titol: \"Desplegar servei\",
}No hi ha new, no hi ha constructors amb quinze paràmetres, no hi ha builders. Si necessites validació en crear, la convenció és crear una funció constructora:
func NovaTasca(titol string) (Tasca, error) {
if titol == \"\" {
return Tasca{}, fmt.Errorf(\"el títol no pot estar buit\")
}
return Tasca{
Titol: titol,
}, nil
}Mètodes
Els mètodes s’associen a un tipus mitjançant un receiver:
// Receiver per valor (no modifica el struct original)
func (t Tasca) EstaCompletada() bool {
return t.Completada
}
// Receiver per punter (pot modificar el struct)
func (t *Tasca) Completar() {
t.Completada = true
}I es fan servir exactament com esperaries:
tasca := Tasca{Titol: \"Aprendre Go\"}
fmt.Println(tasca.EstaCompletada()) // false
tasca.Completar()
fmt.Println(tasca.EstaCompletada()) // trueLa diferència entre receiver per valor i per punter és fonamental:
- Valor: rep una còpia del struct. Útil per a mètodes que només llegeixen.
- Punter: rep una referència. Necessari per a mètodes que modifiquen estat.
Si véns de Java o Kotlin, pensa que func (t *Tasca) Completar() és l’equivalent a un mètode d’instància que modifica this. I func (t Tasca) EstaCompletada() és com si fessis una còpia de l’objecte abans de consultar-lo.
La convenció a Go és que si algun mètode del tipus necessita un receiver per punter, tots els mètodes haurien de fer servir receiver per punter. Això manté la consistència i evita bugs subtils.
Gestió d’errors: just el necessari per començar
La gestió d’errors a Go és un tema que mereix un article complet, i en tinc un dedicat a errors a Go. Però necessites entendre el bàsic des del primer dia perquè escriuràs if err != nil pràcticament a totes les funcions.
El patró bàsic
arxiu, err := os.Open(\"config.json\")
if err != nil {
return fmt.Errorf(\"error obrint config: %w\", err)
}
defer arxiu.Close()Tres coses importants aquí:
if err != nil: és el patró universal. Cada operació que pot fallar retorna un error, i el comproves immediatament.%w(wrapping): embolcalla l’error original per afegir context sense perdre la informació de l’error subjacent. Això és clau per al debugging.defer: programa l’execució dearxiu.Close()per quan la funció actual acabi. És l’equivalent atry-with-resourcesa Java o al context manager de Python, però més flexible.
Crear errors propis
import \"errors\"
var ErrUsuariNoTrobat = errors.New(\"usuari no trobat\")
func BuscarUsuari(id int) (Usuari, error) {
// ... lògica de cerca
if !trobat {
return Usuari{}, ErrUsuariNoTrobat
}
return usuari, nil
}I en gestionar-lo:
usuari, err := BuscarUsuari(42)
if errors.Is(err, ErrUsuariNoTrobat) {
// gestionar cas específic
}Sí, és més verbós que un catch (UserNotFoundException e). Però saps exactament en quina línia es produeix l’error, quina funció el va retornar, i quin context s’ha afegit a cada nivell. No hi ha stack traces de 50 línies on l’error real és a la línia 37 enmig d’un mar de frames de Spring. Crec que aquest trade-off compensa, encara que entenc que no tothom ho vegi així al principi.
Per a una immersió completa en errors custom, wrapping, errors.Is, errors.As i patrons avançats, llegeix l’article d’errors a Go.
El teu primer test: el paquet testing
Go inclou un framework de testing a la llibreria estàndard. No cal instal·lar JUnit, no cal pytest, no cal configurar res.
Convencions
- Els fitxers de test s’anomenen
*_test.go - Les funcions de test comencen per
Testi reben*testing.T - S’executen amb
go test
Exemple complet
Suposem que tens un fitxer calculadora.go:
package calculadora
func Sumar(a, b int) int {
return a + b
}
func Dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf(\"divisió per zero\")
}
return a / b, nil
}El test va a calculadora_test.go, al mateix directori:
package calculadora
import (
\"math\"
\"testing\"
)
func TestSumar(t *testing.T) {
resultat := Sumar(2, 3)
if resultat != 5 {
t.Errorf(\"Sumar(2, 3) = %d; esperat 5\", resultat)
}
}
func TestDividir(t *testing.T) {
resultat, err := Dividir(10, 3)
if err != nil {
t.Fatalf(\"error inesperat: %v\", err)
}
esperat := 3.3333
if math.Abs(resultat-esperat) > 0.001 {
t.Errorf(\"Dividir(10, 3) = %f; esperat %f\", resultat, esperat)
}
}
func TestDividirPerZero(t *testing.T) {
_, err := Dividir(10, 0)
if err == nil {
t.Error(\"s'esperava error en dividir per zero\")
}
}Executa els tests:
go test ./...
# ok github.com/el-teu-usuari/el-meu-projecte/calculadora 0.003s./... executa els tests de tots els paquets del projecte. Sense aquesta convenció, hauríes d’especificar cada paquet manualment.
Table-driven tests
El patró idiomàtic a Go per provar múltiples casos és table-driven tests:
func TestSumar_TableDriven(t *testing.T) {
tests := []struct {
nom string
a, b int
esperat int
}{
{\"positius\", 2, 3, 5},
{\"amb zero\", 0, 5, 5},
{\"negatius\", -1, -2, -3},
{\"mixt\", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.nom, func(t *testing.T) {
resultat := Sumar(tt.a, tt.b)
if resultat != tt.esperat {
t.Errorf(\"Sumar(%d, %d) = %d; esperat %d\",
tt.a, tt.b, resultat, tt.esperat)
}
})
}
}t.Run crea subtests amb nom, cosa que fa que la sortida sigui clara quan alguna cosa falla:
--- FAIL: TestSumar_TableDriven/negatius (0.00s)
calculadora_test.go:25: Sumar(-1, -2) = -3; esperat -4Saps exactament quin cas ha fallat sense llegir un log de 200 línies. Si véns de JUnit amb @ParameterizedTest, és el mateix concepte però sense anotacions ni dependències addicionals.
Cobertura
go test -cover ./...
# ok github.com/el-teu-usuari/el-meu-projecte/calculadora 0.003s coverage: 85.7%
# Per veure quines línies no estan cobertes:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outL’últim comandament obre un navegador amb un mapa visual de cobertura. Verd: cobert. Vermell: no cobert. Sense configurar Jacoco, sense plugins de Gradle, sense generar informes XML que després parseja una altra eina.
Eines que vénen incloses
Un dels avantatges més grans de Go és que les eines estan integrades. No són plugins de tercers que cal configurar, versionar i mantenir.
go fmt
go fmt ./...Formata tot el codi del projecte segons l’estàndard de Go. No hi ha debats sobre tabs vs. espais, no hi ha fitxers de configuració de Prettier, no hi ha CI que falla perquè algú no ha executat el formatejador. A Go es fan servir tabs, punt. El formatejat és part del llenguatge, no una opinió de l’equip. I sent honestos, que aquest debat ja vingui tancat de fàbrica és un regal.
go vet
go vet ./...Analitza el codi buscant errors comuns: arguments incorrectes a fmt.Printf, comprovacions d’errors oblidades, coses que compilen però que probablement són bugs.
go mod tidy
go mod tidyNeteja el go.mod i el go.sum: afegeix dependències que falten i elimina les que no s’utilitzen. És l’equivalent a executar npm prune + npm dedupe en un sol comandament.
Tot junt
La seqüència típica abans de fer un commit:
go fmt ./...
go vet ./...
go test ./...Tres comandaments. Sense task runners, sense Makefiles de 500 línies, sense scripts de CI que instal·len eines que no venen amb el llenguatge. Tot ja és allà.
Què et sorprendrà venint d’altres llenguatges
Cada llenguatge té les seves friccions quan comences. Aquestes són les que més temps em van costar a mi, i crec que a la majoria li passa alguna cosa semblant:
Des de Java/Kotlin
- No hi ha herència. Composició i interfícies. Sempre. Al principi sembla limitant, però després descobreixes que els teus dissenys acaben sent més simples i més fàcils de canviar.
- No hi ha excepcions. El
if err != nilés verbós, però explícit. No hi ha handlers a tres nivells de distància que s’empassen errors. - No hi ha genèrics complexos. Go 1.18 va afegir genèrics, però són bàsics comparats amb els de Java. Això és intencional.
- Compilació ràpida. Un projecte mitjà compila en segons. No en minuts. El feedback loop és immediat.
Des de Python
- Tipat estàtic. Després d’anys amb types opcionals a Python, Go t’obliga a declarar tipus. Al principi molesta, després agradeix que el compilador atrapi errors abans d’executar.
- Sense entorn virtual. No hi ha
venv, no hi haconda, no hi ha conflictes de versions. Un binari, zero dependències en runtime. - Sense màgia. No hi ha decoradors, no hi ha metaclasses, no hi ha monkey patching. El codi fa el que diu. Ni més ni menys.
- Velocitat. Un programa en Go sol ser entre 10x i 100x més ràpid que l’equivalent en Python. Per a scripts d’automatització potser no importa. Per a serveis amb trànsit real, és un abans i un després.
El llenguatge es defensa sol
Això és només la porta d’entrada. Go té molt més per sota, i l’interessant de veritat comença quan passes d’escriure funcions soltes a construir projectes reals. Des d’aquí, et recomano continuar amb els fonaments complets per tenir una visió més àmplia, després entendre mòduls i estructura de projecte per quan el teu codi creixi més enllà d’un main.go, i llegir errors a Go com més aviat millor per dominar el patró que més repetiràs.
Go s’aprèn construint. La documentació oficial és excel·lent, la llibreria estàndard cobreix el 80% del que necessites, i el tooling integrat elimina tota la fricció de configuració que en altres ecosistemes et roba hores. La meva recomanació: munta un projecte petit, escriu tests, desplega un binari i mesura. La primera vegada que compilis un servei, el posis en un contenidor de 15 MB i el vegis arrencar en menys d’un segon, crec que entendràs per què tanta gent s’hi queda.


