Structs a Go: com modelar dades sense classes tradicionals

Structs a Go explicats: mètodes, composició, tags JSON, visibilitat i diferències amb l'orientació a objectes clàssica.

Cover for Structs a Go: com modelar dades sense classes tradicionals

A Go no hi ha classes. No hi ha herència. No hi ha constructors en el sentit tradicional. I tanmateix, pots modelar dominis complexos sense problema. No és que Go elimini el modelatge de dades: elimina part del teatre que de vegades envolta l’orientació a objectes.

Si véns de Java o Kotlin, això al principi incomoda. Estàs acostumat a jerarquies de classes, a extends, a abstract, a patterns d’herència que portes anys practicant. Go et diu: no necessites res d’això. El que necessites és un struct, uns mètodes, i composició. I resulta que amb això construeixes programari igual d’expressiu i bastant més fàcil de llegir.

Els structs són el mecanisme fonamental de modelatge a Go. Tot hi passa: les teves entitats de domini, els teus DTOs, les teves configuracions, les teves respostes d’API. Entendre’ls bé no és opcional. És la base sobre la qual es construeix qualsevol programa Go que vagi més enllà d’un script.


Definir un struct: camps, tipus i valors zero

Un struct a Go és una col·lecció de camps amb nom i tipus. Res més. No té mètodes associats en la seva definició (això ve després). No té herència. No té visibilitat per camp mitjançant keywords com private o protected. És, deliberadament, l’estructura de dades més simple que pots tenir.

type User struct {
    ID        int
    Name      string
    Email     string
    Active    bool
    CreatedAt time.Time
}

Cada camp té un tipus explícit. No hi ha inferència màgica, no hi ha tipus opcionals natius (tot i que pots usar punters per a això). I alguna cosa que sorprèn a molts: tots els camps tenen un valor zero per defecte. No null, no nil (llevat de punters, slices, maps i channels). Un valor zero concret i determinista.

TipusValor zero
int, float640
string\"\"
boolfalse
*T (punter)nil
[]T (slice)nil
map[K]Vnil
time.TimeData zero (any 1)

Això significa que un User{} buit és perfectament vàlid: ID és 0, Name és \"\", Active és false, CreatedAt és la data zero. No llança excepcions, no és null. Té un estat definit i predictible.

var u User
fmt.Println(u.Name)   // \"\" (string buit)
fmt.Println(u.Active) // false
fmt.Println(u.ID)     // 0

Si véns de Java, pensa en això: no necessites inicialitzar camps. No necessites constructors que assignin valors per defecte. El llenguatge ja et garanteix que cada tipus té un estat inicial segur. Això redueix una categoria sencera de bugs relacionats amb NullPointerException.


Crear instàncies: sintaxi literal i funcions constructores

Go no té una keyword new a l’estil Java. Hi ha dues formes principals de crear structs, i cadascuna té el seu ús.

Sintaxi literal

La més directa. Crees el struct i assignes els camps que necessites:

user := User{
    ID:    1,
    Name:  \"Roger\",
    Email: \"roger@oshy.tech\",
    Active: true,
}

Els camps que no especifiquis prenen el seu valor zero. Això és intencional i útil: si un booleà per defecte ha de ser false, simplement no l’incloues.

També pots crear structs sense anomenar els camps, però no ho facis:

// Això compila, però és fràgil i il·legible
user := User{1, \"Roger\", \"roger@oshy.tech\", true, time.Now()}

Si algú afegeix un camp al struct demà, aquest codi es trenca. Usa sempre la sintaxi amb noms de camp.

Funcions constructores

Go no té constructors com a mètode especial del tipus. En el seu lloc, la convenció és crear una funció que comenci amb New:

func NewUser(name, email string) *User {
    return &User{
        Name:      name,
        Email:     email,
        Active:    true,
        CreatedAt: time.Now(),
    }
}

Fixa’t en diversos detalls:

  • Retorna un punter (*User), no un valor. És la convenció habitual quan el struct serà modificat o compartit.
  • Estableix valors per defecte (Active: true, CreatedAt: time.Now()).
  • No necessita una keyword especial. És una funció normal. Sense màgia.

Aquesta convenció és tan forta a Go que quan veus NewX saps immediatament que és un constructor. Sense anotacions, sense reflexió, sense un framework que registri res.

L’operador &

Pots crear un punter a un struct directament amb &:

user := &User{Name: \"Roger\"}

Això és equivalent a crear el struct i llavors prendre la seva adreça. És idiomàtic i el veuràs a tot arreu. Si no estàs còmode amb punters a Go, et recomano llegir primer Punters a Go abans de continuar.


Mètodes: value receivers vs pointer receivers

Aquí és on Go comença a diferenciar-se de les meres estructures de dades. Els structs poden tenir mètodes associats, però no es defineixen dins del struct (com faries en una classe). Es defineixen fora, amb un receiver.

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

El (r Rectangle) abans del nom del mètode és el receiver. Li diu a Go que aquest mètode pertany al tipus Rectangle. Per usar-lo:

rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())      // 50
fmt.Println(rect.Perimeter()) // 30

Fins aquí tot sembla cosmètic. La diferència real apareix amb els pointer receivers.

Value receiver vs pointer receiver

Un value receiver treballa sobre una còpia del struct. Un pointer receiver treballa sobre l’original.

// Value receiver: NO modifica l'original
func (r Rectangle) Scale(factor float64) Rectangle {
    return Rectangle{
        Width:  r.Width * factor,
        Height: r.Height * factor,
    }
}

// Pointer receiver: SÍ modifica l'original
func (r *Rectangle) ScaleInPlace(factor float64) {
    r.Width *= factor
    r.Height *= factor
}
rect := Rectangle{Width: 10, Height: 5}

scaled := rect.Scale(2)
fmt.Println(rect.Width)   // 10 (no ha canviat)
fmt.Println(scaled.Width) // 20

rect.ScaleInPlace(2)
fmt.Println(rect.Width)   // 20 (ha canviat)

Les regles pràctiques per triar:

  • Usa pointer receiver si el mètode modifica el struct.
  • Usa pointer receiver si el struct és gran (evites copiar molta memòria).
  • Usa value receiver si el struct és petit i immutable (com una coordenada o un rang de temps).
  • Sigues consistent: si un mètode del tipus usa pointer receiver, que tots ho facin. Barrejar confon i pot provocar bugs subtils amb interfícies.

A la pràctica, la majoria de mètodes en codi backend usen pointer receivers. Els structs solen representar serveis, repositoris o entitats que es modifiquen o que són massa grans per copiar en cada crida.


Composició sobre herència: structs embeguts

Go no té herència. La paraula extends no existeix. En el seu lloc, Go ofereix composició mitjançant structs embeguts (embedded structs). La diferència no és només sintàctica: canvia fonamentalment com penses sobre la relació entre tipus.

type Address struct {
    Street  string
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address // camp embegut (sense nom explícit)
}

El struct Person no “hereta de” Address. Conté un Address. Però en ser un camp embegut (sense nom explícit), els seus camps es promocionen: pots accedir-hi directament.

p := Person{
    Name: \"Roger\",
    Age:  32,
    Address: Address{
        Street:  \"Carrer Major 1\",
        City:    \"Barcelona\",
        Country: \"Spain\",
    },
}

fmt.Println(p.City)         // \"Barcelona\" (promocionat)
fmt.Println(p.Address.City) // \"Barcelona\" (accés explícit, també vàlid)

Ambdues formes funcionen. I si Address té mètodes, aquells mètodes també es promocionen:

func (a Address) FullAddress() string {
    return fmt.Sprintf(\"%s, %s, %s\", a.Street, a.City, a.Country)
}

fmt.Println(p.FullAddress()) // \"Carrer Major 1, Barcelona, Spain\"

Per què composició i no herència

L’herència crea acoblament vertical. Si canvies la classe pare, afectes tots els fills. Si tens herència de diversos nivells, rastrejar d’on ve un comportament es converteix en un exercici d’arqueologia.

La composició és horitzontal. Un Person conté un Address. Si demà necessites que també contingui un ContactInfo, l’afegeixes. No necessites reestructurar una jerarquia. No trenques contractes implícits d’herència.

type ContactInfo struct {
    Phone string
    Email string
}

type Person struct {
    Name string
    Age  int
    Address
    ContactInfo
}

p := Person{
    Name:        \"Roger\",
    Age:         32,
    Address:     Address{City: \"Barcelona\"},
    ContactInfo: ContactInfo{Email: \"roger@oshy.tech\"},
}

fmt.Println(p.Email) // \"roger@oshy.tech\"
fmt.Println(p.City)  // \"Barcelona\"

Si dos structs embeguts tenen un camp amb el mateix nom, Go no ho resol automàticament. T’obliga a ser explícit. Això és una decisió de disseny: l’ambigüitat es resol en temps de compilació, no en runtime.

type A struct { Name string }
type B struct { Name string }
type C struct {
    A
    B
}

var c C
// c.Name  <- error de compilació: ambigu
c.A.Name = \"des d'A\" // això sí funciona

Això elimina una categoria sencera de bugs que en llenguatges amb herència múltiple (C++, Python) es resolen amb regles de precedència que ningú recorda.


Tags JSON: marshaling i unmarshaling

Els tags d’struct són una de les features més útils i menys glamuroses de Go. Permeten afegir metadades als camps que les biblioteques poden llegir mitjançant reflexió. El cas més comú: controlar com es serialitza un struct a JSON.

type Product struct {
    ID          int       `json:\"id\"`
    Name        string    `json:\"name\"`
    Price       float64   `json:\"price\"`
    InStock     bool      `json:\"in_stock\"`
    Description string    `json:\"description,omitempty\"`
    InternalSKU string    `json:\"-\"`
    CreatedAt   time.Time `json:\"created_at\"`
}

Cada tag és un string entre cometes invertides que segueix la convenció key:\"value\". Per a JSON:

  • json:\"name\" defineix el nom del camp al JSON.
  • json:\"description,omitempty\" omet el camp si té el seu valor zero (string buit, 0, false, nil).
  • json:\"-\" exclou el camp de la serialització. Útil per a camps interns que no han de sortir en la resposta d’una API REST.

Marshaling: struct a JSON

p := Product{
    ID:      1,
    Name:    \"Teclat mecànic\",
    Price:   89.99,
    InStock: true,
    InternalSKU: \"KB-001-MX\",
}

data, err := json.Marshal(p)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Resultat:

{\"id\":1,\"name\":\"Teclat mecànic\",\"price\":89.99,\"in_stock\":true,\"created_at\":\"0001-01-01T00:00:00Z\"}

Fixa’t: description no apareix perquè hem usat omitempty i estava buit. InternalSKU no apareix perquè hem usat \"-\". Els noms de camp segueixen la convenció del tag, no la de Go.

Unmarshaling: JSON a struct

raw := `{\"id\":2,\"name\":\"Ratolí ergonòmic\",\"price\":45.50,\"in_stock\":false}`

var p Product
if err := json.Unmarshal([]byte(raw), &p); err != nil {
    log.Fatal(err)
}
fmt.Println(p.Name)  // \"Ratolí ergonòmic\"
fmt.Println(p.Price) // 45.5

L’unmarshaling ignora camps del JSON que no tenen correspondència al struct, i deixa en valor zero els camps de l’struct que no estan al JSON. Sense excepcions, sense errors. Això és pragmatisme pur.

Tags més enllà de JSON

Els tags no són exclusius de JSON. Biblioteques com pgx (PostgreSQL), yaml, xml, validate i moltes altres els usen:

type Config struct {
    Port     int    `yaml:\"port\" env:\"APP_PORT\" validate:\"required,min=1024\"`
    Host     string `yaml:\"host\" env:\"APP_HOST\" validate:\"required\"`
    LogLevel string `yaml:\"log_level\" env:\"LOG_LEVEL\"`
}

La clau és que els tags són només metadades. El compilador no els valida. Si escrius json:\"naem\" en lloc de json:\"name\", compilarà sense problema i tindràs un bug silenciós. Eines com go vet i linters ajuden a detectar aquests errors.


Visibilitat: exportat vs no exportat

Go no té public, private ni protected. La visibilitat es controla amb una regla brutal en la seva simplicitat: si comença amb majúscula, és exportat (públic). Si comença amb minúscula, no és exportat (privat al paquet).

type User struct {
    ID       int    // exportat: visible fora del paquet
    Name     string // exportat
    email    string // NO exportat: només visible dins del paquet
    password string // NO exportat
}

Això s’aplica a tot: structs, camps, funcions, mètodes, constants, variables. És una convenció enforced pel compilador, no per una keyword. No pots “saltar-te-la” amb reflexió de forma accidental.

Implicacions pràctiques

Quan dissenyes structs que formen part d’una API pública (un paquet que altres importaran), la visibilitat dels camps importa molt:

// paquet auth

type Credentials struct {
    Username string // altres paquets poden llegir-lo i escriure'l
    password string // només el paquet auth pot accedir-hi
}

func NewCredentials(username, password string) Credentials {
    return Credentials{
        Username: username,
        password: hashPassword(password),
    }
}

func (c Credentials) ValidatePassword(input string) bool {
    return checkHash(c.password, input)
}

Des de fora del paquet:

creds := auth.NewCredentials(\"roger\", \"secret123\")
fmt.Println(creds.Username)  // funciona
// fmt.Println(creds.password) // error de compilació
creds.ValidatePassword(\"secret123\") // funciona

El camp password és inaccessible des de fora. No necessites getters/setters com a Java. L’encapsulament és real i el garanteix el compilador.

Visibilitat i JSON

Un detall que agafa a molts: els camps no exportats no es serialitzen a JSON. Si un camp comença amb minúscula, encoding/json l’ignora completament:

type Response struct {
    Status  string `json:\"status\"`
    message string `json:\"message\"` // MAI apareixerà al JSON
}

Això és per disseny. Si necessites que un camp aparegui a JSON, ha de ser exportat. No hi ha manera d’evitar-ho amb tags. És una regla simple que evita exposar accidentalment dades internes.


Comparació i igualtat d’structs

Els structs a Go es poden comparar si tots els seus camps es poden comparar. Pots usar == directament:

type Point struct {
    X, Y int
}

a := Point{1, 2}
b := Point{1, 2}
c := Point{3, 4}

fmt.Println(a == b) // true
fmt.Println(a == c) // false

Això funciona perquè int és comparable. Però no tots els structs es poden comparar:

type Data struct {
    Values []int // els slices NO es poden comparar
}

a := Data{Values: []int{1, 2}}
b := Data{Values: []int{1, 2}}
// fmt.Println(a == b) // error de compilació

Els slices, maps i funcions no es poden comparar amb ==. Si el teu struct conté algun d’aquests tipus, necessites escriure la teva pròpia funció de comparació o usar reflect.DeepEqual (que és lent i s’ha de reservar per a tests).

func (d Data) Equal(other Data) bool {
    if len(d.Values) != len(other.Values) {
        return false
    }
    for i, v := range d.Values {
        if v != other.Values[i] {
            return false
        }
    }
    return true
}

Regla pràctica: si necessites comparar structs complexos en codi de producció, implementa un mètode Equal. Si és només per a tests, reflect.DeepEqual o biblioteques com go-cmp són acceptables.

Els structs comparables també es poden usar com a claus de maps, la qual cosa és útil per a caches i lookups:

type Coordinate struct {
    Lat, Lng float64
}

visited := map[Coordinate]bool{
    {40.4168, -3.7038}: true, // Madrid
    {41.3874, 2.1686}:  true, // Barcelona
}

Quan usar structs vs maps

Aquesta és una pregunta que sorgeix molt, sobretot si véns de Python o JavaScript, on els diccionaris/objectes són l’eina per defecte per a tot.

A Go, la resposta gairebé sempre és “usa un struct”. Però hi ha excepcions.

Usa structs quan:

  • Coneixes la forma de les dades en temps de compilació.
  • Els camps tenen tipus diferents.
  • Necessites mètodes associats.
  • Vols validació del compilador.
  • Les dades es serialitzen/deserialitzen amb una estructura coneguda.
// Això és un struct. Sempre.
type Order struct {
    ID        string
    Customer  string
    Items     []OrderItem
    Total     float64
    Status    string
    CreatedAt time.Time
}

Usa maps quan:

  • La forma de les dades és dinàmica o desconeguda en temps de compilació.
  • Les claus són dinàmiques (configuració, headers HTTP, metadades).
  • Estàs treballant amb dades genèriques que no tenen una estructura fixa.
// Metadades dinàmiques: això és un map
metadata := map[string]string{
    \"source\":  \"api\",
    \"version\": \"2.1\",
    \"region\":  \"eu-west\",
}

// Headers HTTP: també un map
headers := map[string][]string{
    \"Content-Type\":  {\"application/json\"},
    \"Authorization\": {\"Bearer token123\"},
}

L’antipatró: map[string]interface

Si et trobes escrivint map[string]interface{} (o map[string]any des de Go 1.18) a tot arreu, probablement necessites un struct. Els maps sense tipus perden totes les garanties del compilador i converteixen el teu codi Go en JavaScript amb passos extra.

// NO facis això per a dades amb estructura coneguda
data := map[string]any{
    \"name\":  \"Roger\",
    \"age\":   32,
    \"email\": \"roger@oshy.tech\",
}
name := data[\"name\"].(string) // type assertion, pot fer panic en runtime

// Fes això
user := User{
    Name:  \"Roger\",
    Age:   32,
    Email: \"roger@oshy.tech\",
}
// user.Name és string. Sempre. Sense discussió.

Exemple pràctic: modelar una resposta d’API

Posem-ho tot junt amb un exemple real: modelar la resposta d’una API que retorna una llista d’articles paginada. Aquest és el tipus de codi que escrius constantment en un backend Go.

package api

import (
    \"encoding/json\"
    \"time\"
)

// Article representa un article del blog.
type Article struct {
    ID          string    `json:\"id\"`
    Title       string    `json:\"title\"`
    Slug        string    `json:\"slug\"`
    Content     string    `json:\"content,omitempty\"`
    Author      Author    `json:\"author\"`
    Tags        []string  `json:\"tags\"`
    PublishedAt time.Time `json:\"published_at\"`
    Draft       bool      `json:\"draft,omitempty\"`
    viewCount   int       // no exportat: no apareix al JSON, no accessible fora del paquet
}

// Author usa composició en lloc de duplicar camps.
type Author struct {
    Name  string `json:\"name\"`
    Email string `json:\"email\"`
    Bio   string `json:\"bio,omitempty\"`
}

// Pagination encapsula les dades de paginació.
type Pagination struct {
    Page       int  `json:\"page\"`
    PerPage    int  `json:\"per_page\"`
    Total      int  `json:\"total\"`
    TotalPages int  `json:\"total_pages\"`
    HasNext    bool `json:\"has_next\"`
}

// ArticleListResponse és la resposta completa de l'API.
type ArticleListResponse struct {
    Data       []Article  `json:\"data\"`
    Pagination Pagination `json:\"pagination\"`
}

// NewPagination calcula els camps derivats automàticament.
func NewPagination(page, perPage, total int) Pagination {
    totalPages := total / perPage
    if total%perPage != 0 {
        totalPages++
    }
    return Pagination{
        Page:       page,
        PerPage:    perPage,
        Total:      total,
        TotalPages: totalPages,
        HasNext:    page < totalPages,
    }
}

// ToJSON serialitza la resposta. Mètode amb value receiver
// perquè ArticleListResponse no necessita ser modificat.
func (r ArticleListResponse) ToJSON() ([]byte, error) {
    return json.MarshalIndent(r, \"\", \"  \")
}

Usant-lo:

response := api.ArticleListResponse{
    Data: []api.Article{
        {
            ID:    \"1\",
            Title: \"Structs a Go\",
            Slug:  \"structs-go\",
            Author: api.Author{
                Name:  \"Roger Bosch\",
                Email: \"roger@oshy.tech\",
            },
            Tags:        []string{\"Go\", \"structs\", \"backend\"},
            PublishedAt: time.Now(),
        },
    },
    Pagination: api.NewPagination(1, 10, 1),
}

data, err := response.ToJSON()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

Resultat:

{
  \"data\": [
    {
      \"id\": \"1\",
      \"title\": \"Structs a Go\",
      \"slug\": \"structs-go\",
      \"author\": {
        \"name\": \"Roger Bosch\",
        \"email\": \"roger@oshy.tech\"
      },
      \"tags\": [\"Go\", \"structs\", \"backend\"],
      \"published_at\": \"2026-06-24T10:30:00Z\"
    }
  ],
  \"pagination\": {
    \"page\": 1,
    \"per_page\": 10,
    \"total\": 1,
    \"total_pages\": 1,
    \"has_next\": false
  }
}

En aquest exemple apareixen tots els conceptes: structs amb camps tipats, composició (Author dins d’Article), tags JSON amb omitempty, camps no exportats (viewCount), funcions constructores (NewPagination), mètodes amb value receiver (ToJSON), i serialització neta.


Patrons comuns en codi backend real

Per tancar, aquests són patrons que veuràs (i escriuràs) constantment si fas backend a Go. No són teòrics: surten directament de codi en producció.

El patró Options per a constructors complexos

Quan un constructor té massa paràmetres, el patró funcional options és idiomàtic a Go:

type Server struct {
    host         string
    port         int
    readTimeout  time.Duration
    writeTimeout time.Duration
    logger       *slog.Logger
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeouts(read, write time.Duration) Option {
    return func(s *Server) {
        s.readTimeout = read
        s.writeTimeout = write
    }
}

func WithLogger(logger *slog.Logger) Option {
    return func(s *Server) {
        s.logger = logger
    }
}

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:         host,
        port:         8080,
        readTimeout:  5 * time.Second,
        writeTimeout: 10 * time.Second,
        logger:       slog.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}
srv := NewServer(\"localhost\",
    WithPort(9090),
    WithTimeouts(10*time.Second, 30*time.Second),
)

Aquest patró et dona valors per defecte sensats, paràmetres opcionals amb nom, i la possibilitat d’estendre la configuració sense trencar l’API. El usen biblioteques com google.golang.org/grpc i go.uber.org/zap.

Struct com a receptor de servei

En backend Go, els serveis i repositoris són structs amb dependències injectades per constructor. És l’equivalent a una classe amb @Service a Spring, però sense anotacions ni reflexió:

type UserService struct {
    repo   UserRepository
    cache  Cache
    logger *slog.Logger
}

func NewUserService(repo UserRepository, cache Cache, logger *slog.Logger) *UserService {
    return &UserService{
        repo:   repo,
        cache:  cache,
        logger: logger,
    }
}

func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
    if cached, ok := s.cache.Get(ctx, \"user:\"+id); ok {
        return cached.(*User), nil
    }

    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf(\"finding user %s: %w\", id, err)
    }

    s.cache.Set(ctx, \"user:\"+id, user, 5*time.Minute)
    return user, nil
}

Fixa’t que UserRepository i Cache són interfícies, no structs concrets. Això permet injectar implementacions diferents en tests (mocks) i en producció. La regla de Go s’aplica: accepta interfícies, retorna structs.

DTOs separats d’entitats de domini

No barregis el teu model de domini amb els teus DTOs d’API. Són responsabilitats diferents:

// Entitat de domini (capa interna)
type Task struct {
    ID          string
    Title       string
    Description string
    Status      TaskStatus
    AssigneeID  string
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// DTO de resposta (capa d'API)
type TaskResponse struct {
    ID          string `json:\"id\"`
    Title       string `json:\"title\"`
    Description string `json:\"description,omitempty\"`
    Status      string `json:\"status\"`
    Assignee    string `json:\"assignee,omitempty\"`
    CreatedAt   string `json:\"created_at\"`
}

// Conversió explícita
func ToTaskResponse(t Task) TaskResponse {
    return TaskResponse{
        ID:          t.ID,
        Title:       t.Title,
        Description: t.Description,
        Status:      string(t.Status),
        Assignee:    t.AssigneeID,
        CreatedAt:   t.CreatedAt.Format(time.RFC3339),
    }
}

És més codi que posar tags JSON directament a l’entitat. Però quan la teva API necessiti retornar dades de forma diferent a com les emmagatzemes (i això passa sempre), tindràs la separació feta. Ho explico amb més detall a estructura de projecte i arquitectura neta a Go.

Structs com a configuració tipada

En lloc de llegir variables d’entorn com strings dispersos pel codi, centralitza la configuració en un struct:

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Redis    RedisConfig
}

type ServerConfig struct {
    Host string `env:\"SERVER_HOST\" envDefault:\"0.0.0.0\"`
    Port int    `env:\"SERVER_PORT\" envDefault:\"8080\"`
}

type DatabaseConfig struct {
    URL             string        `env:\"DATABASE_URL,required\"`
    MaxConns        int           `env:\"DB_MAX_CONNS\" envDefault:\"25\"`
    ConnMaxLifetime time.Duration `env:\"DB_CONN_MAX_LIFETIME\" envDefault:\"5m\"`
}

type RedisConfig struct {
    Addr     string `env:\"REDIS_ADDR\" envDefault:\"localhost:6379\"`
    Password string `env:\"REDIS_PASSWORD\"`
    DB       int    `env:\"REDIS_DB\" envDefault:\"0\"`
}

Amb una biblioteca com github.com/caarlos0/env pots omplir aquest struct directament des de variables d’entorn. Resultat: configuració tipada, amb valors per defecte, amb validació, i sense un sol os.Getenv solt al teu codi.


Menys cerimònia, més claredat

Els structs a Go són simples per disseny. No tenen herència, no tenen constructors màgics, no tenen visibilitat granular amb cinc keywords. I això és precisament el que els fa poderosos: t’obliguen a modelar les teves dades de forma explícita, a compondre en lloc d’heretar, a ser clar en lloc d’abstracte.

Si véns de llenguatges amb orientació a objectes clàssica, el canvi pot semblar un pas enrere. Però després d’usar Go en producció durant un temps, t’adones que la majoria d’abstraccions OOP que usaves no eren necessàries. Eren cerimonioses. Go elimina la cerimònia i et deixa amb el que importa: dades, comportament i composició.

Els structs són la base. Sobre ells construeixes serveis, handlers d’API, repositoris, configuració, DTOs. Per al següent pas, entén com les interfícies a Go complementen els structs, i com junts formen el sistema de tipus que fa que Go sigui Go. I quan estiguis a punt per construir alguna cosa real, una API REST amb Go és el millor camp de proves.

Articles relacionats

OshyTech

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

Navegació

Copyright 2026 OshyTech. Tots els drets reservats