Saltar al contenido principal

Go desde cero, parte 5: structs, métodos y composición (clases no incluidas)

> serie: aprende go de 0 a 100 | artículo 5 de 19

Go no tiene clases, tampoco herencia, y aun así se escriben sistemas enormes con él. El truco, los structs en Go para los datos, métodos para el comportamiento y composición para reutilizar. Vamos por partes.


Structs: datos y punto

Un struct es una colección de campos con nombre (A Tour of Go, structs):

type Usuario struct {
  Nombre string
  Email string
  Activo bool
}

// Literales: posicionales o con nombre (mejor con nombre)
u := Usuario{Nombre: "Jenn", Email: "jenn@ejemplo.dev", Activo: true}

// Acceso directo con punto. Sin getters ni setters.
u.Activo = false

Detalle con miga: la visibilidad en Go va por mayúsculas. Campo o función que empieza en mayúscula es exportado (público fuera del paquete), en minúscula es privado del paquete. No hay public ni private: la propia letra lo dice (A Tour of Go, exported names).

Métodos: funciones con receptor

Un método es una función con un parámetro especial delante, el receptor (A Tour of Go, methods):

type Rectangulo struct {
  Ancho, Alto float64
}

// (r Rectangulo) es el receptor: convierte
// la función en método del tipo.
func (r Rectangulo) Area() float64 {
  return r.Ancho * r.Alto
}

r := Rectangulo{Ancho: 3, Alto: 4}
fmt.Println(r.Area()) // 12

Si vienes de Python, el receptor es tu self pero con tipo y nombre corto. Si vienes de Java, es el this sacado a la firma. La gracia: puedes definir métodos sobre CUALQUIER tipo tuyo, no solo structs. Aquel type Nivel int del artículo 2 puede tener métodos.

Receptor por valor o por puntero

La decisión más importante de este artículo (A Tour of Go, pointer receivers):

// Receptor por valor: trabaja con una COPIA.
func (r Rectangulo) Duplica() {
  r.Ancho *= 2 // se pierde al salir
}

// Receptor por puntero: modifica el original.
func (r *Rectangulo) DuplicaBien() {
  r.Ancho *= 2
}

r.DuplicaBien() // Go hace (&r) por ti

Regla práctica para los structs en Go: si el método modifica el receptor o el struct es grande, puntero. Si es de solo lectura y pequeño, valor. Y por coherencia: si un método del tipo usa puntero, que lo usen todos. Los punteros tienen su artículo completo (el 10).

Composición: el embedding

Go no tiene herencia y lo dice sin complejos (FAQ, inheritance). Lo que ofrece es embedding: meter un tipo dentro de otro sin nombre de campo, y sus métodos y campos se promocionan al contenedor (Effective Go, embedding):

type Motor struct {
  Caballos int
}

func (m Motor) Arranca() string {
  return "brummm"
}

type Coche struct {
  Motor // embebido: sin nombre de campo
  Marca string
}

c := Coche{Motor: Motor{Caballos: 90}, Marca: "Retro"}
fmt.Println(c.Arranca()) // método promocionado
fmt.Println(c.Caballos) // campo promocionado

Esto no es herencia disfrazada: Coche no ES un Motor, TIENE un motor y delega en él. No hay jerarquías frágiles ni diamantes de la muerte, y el acoplamiento queda a la vista en el struct.

La pregunta de si Go es orientado a objetos tiene respuesta oficial: sí y no.(FAQ)

Structs en Go: Diagrama pixelart de un struct Coche con un Motor embebido cuyos métodos quedan promocionados hacia fuera
[Composición: el Motor presta sus métodos sin necesidad de herencia.]

El constructor que no es constructor

Sin clases no hay constructores. La convención: una función NewTipo que valida y devuelve puntero (Effective Go, composite literals):

// Constructor idiomático: función New que valida.
func NewUsuario(nombre, email string) (*Usuario, error) {
  if email == "" {
    return nil, errors.New("email obligatorio")
  }
  return &Usuario{Nombre: nombre, Email: email, Activo: true}, nil
}

Reto de la semana: pon a prueba los structs en Go

  • Modela Rectangulo y Circulo con métodos Area() y Perimetro(). Guárdalos: los reutilizamos en el artículo 6.
  • Crea un tipo Cuenta con saldo y métodos Ingresa y Retira. Decide receptor valor o puntero y justifícalo en un comentario.
  • Bonus: embebe un struct Registro con método Log(msg string) dentro de Cuenta y llama a cuenta.Log(…) directamente.

Structs en Go: nivel completado.

En el próximo artículo: interfaces, o cómo Go hace duck typing con verificación en compilación.

> exit
Retrato pixel art de Jenniffer Cubillos

gracias por leer