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 Orderse puede usar sin ninguna preparación. - Escribe los literales compuestos con nombres de campo, como en
Order{ID: 1001, Total: 4999}.go vetseñala los literales sin claves de tipos de otros paquetes. - Solo los campos exportados (con mayúscula inicial) son visibles para
encoding/jsony 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. Usaomitzero(Go 1.24) paratime.Timey otros structs, porqueomitemptynunca 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.gogopackage 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.gogokeyed := 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.gogoaddr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttextmain.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.gogotype 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.gogotype 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.gogotype 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.omitemptydescarta el campo cuando valefalse,0,"",nilo es un slice o un map vacío.Nicknameestá vacío, así que desaparece.json:"-"no codifica nunca el campo.PasswordHashqueda fuera de todas las respuestas.omitemptyno funciona con structs.DeletedAtes untime.Timecero y aun así aparece como0001-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étodoIsZero() bool, comotime.Time, llama a ese método.LastLoginAtdesaparece (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.gogof, _ := reflect.TypeFor[User]().FieldByName("Nickname") fmt.Println(f.Tag.Get("json"))
example.texttextnickname,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.gogotype User struct { Email string `json: "email"` Name string `json:"name" db:"name"` ID int64 `json:"id"` Ref int64 `json:"id"` }
example.texttextmain.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.gogotype 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.texttext0 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.gogotype 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.texttext2026-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.gogotype 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.texttextord_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.gogotype 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.texttext2026/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.gogotype 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.gogotype 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.gogotype 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.texttexttrue 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.gogotype 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.gogotype 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.texttext1 [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.gogotype Counter struct { mu sync.Mutex hits int } func report(c Counter) { fmt.Println(c.hits) }
example.texttextmain.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.gogovar 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.texttextshipped 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.gogoseen := 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.texttextskipping 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.gogotype 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.texttext32 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 Go | Clase (Java, C#, Python) | |
|---|---|---|
| Constructor | Ninguno. El valor cero, o una función NewX por convención | Un método constructor especial |
| Reutilización | Incrustación (composición) | Herencia, además de composición |
| Polimorfismo | Interfaces, satisfechas de forma implícita | Clases base e interfaces declaradas |
| Visibilidad | Por paquete, según la mayúscula inicial | public, private, protected por miembro |
| Métodos | Declarados fuera del tipo, en cualquier parte del paquete | Declarados dentro del cuerpo de la clase |
| Semántica de copia | Por valor por defecto, & para un puntero | Por 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.gogotype 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
- The Go Programming Language Specification, Struct types: https://go.dev/ref/spec#Struct_types
- The Go Programming Language Specification, Composite literals: https://go.dev/ref/spec#Composite_literals
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Comparison operators: https://go.dev/ref/spec#Comparison_operators
- encoding/json, Marshal: https://pkg.go.dev/encoding/json#Marshal
- reflect, StructTag: https://pkg.go.dev/reflect#StructTag
- Notas de la versión Go 1.24, omitzero: https://go.dev/doc/go1.24
- Notas de la versión Go 1.26, new con una expresión: https://go.dev/doc/go1.26
- Analizadores de go vet (composites, structtag, copylocks): https://pkg.go.dev/cmd/vet
