06 — Structs — Go's Composition-First Record Type
Go's closest thing to a class turned out to be a record with named fields and no hierarchy — which is exactly the point. The model that clicked: a struct is a fixed set of typed fields with no inheritance and no methods baked into the declaration; you compose complex types by embedding one struct inside another, and you control serialization with tags. [1] Go's whole philosophy — _composition over inheritance_ — is most visible here. Once I stopped reaching for class hierarchies and started reaching for embedding and interfaces, structs stopped feeling limited and started feeling like the cleanest record type I had used.
The basics — fields and literals
A struct is a collection of named fields, declared with type ... struct [1]:
type User struct {
ID int
Name string
Email string
}
u := User{ID: 1, Name: "Ave", Email: "ave@example.com"}
fmt.Println(u.Name) // field access by dot notationFields are accessed by dot notation, and a struct literal can name its fields (the Field: value form, which is order-independent and survives reordering) or list them positionally (brittle, avoided outside small tests). An uninitialized struct — var u User — gets the zero value of _every_ field, so u.ID == 0, u.Name == "". As with the zero-value habit from the variables notes, designing structs so their all-zero state is useful is a real idiom: http.Server{} with no fields set is a valid server with sensible defaults.
Struct tags — metadata for serialization
Any field can carry a tag — a backtick-enclosed string of key:"value" pairs that the reflection-based packages read at runtime [2]:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Password string `json:"-"`
}The encoding/json package is the most common consumer. json:"name" renames the field in the JSON output (Go's Name becomes lowercase name by convention). omitempty drops the field when it holds its zero value. json:"-" excludes the field entirely — the standard way to keep a Password out of serialized output [2]. Other tag keys serve other ecosystems: db:"..." for SQL drivers, xml:"..." for XML, validate:"..." for validators [4]. Tags are how a single struct type participates in multiple wire formats without changing its definition.
Embedding — composition instead of inheritance
Go has no inheritance, no extends, no super. The replacement is embedding: placing one struct type inside another _without a field name_, which "promotes" the embedded type's fields and methods onto the outer struct [3]:
type Address struct {
City string
Country string
}
type User struct {
ID int
Name string
Address // embedded — no field name
}
u := User{ID: 1, Name: "Ave", Address: Address{City: "Jakarta", Country: "ID"}}
fmt.Println(u.City) // promoted — same as u.Address.City
fmt.Println(u.Country) // promotedBecause Address is embedded anonymously, its City and Country fields are reachable directly on u — they are _promoted_. This is not inheritance: there is no subtype relationship, no polymorphism, no method dispatch up a chain. It is flat composition with a syntactic convenience. The power compounds when the embedded type has methods (covered in the methods notes): those methods are promoted too, which is how Go builds rich types from small pieces.
Two rules keep embedding honest. You cannot embed two types that promote a field with the same name (the compiler rejects the ambiguity). And embedding a value versus a pointer changes whether the outer struct can call pointer-receiver methods on the inner type — a detail that matters once methods enter the picture [5].
How I use this
Two design habits came from the composition-first model. When I would once have written a class hierarchy — AdminUser extends User, GuestUser extends User — I now write one User struct and a small set of behaviors as interfaces that User satisfies. The variants become field differences or embed additional capability structs, never subclasses. And for anything crossing a process boundary, I tag the fields up front: the json:"-" on secrets and omitempty on optional fields are decisions I make at the struct definition, not retrofit after a serialization bug. The result is record types that double cleanly as API payloads and stay readable as they grow.
References
[1] The Go Authors, "Go Tour: Structs," go.dev, 2024. [Online]. Available: https://go.dev/tour/moretypes/2
[2] The Go Authors, "Well-known struct tags," Go Wiki, 2024. [Online]. Available: https://go.dev/wiki/Well-known-struct-tags
[3] Go by Example, "Struct Embedding," 2024. [Online]. Available: https://gobyexample.com/struct-embedding
[4] S. Dubey, "Working with JSON and Struct Tags in Go," Medium, 2024. [Online]. Available: https://medium.com/@sanyamdubey28/working-with-json-and-struct-tags-in-go-0e6a7c4fc6b0
[5] D. Kashyap, "Interfaces and Embedding in Golang (Go)," dev.to, 2024. [Online]. Available: https://dev.to/diwakarkashyap/interfaces-and-embedding-in-golang-go-2em4
Knowledge check · Question 1 of 5
What is struct embedding in Go?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!