Structs en Go: cómo modelar datos sin clases tradicionales

Structs en Go explicados: métodos, composición, tags JSON, visibilidad y diferencias con la orientación a objetos clásica.

Cover for Structs en Go: cómo modelar datos sin clases tradicionales

En Go no hay clases. No hay herencia. No hay constructores en el sentido tradicional. Y sin embargo, puedes modelar dominios complejos sin problema. No es que Go elimine el modelado de datos: elimina parte del teatro que a veces rodea la orientación a objetos.

Si vienes de Java o Kotlin, esto al principio incomoda. Estás acostumbrado a jerarquías de clases, a extends, a abstract, a patterns de herencia que llevas años practicando. Go te dice: no necesitas nada de eso. Lo que necesitas es un struct, unos métodos, y composición. Y resulta que con eso construyes software igual de expresivo y bastante más fácil de leer.

Los structs son el mecanismo fundamental de modelado en Go. Todo pasa por ellos: tus entidades de dominio, tus DTOs, tus configuraciones, tus respuestas de API. Entenderlos bien no es opcional. Es la base sobre la que se construye cualquier programa Go que vaya más allá de un script.


Definir un struct: campos, tipos y valores cero

Un struct en Go es una colección de campos con nombre y tipo. Nada más. No tiene métodos asociados en su definición (eso viene después). No tiene herencia. No tiene visibilidad por campo mediante keywords como private o protected. Es, deliberadamente, la estructura de datos más simple que puedes tener.

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

Cada campo tiene un tipo explícito. No hay inferencia mágica, no hay tipos opcionales nativos (aunque puedes usar punteros para eso). Y algo que sorprende a muchos: todos los campos tienen un valor cero por defecto. No null, no nil (salvo en punteros, slices, maps y channels). Un valor cero concreto y determinista.

TipoValor cero
int, float640
string""
boolfalse
*T (puntero)nil
[]T (slice)nil
map[K]Vnil
time.TimeFecha cero (año 1)

Esto significa que un User{} vacío es perfectamente válido: ID es 0, Name es "", Active es false, CreatedAt es la fecha cero. No lanza excepciones, no es null. Tiene un estado definido y predecible.

var u User
fmt.Println(u.Name)   // "" (string vacío)
fmt.Println(u.Active) // false
fmt.Println(u.ID)     // 0

Si vienes de Java, piensa en esto: no necesitas inicializar campos. No necesitas constructores que asignen valores por defecto. El lenguaje ya te garantiza que cada tipo tiene un estado inicial seguro. Esto reduce una categoría entera de bugs relacionados con NullPointerException.


Crear instancias: sintaxis literal y funciones constructoras

Go no tiene una keyword new al estilo Java. Hay dos formas principales de crear structs, y cada una tiene su uso.

Sintaxis literal

La más directa. Creas el struct y asignas los campos que necesitas:

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

Los campos que no especifiques toman su valor cero. Esto es intencional y útil: si un booleano por defecto debe ser false, simplemente no lo incluyes.

También puedes crear structs sin nombrar los campos, pero no lo hagas:

// Esto compila, pero es frágil e ilegible
user := User{1, "Roger", "roger@oshy.tech", true, time.Now()}

Si alguien añade un campo al struct mañana, este código se rompe. Siempre usa la sintaxis con nombres de campo.

Funciones constructoras

Go no tiene constructores como método especial del tipo. En su lugar, la convención es crear una función que empiece con New:

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

Fíjate en varios detalles:

  • Devuelve un puntero (*User), no un valor. Es la convención habitual cuando el struct va a ser modificado o compartido.
  • Establece valores por defecto (Active: true, CreatedAt: time.Now()).
  • No necesita una keyword especial. Es una función normal. Sin magia.

Esta convención es tan fuerte en Go que cuando ves NewX sabes inmediatamente que es un constructor. Sin anotaciones, sin reflexión, sin un framework que registre nada.

El operador &

Puedes crear un puntero a un struct directamente con &:

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

Esto es equivalente a crear el struct y luego tomar su dirección. Es idiomático y lo verás en todas partes. Si no estás cómodo con punteros en Go, te recomiendo leer primero Punteros en Go antes de seguir.


Métodos: value receivers vs pointer receivers

Aquí es donde Go empieza a diferenciarse de las meras estructuras de datos. Los structs pueden tener métodos asociados, pero no se definen dentro del struct (como harías en una clase). Se definen fuera, con 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) antes del nombre del método es el receiver. Le dice a Go que este método pertenece al tipo Rectangle. Para usarlo:

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

Hasta aquí todo parece cosmético. La diferencia real aparece con los pointer receivers.

Value receiver vs pointer receiver

Un value receiver trabaja sobre una copia del struct. Un pointer receiver trabaja sobre el original.

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

// Pointer receiver: SÍ modifica el 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 cambiado)
fmt.Println(scaled.Width) // 20

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

Las reglas prácticas para elegir:

  • Usa pointer receiver si el método modifica el struct.
  • Usa pointer receiver si el struct es grande (evitas copiar mucha memoria).
  • Usa value receiver si el struct es pequeño e inmutable (como una coordenada o un rango de tiempo).
  • Sé consistente: si un método del tipo usa pointer receiver, que todos lo usen. Mezclar confunde y puede provocar bugs sutiles con interfaces.

En la práctica, la mayoría de métodos en código backend usan pointer receivers. Los structs suelen representar servicios, repositorios o entidades que se modifican o que son demasiado grandes para copiar en cada llamada.


Composición sobre herencia: structs embebidos

Go no tiene herencia. La palabra extends no existe. En su lugar, Go ofrece composición mediante structs embebidos (embedded structs). La diferencia no es solo sintáctica: cambia fundamentalmente cómo piensas sobre la relación entre tipos.

type Address struct {
    Street  string
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address // campo embebido (sin nombre explícito)
}

El struct Person no “hereda de” Address. Contiene un Address. Pero al ser un campo embebido (sin nombre explícito), sus campos se promocionan: puedes acceder a ellos directamente.

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

fmt.Println(p.City)    // "Barcelona" (promocionado)
fmt.Println(p.Address.City) // "Barcelona" (acceso explícito, también válido)

Ambas formas funcionan. Y si Address tiene métodos, esos métodos también se promocionan:

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"

Por qué composición y no herencia

La herencia crea acoplamiento vertical. Si cambias la clase padre, afectas a todos los hijos. Si tienes herencia de varios niveles, rastrear de dónde viene un comportamiento se convierte en un ejercicio de arqueología.

La composición es horizontal. Un Person contiene un Address. Si mañana necesitas que también contenga un ContactInfo, lo añades. No necesitas reestructurar una jerarquía. No rompes contratos implícitos de herencia.

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 embebidos tienen un campo con el mismo nombre, Go no lo resuelve automáticamente. Te obliga a ser explícito. Esto es una decisión de diseño: la ambigüedad se resuelve en tiempo de compilación, 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ón: ambiguo
c.A.Name = "desde A" // esto sí funciona

Esto elimina una categoría entera de bugs que en lenguajes con herencia múltiple (C++, Python) se resuelven con reglas de precedencia que nadie recuerda.


Tags JSON: marshaling y unmarshaling

Los tags de struct son una de las features más útiles y menos glamurosas de Go. Permiten añadir metadatos a los campos que las librerías pueden leer mediante reflexión. El caso más común: controlar cómo se serializa 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 es un string entre backticks que sigue la convención key:"value". Para JSON:

  • json:"name" define el nombre del campo en JSON.
  • json:"description,omitempty" omite el campo si tiene su valor cero (string vacío, 0, false, nil).
  • json:"-" excluye el campo de la serialización. Útil para campos internos que no deben salir en la respuesta de una API REST.

Marshaling: struct a JSON

p := Product{
    ID:      1,
    Name:    "Teclado mecánico",
    Price:   89.99,
    InStock: true,
    InternalSKU: "KB-001-MX",
}

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

Resultado:

{"id":1,"name":"Teclado mecánico","price":89.99,"in_stock":true,"created_at":"0001-01-01T00:00:00Z"}

Fíjate: description no aparece porque usamos omitempty y estaba vacío. InternalSKU no aparece porque usamos "-". Los nombres de campo siguen la convención del tag, no la de Go.

Unmarshaling: JSON a struct

raw := `{"id":2,"name":"Ratón ergonómico","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)  // "Ratón ergonómico"
fmt.Println(p.Price) // 45.5

El unmarshaling ignora campos del JSON que no tienen correspondencia en el struct, y deja en valor cero los campos del struct que no están en el JSON. Sin excepciones, sin errores. Esto es pragmatismo puro.

Tags más allá de JSON

Los tags no son exclusivos de JSON. Librerías como pgx (PostgreSQL), yaml, xml, validate y muchas otras los usan:

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 clave es que los tags son solo metadatos. El compilador no los valida. Si escribes json:"naem" en vez de json:"name", compilará sin problema y tendrás un bug silencioso. Herramientas como go vet y linters ayudan a detectar estos errores.


Visibilidad: exportado vs no exportado

Go no tiene public, private ni protected. La visibilidad se controla con una regla brutal en su simplicidad: si empieza con mayúscula, es exportado (público). Si empieza con minúscula, no es exportado (privado al paquete).

type User struct {
    ID       int    // exportado: visible fuera del paquete
    Name     string // exportado
    email    string // NO exportado: solo visible dentro del paquete
    password string // NO exportado
}

Esto aplica a todo: structs, campos, funciones, métodos, constantes, variables. Es una convención enforced por el compilador, no por un keyword. No puedes “saltártela” con reflexión de forma accidental.

Implicaciones prácticas

Cuando diseñas structs que forman parte de una API pública (un paquete que otros van a importar), la visibilidad de los campos importa mucho:

// paquete auth

type Credentials struct {
    Username string // otros paquetes pueden leerlo y escribirlo
    password string // solo el paquete auth puede acceder
}

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)
}

Desde fuera del paquete:

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

El campo password es inaccesible desde fuera. No necesitas getters/setters como en Java. El encapsulamiento es real y lo garantiza el compilador.

Visibilidad y JSON

Un detalle que pilla a muchos: los campos no exportados no se serializan a JSON. Si un campo empieza con minúscula, encoding/json lo ignora completamente:

type Response struct {
    Status  string `json:"status"`
    message string `json:"message"` // NUNCA aparecerá en el JSON
}

Esto es por diseño. Si necesitas que un campo aparezca en JSON, tiene que ser exportado. No hay forma de evitarlo con tags. Es una regla simple que evita exponer accidentalmente datos internos.


Comparación e igualdad de structs

Los structs en Go son comparables si todos sus campos son comparables. Puedes usar == directamente:

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

Esto funciona porque int es comparable. Pero no todos los structs son comparables:

type Data struct {
    Values []int // los slices NO son comparables
}

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

Los slices, maps y funciones no se pueden comparar con ==. Si tu struct contiene alguno de estos tipos, necesitas escribir tu propia función de comparación o usar reflect.DeepEqual (que es lento y debe reservarse para 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 necesitas comparar structs complejos en código de producción, implementa un método Equal. Si es solo para tests, reflect.DeepEqual o librerías como go-cmp son aceptables.

Los structs comparables también pueden usarse como claves de maps, lo cual es útil para caches y lookups:

type Coordinate struct {
    Lat, Lng float64
}

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

Cuándo usar structs vs maps

Esta es una pregunta que surge mucho, sobre todo si vienes de Python o JavaScript, donde los diccionarios/objetos son la herramienta por defecto para todo.

En Go, la respuesta casi siempre es “usa un struct”. Pero hay excepciones.

Usa structs cuando:

  • Conoces la forma de los datos en tiempo de compilación.
  • Los campos tienen tipos diferentes.
  • Necesitas métodos asociados.
  • Quieres validación del compilador.
  • Los datos se serializan/deserializan con una estructura conocida.
// Esto es un struct. Siempre.
type Order struct {
    ID        string
    Customer  string
    Items     []OrderItem
    Total     float64
    Status    string
    CreatedAt time.Time
}

Usa maps cuando:

  • La forma de los datos es dinámica o desconocida en tiempo de compilación.
  • Las claves son dinámicas (configuración, headers HTTP, metadatos).
  • Estás trabajando con datos genéricos que no tienen una estructura fija.
// Metadatos dinámicos: esto es un map
metadata := map[string]string{
    "source":  "api",
    "version": "2.1",
    "region":  "eu-west",
}

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

El anti-pattern: map[string]interface

Si te encuentras escribiendo map[string]interface{} (o map[string]any desde Go 1.18) en todas partes, probablemente necesitas un struct. Los maps sin tipo pierden todas las garantías del compilador y convierten tu código Go en JavaScript con pasos extra.

// NO hagas esto para datos con estructura conocida
data := map[string]any{
    "name":  "Roger",
    "age":   32,
    "email": "roger@oshy.tech",
}
name := data["name"].(string) // type assertion, puede hacer panic en runtime

// Haz esto
user := User{
    Name:  "Roger",
    Age:   32,
    Email: "roger@oshy.tech",
}
// user.Name es string. Siempre. Sin discusión.

Ejemplo práctico: modelar una respuesta de API

Vamos a juntar todo lo visto con un ejemplo real: modelar la respuesta de una API que devuelve una lista de artículos paginada. Este es el tipo de código que escribes constantemente en un backend Go.

package api

import (
    "encoding/json"
    "time"
)

// Article representa un artículo 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 exportado: no aparece en JSON, no accesible fuera del paquete
}

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

// Pagination encapsula los datos de paginación.
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 es la respuesta completa de la API.
type ArticleListResponse struct {
    Data       []Article  `json:"data"`
    Pagination Pagination `json:"pagination"`
}

// NewPagination calcula los campos derivados automáticamente.
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 serializa la respuesta. Método con value receiver
// porque ArticleListResponse no necesita ser modificado.
func (r ArticleListResponse) ToJSON() ([]byte, error) {
    return json.MarshalIndent(r, "", "  ")
}

Usándolo:

response := api.ArticleListResponse{
    Data: []api.Article{
        {
            ID:    "1",
            Title: "Structs en 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))

Resultado:

{
  "data": [
    {
      "id": "1",
      "title": "Structs en 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 este ejemplo aparecen todos los conceptos: structs con campos tipados, composición (Author dentro de Article), tags JSON con omitempty, campos no exportados (viewCount), funciones constructoras (NewPagination), métodos con value receiver (ToJSON), y serialización limpia.


Patrones comunes en código backend real

Para cerrar, estos son patrones que verás (y escribirás) constantemente si haces backend en Go. No son teóricos: salen directamente de código en producción.

El patrón Options para constructores complejos

Cuando un constructor tiene demasiados parámetros, el patrón funcional options es idiomático en 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),
)

Este patrón te da valores por defecto sensatos, parámetros opcionales con nombre, y la posibilidad de extender la configuración sin romper la API. Lo usan librerías como google.golang.org/grpc y go.uber.org/zap.

Struct como receptor de servicio

En backend Go, los servicios y repositorios son structs con dependencias inyectadas por constructor. Es el equivalente a una clase con @Service en Spring, pero sin anotaciones ni reflexión:

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
}

Fíjate que UserRepository y Cache son interfaces, no structs concretos. Esto permite inyectar implementaciones diferentes en tests (mocks) y en producción. La regla de Go aplica: acepta interfaces, devuelve structs.

DTOs separados de entidades de dominio

No mezcles tu modelo de dominio con tus DTOs de API. Son responsabilidades distintas:

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

// DTO de respuesta (capa de 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ón 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),
    }
}

Es más código que poner tags JSON directamente en la entidad. Pero cuando tu API necesite devolver datos de forma diferente a como los almacenas (y esto pasa siempre), tendrás la separación hecha. Lo explico en más detalle en estructura de proyecto y arquitectura limpia en Go.

Structs como configuración tipada

En vez de leer variables de entorno como strings desperdigados por el código, centraliza la configuración 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"`
}

Con una librería como github.com/caarlos0/env puedes llenar este struct directamente desde variables de entorno. Resultado: configuración tipada, con valores por defecto, con validación, y sin un solo os.Getenv suelto en tu código.


Menos ceremonia, más claridad

Los structs en Go son simples por diseño. No tienen herencia, no tienen constructores mágicos, no tienen visibilidad granular con cinco keywords. Y eso es precisamente lo que los hace poderosos: te obligan a modelar tus datos de forma explícita, a componer en vez de heredar, a ser claro en vez de abstracto.

Si vienes de lenguajes con orientación a objetos clásica, el cambio puede parecer un paso atrás. Pero después de usar Go en producción durante un tiempo, te das cuenta de que la mayoría de abstracciones OOP que usabas no eran necesarias. Eran ceremoniales. Go elimina la ceremonia y te deja con lo que importa: datos, comportamiento, y composición.

Los structs son la base. Sobre ellos construyes servicios, handlers de API, repositorios, configuración, DTOs. Para el siguiente paso, entiende cómo las interfaces en Go complementan a los structs, y cómo juntos forman el sistema de tipos que hace que Go sea Go. Y cuando estés listo para construir algo real, una API REST con Go es el mejor campo de pruebas.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados