En esta página

Structs y métodos

15 min lectura TextoCap. 2 — Funciones y estructuras

Structs: la forma de modelar datos en Go

Go no tiene clases. En lugar de eso, usa structs para agrupar datos relacionados, y métodos para asociar comportamiento a esos datos. Este modelo es más simple que la herencia de clases de Java o C#, pero igualmente poderoso cuando se combina con interfaces y embedding.

Si conoces C o Rust, los structs de Go te resultarán familiares. Si vienes de Java o Python, piensa en un struct como una clase, pero sin herencia.

Definiendo structs

// Definición básica
type Persona struct {
    Nombre   string
    Apellido string
    Edad     int
    Email    string
}

// Struct anidado
type Dirección struct {
    Calle  string
    Ciudad string
    País   string
}

type Empleado struct {
    Persona    // embedding (composición)
    Empresa    string
    Salario    float64
    Dirección  Dirección  // campo nombrado (no embedding)
}

Los nombres que empiezan con mayúscula son exportados (visibles fuera del paquete). Los que empiezan con minúscula son privados (solo visibles en el mismo paquete).

Creando instancias de structs

// 1. Literal con campos nombrados (recomendado)
p := Persona{
    Nombre:   "Ana",
    Apellido: "García",
    Edad:     28,
    Email:    "[email protected]",
}

// 2. Campos en orden (frágil, evitar)
p2 := Persona{"Luis", "Pérez", 35, "[email protected]"}

// 3. Valor cero — todos los campos inicializados en su zero value
var p3 Persona  // Nombre:"", Apellido:"", Edad:0, Email:""

// 4. Pointer al struct
pp := &Persona{Nombre: "María", Edad: 22}
pp.Email = "[email protected]"  // Go desreferencia automáticamente

Constructores: funciones factory

Go no tiene constructores especiales. El patrón idiomático es crear funciones New... que validan, inicializan y retornan el struct:

type Rectangulo struct {
    Ancho  float64
    Alto   float64
    Color  string
}

// Constructor que valida
func NuevoRectangulo(ancho, alto float64, color string) (*Rectangulo, error) {
    if ancho <= 0 || alto <= 0 {
        return nil, fmt.Errorf("dimensiones inválidas: ancho=%.2f, alto=%.2f", ancho, alto)
    }
    return &Rectangulo{
        Ancho: ancho,
        Alto:  alto,
        Color: color,
    }, nil
}

// Uso
r, err := NuevoRectangulo(5, 3, "azul")
if err != nil {
    fmt.Println("Error:", err)
    return
}

Métodos: comportamiento asociado a structs

Un método es una función con un receiver — un parámetro especial que indica a qué tipo está asociado:

// Sintaxis: func (receiver TipoReceiver) NombreMétodo(params) (retornos)

type Rectángulo struct {
    Ancho, Alto float64
}

// Receiver por valor — recibe una copia del struct
func (r Rectángulo) Área() float64 {
    return r.Ancho * r.Alto
}

func (r Rectángulo) Perímetro() float64 {
    return 2 * (r.Ancho + r.Alto)
}

// Receiver por puntero — puede modificar el struct original
func (r *Rectángulo) Escalar(factor float64) {
    r.Ancho *= factor
    r.Alto *= factor
}

func main() {
    rect := Rectángulo{Ancho: 5, Alto: 3}
    fmt.Println("Área:", rect.Área())           // 15
    fmt.Println("Perímetro:", rect.Perímetro()) // 16

    rect.Escalar(2)  // modifica rect directamente
    fmt.Println("Nuevo ancho:", rect.Ancho)  // 10
    fmt.Println("Nuevo alto:", rect.Alto)    // 6
}

Receiver por valor vs. por puntero

Esta distinción es fundamental:

type Contador struct {
    valor int
}

// Receiver por valor: NO modifica el original
func (c Contador) ValorActual() int {
    return c.valor
}

func (c Contador) Incrementar() Contador {
    c.valor++
    return c  // retorna una copia modificada
}

// Receiver por puntero: SÍ modifica el original
func (c *Contador) IncrementarInPlace() {
    c.valor++
}

func main() {
    c := Contador{valor: 0}

    c.IncrementarInPlace()  // c.valor = 1
    c.IncrementarInPlace()  // c.valor = 2
    fmt.Println(c.ValorActual())  // 2

    nuevoC := c.Incrementar()  // c.valor sigue siendo 2
    fmt.Println(nuevoC.ValorActual())  // 3
    fmt.Println(c.ValorActual())       // 2 (sin cambio)
}

Regla práctica: Si algún método necesita puntero, haz que todos los métodos del tipo usen puntero. La consistencia facilita la implementación de interfaces.

Embedding: composición sobre herencia

Go usa composición en lugar de herencia. El embedding permite que los campos y métodos de un struct se "promuevan" a otro:

type Animal struct {
    Nombre string
    Edad   int
}

func (a Animal) Describir() string {
    return fmt.Sprintf("%s tiene %d años", a.Nombre, a.Edad)
}

type Perro struct {
    Animal        // embedding — Perro "hereda" campos y métodos de Animal
    Raza   string
}

func (p Perro) Ladrar() string {
    return "¡Woof!"
}

func main() {
    rex := Perro{
        Animal: Animal{Nombre: "Rex", Edad: 3},
        Raza:   "Labrador",
    }

    // Acceso directo a campos y métodos embebidos
    fmt.Println(rex.Nombre)       // "Rex" (promovido de Animal)
    fmt.Println(rex.Describir())  // "Rex tiene 3 años" (método promovido)
    fmt.Println(rex.Ladrar())     // "¡Woof!"
    fmt.Println(rex.Raza)         // "Labrador"

    // También puedes acceder explícitamente
    fmt.Println(rex.Animal.Nombre)
}

Sobreescribir métodos embebidos

func (p Perro) Describir() string {
    // Llama al método embebido y agrega información extra
    return p.Animal.Describir() + fmt.Sprintf(", raza: %s", p.Raza)
}

Struct tags: metadatos para serialización

Los struct tags son metadatos que se añaden a los campos usando backticks. Son usados por paquetes como encoding/json, encoding/xml, ORMs y frameworks de validación:

type Usuario struct {
    ID        int    `json:"id" db:"user_id"`
    Nombre    string `json:"nombre" db:"nombre" validate:"required,min=2"`
    Email     string `json:"email" db:"email" validate:"required,email"`
    Contraseña string `json:"-"`  // omitir en JSON
    CreatedAt  time.Time `json:"created_at,omitempty"`
}

// Serialización JSON
u := Usuario{ID: 1, Nombre: "Ana", Email: "[email protected]"}
datos, err := json.Marshal(u)
// {"id":1,"nombre":"Ana","email":"[email protected]"}

// Deserialización JSON
var u2 Usuario
json.Unmarshal([]byte(`{"id":2,"nombre":"Luis","email":"[email protected]"}`), &u2)

Los tags más comunes:

  • json:"nombre" — nombre del campo en JSON
  • json:"-" — omitir campo en JSON
  • json:",omitempty" — omitir si el valor es cero
  • db:"nombre" — nombre de columna en base de datos
  • validate:"required" — validación con paquetes como go-playground/validator

Structs anónimos

Útiles para datos temporales o pruebas:

// Struct anónimo en variable
configuración := struct {
    Host    string
    Puerto  int
    Debug   bool
}{
    Host:   "localhost",
    Puerto: 8080,
    Debug:  true,
}

// Slice de structs anónimos (común en tests)
casos := []struct {
    entrada   int
    esperado  int
}{
    {1, 1},
    {5, 120},
    {10, 3628800},
}

Comparación de structs

Los structs son comparables con == si todos sus campos son comparables (no hay slices, maps ni funciones):

type Punto struct{ X, Y int }

p1 := Punto{1, 2}
p2 := Punto{1, 2}
p3 := Punto{3, 4}

fmt.Println(p1 == p2)  // true
fmt.Println(p1 == p3)  // false

Con el dominio de structs y métodos, en la próxima lección aprenderemos las interfaces — el mecanismo que hace que el polimorfismo en Go sea elegante e implícito.

Receiver por valor vs. por puntero
Usa receiver por puntero (*T) cuando el método necesita modificar el struct, cuando el struct es grande (evita copias costosas), o para consistencia (si algún método necesita puntero, hazlos todos puntero). Usa receiver por valor (T) cuando el método solo lee el struct y el struct es pequeño.
Go usa composición, no herencia
Go no tiene herencia de clases. En su lugar, usa embedding (composición). Cuando embutes un struct dentro de otro, los métodos y campos del struct interno se promueven al externo. Esto logra reutilización de código sin los problemas de la herencia profunda.