---
title: "06 — Structs — Go's Composition-First Record Type"
uid: structs
tags: ["struct-tags", "golang", "json", "embedding", "roadmap:golang", "structs", "fundamentals", "composition"]
excerpt: "A struct is a fixed set of typed fields with no inheritance — you compose complex types by embedding and control serialization with tags. Composition over inheritance, visible everywhere."
date: 2026-08-13T03:28:12+0000
source: https://www.aveshina.my.id/en/blog/structs
---

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](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](https://go.dev/wiki/Well-known-struct-tags)

[3] Go by Example, "Struct Embedding," 2024. [Online]. Available: [https://gobyexample.com/struct-embedding](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](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](https://dev.to/diwakarkashyap/interfaces-and-embedding-in-golang-go-2em4)

```quiz
Q: What is struct embedding in Go?
- A form of inheritance where the outer struct is a subtype of the embedded type
- Including one struct inside another without a field name, promoting its fields and methods onto the outer struct
correct: 1
explain: Embedding is composition. The embedded type's fields (and methods) are promoted to the outer struct, but there is no subtype relationship or polymorphic dispatch — it is flat, not a hierarchy.

Q: What does the struct tag `json:"email,omitempty"` instruct encoding/json to do?
- Always include the email field, erroring if empty
- Output the field as "email" in JSON, and omit it when the field holds its zero value
correct: 1
explain: The first part renames the field in JSON; omitempty tells the encoder to skip the field when it is the zero value for its type.

Q: A Go struct declared with `var u User` and no field values has…
- every field set to its type's zero value
- undefined garbage and must be initialized before use
correct: 0
explain: Go zeroes every field. var u User gives a struct where each field is its zero value (0, "", nil, false as appropriate). Designing structs so this state is useful is idiomatic.

Q: Which tag would you put on a Password field to keep it out of JSON output entirely?
- json:"password,omitempty"
- json:"-"
correct: 1
explain: json:"-" excludes the field from all JSON encoding and decoding. omitempty would still emit it when non-empty, which is wrong for a secret.

Q: Why does Go promote embedded fields rather than support inheritance?
- Because the language designers could not implement inheritance
- Because composition with promoted fields avoids the brittle coupling and deep hierarchies that inheritance creates in large codebases
correct: 1
explain: Go's design philosophy is composition over inheritance. Embedding gives reuse without the fragile-base-class and subtype-polymorphism problems that plague deep class trees.
```
