Un struct en Go es un tipo compuesto que agrupa varios campos bajo un mismo nombre. Es la pieza básica para representar entidades con datos propios.
Si vienes de un lenguaje orientado a objetos como Java, C# o C++, probablemente estés buscando la palabra reservada class.
En Go no existen las clases. Go agrupa los datos en structs y asocia el comportamiento mediante métodos.
Vamos a ver cómo definir nuestros propios tipos compuestos y cómo usar la composición en lugar de una jerarquía de clases.
Qué es un struct
Un struct es una colección de campos tipados. Es la forma que tenemos en Go de crear tipos que agrupan información relacionada.
Piensa en un Struct como el plano de un objeto, o como un “DTO” (Data Transfer Object) en otros lenguajes.
Definición
Se usa la palabra clave type, seguida del nombre, la palabra struct y las llaves.
package main
import "fmt"
// Definimos un nuevo tipo llamado 'Jugador'
type Jugador struct {
Nombre string
Nivel int
Clase string
Vivo bool
}Recuerda la regla de visibilidad: si el nombre del struct o de uno de sus campos empieza por mayúscula, estará exportado. Si empieza por minúscula, solo será accesible desde su paquete.
Crear valores de un struct
Una vez definido el “plano”, podemos crear instancias de varias formas. Go es muy flexible aquí.
Declaración con valores cero
Si declaramos la variable sin asignarle nada, Go rellena todos los campos con su Zero Value.
var j1 Jugador
// j1.Nombre es ""
// j1.Nivel es 0
// j1.Vivo es falseLiteral de struct
Podemos inicializar los valores al crearlos.
// Asignación con nombres (Recomendado)
j2 := Jugador{
Nombre: "Link",
Nivel: 10,
Clase: "Espadachín",
Vivo: true,
}
// Asignación posicional (No recomendado)
// Es frágil: si cambias el orden de los campos en el struct, esto rompe.
j3 := Jugador{"Zelda", 99, "Maga", true}Usar new
La función new(T) reserva memoria, pone todo a cero y devuelve un puntero (*T).
j4 := new(Jugador) // j4 es de tipo *Jugador
j4.Nombre = "Ganondorf"Acceso a los campos, también con punteros
Para acceder a los datos usamos el punto ..
Si tenemos un puntero a un struct, no necesitamos desreferenciarlo manualmente para seleccionar un campo. Go admite la forma abreviada.
j := Jugador{Nombre: "Mario"}
puntero := &j
// En C/C++ tendrías que hacer: (*puntero).Nombre
// En Go funciona directo:
fmt.Println(puntero.Nombre) // Imprime "Mario"
puntero.Nivel = 5 // Modifica el originalCampos embebidos y composición
En Go no existe la herencia (extends). No puedes decir que Coche hereda de Vehiculo.
En su lugar, Go apuesta todo a la Composición (Has-a en lugar de Is-a).
Para facilitarlo existen los campos embebidos (embedding).
Si declaramos un campo usando solo su tipo, sus campos y métodos pueden promocionarse al tipo exterior. No hay tipos padre e hijo: el campo embebido sigue existiendo y podemos seleccionarlo explícitamente.
type Motor struct {
Caballos int
Tipo string
}
type Coche struct {
Marca string
Motor // <--- Campo anónimo (Embedding)
}
func main() {
miCoche := Coche{
Marca: "Tesla",
Motor: Motor{
Caballos: 500,
Tipo: "Eléctrico",
},
}
// ACCESO DIRECTO (Promoción de campos)
// Podemos acceder a Caballos como si fuera de Coche
fmt.Println(miCoche.Caballos) // 500
// También podemos ser explícitos si queremos
fmt.Println(miCoche.Motor.Caballos) // 500
}Promoción y colisiones
La promoción permite usar miCoche.Arrancar() si Motor tiene ese método, aunque Coche y Motor siguen siendo tipos distintos.
Si hay colisión de nombres (por ejemplo, Coche y Motor tienen un campo Nombre), Go te obliga a usar la ruta completa (miCoche.Motor.Nombre) para resolver la ambigüedad.
Constructores en Go
Seguramente te preguntes: ¿Y dónde está el constructor?
Go no tiene constructores especiales como public Jugador() {...}.
Cuando necesitamos controlar la inicialización, la convención es crear una función normal llamada NewTipo. Puede devolver un valor o un puntero y, si la construcción puede fallar, también un error.
// Función de construcción
func NewJugador(nombre string) *Jugador {
return &Jugador{
Nombre: nombre,
Nivel: 1, // Valores por defecto lógicos
Vivo: true,
}
}
func main() {
j := NewJugador("Luigi")
}Usar estas funciones “fábrica” es muy útil cuando la inicialización del struct requiere lógica compleja o validaciones, o para asegurar que ciertos campos privados se inicializan correctamente.