Volver al blog

La palabra clave struct en Go: campos, etiquetas, incrustación y métodos

Cómo funciona la palabra clave struct en Go: declaración de campos, valores cero y literales compuestos, campos exportados y JSON, etiquetas de struct con omitempty y omitzero, receptores de valor frente a receptores de puntero, incrustación, comparación y copia, el struct vacío y el orden de los campos.

La palabra clave struct en Go: campos, etiquetas, incrustación y métodos

La palabra clave struct agrupa campos con nombre en un solo tipo. Una declaración type Order struct { ... } es la forma de describir en Go un pedido, un usuario, el cuerpo de una petición HTTP o un archivo de configuración. Los structs no tienen constructores ni herencia. El comportamiento se añade con métodos, el código se reutiliza incrustando un struct dentro de otro y la codificación se controla con etiquetas de struct (especificación de Go).

Resumen rápido

  • type Order struct { ... } declara un tipo struct. Cada campo tiene un nombre y un tipo, y los campos del mismo tipo pueden compartir línea.
  • En el valor cero de un struct, cada campo tiene su propio valor cero, así que var o Order se puede usar sin ninguna preparación.
  • Escribe los literales compuestos con nombres de campo, como en Order{ID: 1001, Total: 4999}. go vet señala los literales sin claves de tipos de otros paquetes.
  • Solo los campos exportados (con mayúscula inicial) son visibles para encoding/json y para otros paquetes. Un campo en minúscula se omite sin ningún error.
  • Las etiquetas de struct como json:"email,omitempty" controlan la codificación. Usa omitzero (Go 1.24) para time.Time y otros structs, porque omitempty nunca los omite.
  • Los métodos con receptor de puntero pueden modificar el struct. Los métodos con receptor de valor trabajan sobre una copia.
  • Incrustar un tipo promueve sus campos y sus métodos al struct exterior. Así es como Go reutiliza código sin herencia.
  • Los structs solo se pueden comparar con == cuando todos sus campos son comparables. Asignar un struct lo copia, pero los slices y los maps que contiene siguen compartidos.

¿Cómo se declara un struct en Go?

Escribe type, el nombre, struct y los campos entre llaves. Cada campo es un nombre seguido de un tipo:

example.gogo
package main

import (
	"fmt"
	"time"
)

type Order struct {
	ID         int64
	CustomerID int64
	Status     string
	Total      int64 // cents
	CreatedAt  time.Time
}

type Address struct {
	Street, City, Country string
}

func main() {
	var o Order
	fmt.Printf("%+v\n", o)
	fmt.Println(o.Status == "", o.CreatedAt.IsZero())
}
example.texttext
{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC}
true true

Address muestra la forma corta. Los campos del mismo tipo pueden compartir línea, separados por comas. Aun así, la mayoría de los códigos base ponen un campo por línea, porque así los diffs son pequeños y queda sitio para una etiqueta o un comentario.

var o Order no necesita constructor. Cada campo empieza con su valor cero: 0 para los números, "" para las cadenas, nil para los punteros, slices y maps, y el time.Time cero para CreatedAt. La palabra clave var en Go explica los valores cero a fondo.

Como cualquier tipo, un struct se puede declarar dentro de una función cuando solo esa función lo necesita. La palabra clave type en Go muestra un struct local loginRequest que se usa para decodificar un cuerpo JSON.

¿Cómo se crea un valor de struct?

Lo habitual es usar el valor cero con var, un literal compuesto con nombres de campo o un puntero a un literal con &:

example.gogo
keyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999}
partial := Order{ID: 1002, Status: "pending"}
positional := Order{1003, 42, "paid", 1250}
p := &Order{ID: 1004}
p.Status = "shipped"
example.texttext
{ID:1001 CustomerID:42 Status:paid Total:4999}
{ID:1002 CustomerID:0 Status:pending Total:0}
{ID:1003 CustomerID:42 Status:paid Total:1250}
{ID:1004 CustomerID:0 Status:shipped Total:0}

Un literal con claves puede listar cualquier subconjunto de campos en cualquier orden, y el resto recibe su valor cero. Un literal posicional tiene que listar todos los campos en el orden de declaración, y eso lo hace frágil. Si añades un campo Currency a Order, todos los literales posicionales dejan de compilar, mientras que los que usan claves siguen funcionando.

Con tipos struct de otros paquetes, go vet avisa de los literales posicionales mediante su analizador composites:

example.gogo
addr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttext
main.go:9:10: net.TCPAddr struct literal uses unkeyed fields

p.Status funciona sobre un puntero sin escribir (*p).Status. Go desreferencia automáticamente un puntero a struct cuando accedes a un campo.

¿Cómo se asigna un campo puntero opcional en una sola línea?

Usa new con un valor, algo que funciona desde Go 1.26. Los campos puntero son la forma habitual de distinguir «no enviado» de «enviado como cero» en el cuerpo de una petición PATCH, y antes de Go 1.26 necesitabas una variable temporal o una función auxiliar para cada uno:

example.gogo
type UpdateUser struct {
	Name  *string `json:"name,omitempty"`
	Email *string `json:"email,omitempty"`
	Age   *int    `json:"age,omitempty"`
}

func main() {
	patch := UpdateUser{
		Email: new("[email protected]"),
		Age:   new(31),
	}
	out, _ := json.Marshal(patch)
	fmt.Println(string(out))
}
example.texttext
{"email":"[email protected]","age":31}

new(31) reserva un int, guarda 31 en él y devuelve el puntero (notas de la versión Go 1.26). Con go 1.25 en go.mod, el compilador lo rechaza:

example.texttext
./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)

¿Por qué faltan en el JSON los campos de struct en minúscula?

Porque no están exportados, y otros paquetes no pueden ver los campos no exportados. encoding/json es otro paquete, así que los omite sin ningún error:

example.gogo
type User struct {
	ID       int64
	Email    string
	password string
	role     string
}

func main() {
	u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"}
	out, err := json.Marshal(u)
	fmt.Println(string(out), err)

	var in User
	err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in)
	fmt.Printf("%+v %v\n", in, err)
}
example.texttext
{"ID":7,"Email":"[email protected]"} <nil>
{ID:8 Email:[email protected] password: role:} <nil>

Las dos llamadas devuelven un error nil, y el role de la entrada se descarta sin ningún aviso. Cuando una API en Go devuelve {} o un campo se queda vacío después de decodificar, esto es lo primero que conviene revisar. La solución es poner el campo en mayúscula y usar una etiqueta para conservar el nombre en minúscula en el JSON.

La misma regla se aplica a todos los paquetes que leen campos mediante reflexión: encoding/xml, los escáneres de database/sql, GORM, las bibliotecas de YAML y la ejecución de plantillas. También puedes aprovecharlo a propósito. Un campo no exportado es un lugar seguro para datos que nunca deben codificarse, como una conexión a la base de datos o una caché.

¿Qué son las etiquetas de struct en Go?

Una etiqueta de struct es un literal de cadena que va después del tipo de un campo. El propio Go la ignora, y los paquetes la leen mediante reflexión para decidir cómo tratar ese campo. La etiqueta JSON es la que más vas a escribir:

example.gogo
type User struct {
	ID           int64     `json:"id"`
	Email        string    `json:"email"`
	Nickname     string    `json:"nickname,omitempty"`
	PasswordHash string    `json:"-"`
	DeletedAt    time.Time `json:"deleted_at,omitempty"`
	LastLoginAt  time.Time `json:"last_login_at,omitzero"`
}

func main() {
	u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	enc.Encode(u)
}
example.texttext
{
  "id": 7,
  "email": "[email protected]",
  "deleted_at": "0001-01-01T00:00:00Z"
}

Esto es lo que pasa en la salida, campo a campo:

  • json:"id" cambia el nombre del campo en la salida.
  • omitempty descarta el campo cuando vale false, 0, "", nil o es un slice o un map vacío. Nickname está vacío, así que desaparece.
  • json:"-" no codifica nunca el campo. PasswordHash queda fuera de todas las respuestas.
  • omitempty no funciona con structs. DeletedAt es un time.Time cero y aun así aparece como 0001-01-01T00:00:00Z, que los clientes suelen interpretar como una fecha real.
  • omitzero, añadido en Go 1.24, descarta un campo cuando tiene el valor cero de su tipo. Con tipos que tienen un método IsZero() bool, como time.Time, llama a ese método. LastLoginAt desaparece (notas de la versión Go 1.24).

En código nuevo, usa omitzero en los campos de tipo struct y en time.Time. omitempty sigue siendo adecuado para cadenas, números, slices y maps.

Un campo puede llevar etiquetas para varios paquetes, separadas por espacios: `json:"email" db:"email" validate:"required,email"`. Cada paquete lee solo su propia clave con reflect.StructTag.Get:

example.gogo
f, _ := reflect.TypeFor[User]().FieldByName("Nickname")
fmt.Println(f.Tag.Get("json"))
example.texttext
nickname,omitempty

¿Por qué se ignora mi etiqueta de struct?

Normalmente por un espacio o por unas comillas que faltan. El formato es estricto: key:"value", sin espacio después de los dos puntos. json: "email" le parece correcto a una persona, pero Tag.Get("json") devuelve una cadena vacía, así que el campo se codifica como Email. El compilador acepta cualquier cadena como etiqueta, así que no se queja. go vet sí lo hace, mediante su analizador structtag, que además detecta dos campos con el mismo nombre JSON:

example.gogo
type User struct {
	Email string `json: "email"`
	Name  string `json:"name" db:"name"`
	ID    int64  `json:"id"`
	Ref   int64  `json:"id"`
}
example.texttext
main.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6

Los editores que usan gopls muestran los mismos avisos mientras escribes.

¿Cómo funcionan los métodos sobre structs?

Un método es una función con un receptor, que se escribe antes del nombre. El receptor es un valor (c Cart) o un puntero (c *Cart), y esa elección decide si el método puede modificar el struct:

example.gogo
type Cart struct {
	Items []string
	Total int64
}

func (c Cart) AddValue(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func (c *Cart) Add(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func main() {
	var c Cart
	c.AddValue("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)

	c.Add("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)
}
example.texttext
0 0
1 4999

AddValue compila y se ejecuta, pero el carrito sigue vacío. Un receptor de valor es una copia del struct, así que el método actualiza la copia y luego la descarta. El código Go nuevo cae a menudo en este error, porque ningún error ni aviso lo señala. Add recibe un puntero y modifica el carrito de quien lo llama.

No hace falta escribir (&c).Add(...). Cuando c es direccionable, Go toma la dirección por ti. Lo contrario también funciona: un puntero puede llamar a métodos con receptor de valor.

Usa un receptor de puntero cuando el método modifica el struct, cuando el struct es grande o cuando contiene un sync.Mutex u otro campo similar que no se debe copiar. Usa un receptor de valor para valores pequeños e inmutables, como un Money o un Point. Si algún método de un tipo necesita receptor de puntero, dáselo a todos. Mezclar los dos hace que el conjunto de métodos sea más difícil de razonar, porque entonces solo *Cart tiene todos los métodos, y solo *Cart satisface las interfaces que los necesitan todos (especificación de Go).

¿Cómo funciona la incrustación de structs en Go?

Un campo escrito solo con un tipo y sin nombre es un campo incrustado. Sus campos y métodos se promueven, lo que significa que puedes usarlos como si estuvieran declarados en el struct exterior:

example.gogo
type Timestamps struct {
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (t *Timestamps) Touch(now time.Time) {
	if t.CreatedAt.IsZero() {
		t.CreatedAt = now
	}
	t.UpdatedAt = now
}

type Order struct {
	ID int64
	Timestamps
}

type User struct {
	ID    int64
	Email string
	Timestamps
}

func main() {
	now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC)
	var o Order
	o.Touch(now)
	fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen))

	u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}}
	fmt.Println(u.CreatedAt.Year())
}
example.texttext
2026-09-26 9:00AM
2026

o.Touch(now) es una forma abreviada de o.Timestamps.Touch(now). El campo incrustado sigue existiendo con el nombre de su tipo, y así es también como se asigna en un literal: Timestamps: Timestamps{...}. encoding/json también aplana los structs incrustados, así que el Order de arriba se codifica como {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"}.

La incrustación no es herencia. Un Order no es un Timestamps, y no puedes pasar un Order a una función que recibe un Timestamps. Cuando se ejecuta Touch, su receptor es el Timestamps interior. No tiene forma de llegar al Order que lo rodea ni de llamar a los métodos que define Order. Una subclase en Java o Python sí puede hacerlo, y de ahí salen las preguntas del tipo «¿qué sobrescritura se ejecuta?». En Go no se plantean.

Cuando el struct exterior declara un campo o un método con el mismo nombre, gana el exterior, y el incrustado sigue siendo accesible a través del nombre del tipo:

example.gogo
type Base struct{ ID int64 }

func (Base) Describe() string { return "base" }

type Order struct {
	Base
	ID string
}

func (Order) Describe() string { return "order" }

func main() {
	o := Order{Base: Base{ID: 42}, ID: "ord_42"}
	fmt.Println(o.ID, o.Base.ID)
	fmt.Println(o.Describe(), o.Base.Describe())
}
example.texttext
ord_42 42
order base

¿Se puede incrustar una interfaz en un struct?

Sí, y es una forma habitual de envolver un tipo sobrescribiendo un solo método. Un middleware HTTP que registra el código de estado incrusta http.ResponseWriter, obtiene Header y Write sin hacer nada y reemplaza solo WriteHeader:

example.gogo
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

func logStatus(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, req)
		slog.Info("request", "path", req.URL.Path, "status", rec.status)
	})
}
example.texttext
2026/09/26 09:04:44 INFO request path=/orders/99 status=404

Como *statusRecorder tiene los tres métodos, satisface http.ResponseWriter y se puede pasar al siguiente handler. Si la interfaz incrustada es nil, llamar a uno de sus métodos provoca un panic, así que asígnala siempre en el literal.

¿Conviene incrustar sync.Mutex en un struct?

En un tipo exportado, no. La incrustación promueve Lock y Unlock a la API pública del tipo, así que cualquiera que lo use puede bloquear tu caché desde fuera:

example.gogo
type Cache struct {
	sync.Mutex
	items map[string]string
}

Un campo con nombre y no exportado mantiene el bloqueo como un detalle de implementación:

example.gogo
type SafeCache struct {
	mu    sync.Mutex
	items map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	v, ok := c.items[key]
	return v, ok
}

El valor cero de sync.Mutex es un mutex desbloqueado, así que SafeCache no necesita ninguna preparación para el bloqueo. Lo que sí necesita es crear su map antes de la primera escritura.

¿Se pueden comparar y copiar structs en Go?

Puedes comparar dos structs con == cuando todos sus campos son comparables. La comparación se hace campo a campo. Eso convierte a los structs pequeños en buenos tipos de valor y buenas claves de map:

example.gogo
type Money struct {
	Amount   int64
	Currency string
}

type RouteKey struct {
	Method string
	Path   string
}

func main() {
	fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"})

	hits := map[RouteKey]int{}
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"POST", "/orders"}]++
	fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits))
}
example.texttext
true
2 2

Una clave compuesta como RouteKey es más limpia que pegar cadenas como "GET /orders", y no puede colisionar cuando una ruta contiene un espacio. Aquí los literales posicionales están bien, porque RouteKey se declara en el mismo paquete y tiene dos campos evidentes.

Los slices, los maps y las funciones no son comparables, así que un struct que contenga alguno de ellos tampoco lo es. El compilador te lo dice:

example.gogo
type Order struct {
	ID    int64
	Items []string
}

func main() {
	a := Order{ID: 1}
	b := Order{ID: 1}
	fmt.Println(a == b)
}
example.texttext
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)

Compara esos structs campo a campo, con slices.Equal para el slice, o con reflect.DeepEqual en los tests.

¿Asignar un struct lo copia?

Sí, pero la copia es superficial. Se copian todos los campos, y un campo slice o map se copia como una cabecera que sigue apuntando a los mismos datos:

example.gogo
type Order struct {
	ID    int64
	Items []string
	Meta  map[string]string
}

func main() {
	original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}}
	copied := original
	copied.ID = 2
	copied.Items[0] = "mouse"
	copied.Meta["source"] = "api"
	fmt.Println(original.ID, original.Items, original.Meta)
}
example.texttext
1 [mouse] map[source:api]

Cambiar copied.ID no tocó el original. Cambiar un elemento de copied.Items o una clave de copied.Meta también cambió el original. Para obtener una copia independiente, clona tú mismo esos campos con slices.Clone y maps.Clone.

La misma copia ocurre cuando pasas un struct por valor a una función. Con un struct que contiene un sync.Mutex, esa copia es un error, porque la copia recibe su propio bloqueo. go vet lo avisa mediante el analizador copylocks:

example.gogo
type Counter struct {
	mu   sync.Mutex
	hits int
}

func report(c Counter) {
	fmt.Println(c.hits)
}
example.texttext
main.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex
main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex

La solución es func report(c *Counter).

¿Qué es un struct anónimo en Go?

Es un tipo struct escrito en línea y sin nombre. Se usa cuando una forma de datos hace falta una sola vez. Los dos sitios donde más lo verás son los tests basados en tablas y la decodificación de una parte de una respuesta JSON:

example.gogo
var resp struct {
	Data struct {
		Status string `json:"status"`
	} `json:"data"`
}
body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}`
if err := json.Unmarshal([]byte(body), &resp); err != nil {
	panic(err)
}
fmt.Println(resp.Data.Status)

tests := []struct {
	name  string
	input string
	want  string
}{
	{"trims spaces", "  [email protected] ", "[email protected]"},
	{"already clean", "[email protected]", "[email protected]"},
}
for _, tt := range tests {
	got := strings.ToLower(strings.TrimSpace(tt.input))
	fmt.Println(tt.name, got == tt.want)
}
example.texttext
shipped
trims spaces true
already clean true

El decodificador solo rellena los campos que declara el struct, así que carrier y meta se ignoran. No necesitas un tipo con nombre para una respuesta de la que lees un solo campo. En cuanto la misma forma aparezca en dos sitios, ponle nombre.

¿Para qué sirve struct{}?

struct{} es un struct sin campos. Ocupa cero bytes, así que es el tipo de valor preferido cuando solo importa que algo esté presente. Los dos casos habituales son un conjunto construido sobre un map y un canal que solo sirve para avisar:

example.gogo
seen := map[string]struct{}{}
for _, id := range []string{"evt_1", "evt_2", "evt_1"} {
	if _, dup := seen[id]; dup {
		fmt.Println("skipping duplicate", id)
		continue
	}
	seen[id] = struct{}{}
}
fmt.Println(len(seen), unsafe.Sizeof(struct{}{}))

done := make(chan struct{})
go func() {
	defer close(done)
}()
<-done
fmt.Println("worker finished")
example.texttext
skipping duplicate evt_1
2 0
worker finished

map[string]struct{} expresa «conjunto de cadenas» con más claridad que map[string]bool, donde quien lee tiene que preguntarse qué significa false. chan struct{} indica que por el canal no viajan datos, solo el hecho de que se ha cerrado. ctx.Done() devuelve un <-chan struct{} por el mismo motivo.

¿El orden de los campos cambia el tamaño de un struct?

Sí. Cada campo se alinea según su tamaño, así que el compilador inserta relleno entre un campo pequeño y uno más grande que va detrás. Los mismos cinco campos ocupan el doble de memoria en el orden equivocado en una máquina de 64 bits:

example.gogo
type EventLoose struct {
	Active   bool
	ID       int64
	Retried  bool
	Attempts int32
	Urgent   bool
}

type EventPacked struct {
	ID       int64
	Attempts int32
	Active   bool
	Retried  bool
	Urgent   bool
}

func main() {
	fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{}))
}
example.texttext
32 16

EventLoose añade 7 bytes de relleno después de Active para que ID empiece en un límite de 8 bytes, y después más relleno tras Retried y Urgent. EventPacked ordena los campos del más grande al más pequeño y solo desperdicia 1 byte al final.

En la mayoría de los structs esto no importa, y agrupar los campos por significado es más legible. Empieza a importar cuando guardas millones de valores en un slice o en una caché. El analizador fieldalignment de golang.org/x/tools señala los structs que podrían ser más pequeños.

Struct de Go frente a clase: ¿qué diferencia hay?

Un struct de Go contiene datos y puede tener métodos, lo que cubre la mayor parte de lo que hace una clase en Java, C# o Python. El resto es distinto a propósito:

Struct de GoClase (Java, C#, Python)
ConstructorNinguno. El valor cero, o una función NewX por convenciónUn método constructor especial
ReutilizaciónIncrustación (composición)Herencia, además de composición
PolimorfismoInterfaces, satisfechas de forma implícitaClases base e interfaces declaradas
VisibilidadPor paquete, según la mayúscula inicialpublic, private, protected por miembro
MétodosDeclarados fuera del tipo, en cualquier parte del paqueteDeclarados dentro del cuerpo de la clase
Semántica de copiaPor valor por defecto, & para un punteroPor referencia por defecto

Cuando un struct necesita validación o valores por defecto, escribe una función normal que lo devuelva. La convención es NewX, y es normal que también devuelva un error:

example.gogo
type Client struct {
	baseURL string
	http    *http.Client
	retries int
}

func NewClient(baseURL string) (*Client, error) {
	if baseURL == "" {
		return nil, errors.New("baseURL is required")
	}
	return &Client{
		baseURL: baseURL,
		http:    &http.Client{Timeout: 10 * time.Second},
		retries: 3,
	}, nil
}

Como los campos no están exportados, el código de fuera del paquete solo puede obtener un Client a través de NewClient, con los valores por defecto ya aplicados. Como Go no tiene super ni sobrescritura, nunca tienes que recorrer una jerarquía de clases para saber qué hace un método. El comportamiento de un struct son sus propios métodos más los que incrusta, y puedes leerlos todos en un mismo paquete. Eso ayuda mucho cuando un código base en Go crece.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Composite Types construye tu primer struct, funciones constructoras, métodos e incrustación paso a paso, y luego los usa en un pequeño proyecto. Pointers & Memory trata los punteros a structs, new y los receptores de valor frente a los de puntero, y termina con un almacén de feature flags que actualiza structs en el sitio. Interfaces & Polymorphism muestra cómo los structs satisfacen interfaces sin declararlo. El Training Ground tiene ejercicios cortos e independientes para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Es struct una palabra clave en Go?

Sí. struct es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable ni de función. Inicia un tipo struct, que casi siempre recibe un nombre con type, como en type Order struct { ... }. También aparece en los structs anónimos y en el struct vacío struct{}.

¿Tiene clases Go?

No. En su lugar, Go tiene structs con métodos, interfaces e incrustación. Un struct contiene los datos, los métodos añaden comportamiento, las interfaces aportan polimorfismo y la incrustación reutiliza código mediante composición. No hay herencia ni palabra clave para constructores. Por convención, una función NewX cumple ese papel.

¿Debo usar un receptor de valor o de puntero?

Usa un receptor de puntero si el método modifica el struct, si el struct es grande o si contiene un sync.Mutex. Usa un receptor de valor para valores pequeños que nunca cambian, como un Money o un Point. Si un método de un tipo necesita receptor de puntero, haz que todos lo tengan para que el tipo se comporte de forma coherente.

¿Por qué json.Marshal devuelve un objeto vacío para mi struct?

Porque los campos del struct no están exportados. encoding/json solo ve los campos que empiezan por mayúscula, y omite los demás sin ningún error. Cambia el nombre de email a Email y añade `json:"email"` para conservar el nombre en minúscula en el JSON.

¿Qué diferencia hay entre omitempty y omitzero?

omitempty descarta false, 0, "", nil y los slices y maps vacíos, pero nunca descarta un struct, así que un time.Time cero sigue apareciendo como 0001-01-01T00:00:00Z. omitzero, añadido en Go 1.24, descarta cualquier campo que tenga el valor cero de su tipo, y usa el método IsZero del tipo cuando existe.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita