AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 06 — Structs — Go's Composition-First Record Type

06 — Structs — Go's Composition-First Record Type

August 13, 20264 min read
Download as Markdown

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 notation

Fields 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) // promoted

Because 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!