πŸ—οΈ Chapter 9 of 20

Structs

Structs are Go's primary way to create composite types. Master struct embedding (Go's version of inheritance), struct tags, memory layout and padding, and constructor patterns used in production codebases.

⏱ β€”πŸ“Š Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • Define structs and use all initialisation forms
  • Understand struct memory layout and field padding
  • Use struct embedding to achieve code reuse
  • Apply struct tags for JSON, YAML, and database ORM
  • Implement the constructor pattern for complex structs
  • Know when to pass structs by value vs by pointer
  • Use anonymous structs for one-off data shapes

πŸ“ Struct Definition & Initialisation

go
package main

import "fmt"

// Define a struct type
type Person struct {
    Name    string
    Age     int
    Email   string
    IsAdmin bool
}

// Unexported fields (lowercase) are not accessible outside the package
type Account struct {
    ID       int
    Username string
    balance  float64  // unexported β€” only accessible within package
}

func main() {
    // Form 1: zero value (all fields zeroed)
    var p1 Person
    fmt.Println(p1)  // { 0  false}

    // Form 2: positional literal (fragile β€” breaks if fields are reordered!)
    p2 := Person{"Alice", 30, "alice@example.com", true}

    // Form 3: named fields (preferred β€” readable, order-independent)
    p3 := Person{
        Name:    "Bob",
        Age:     25,
        Email:   "bob@example.com",
        IsAdmin: false,
    }

    // Unset fields get zero values
    p4 := Person{Name: "Charlie"}  // Age=0, Email="", IsAdmin=false

    // Accessing fields
    fmt.Println(p3.Name)     // Bob
    p3.Age = 26              // modify a field
    fmt.Println(p3.Age)      // 26

    // Pointer to struct β€” Go auto-dereferences
    p5 := &Person{Name: "Dave", Age: 40}
    fmt.Println(p5.Name)  // Dave (same as (*p5).Name)
    p5.Age++              // Go auto-dereferences: (*p5).Age++

    // Struct comparison β€” structs are comparable if all fields are comparable
    p6 := Person{Name: "Alice", Age: 30, Email: "alice@example.com", IsAdmin: true}
    fmt.Println(p2 == p6)  // true (all fields equal)

    _ = p1; _ = p4
}
βœ… Output
{ 0 false} Bob 26 Dave true

🧠 Memory Layout & Padding

Fields in a struct are laid out in memory in the order they are declared. The compiler may insert padding bytes between fields to satisfy alignment requirements. Field order can significantly affect struct size.

πŸ“Š Struct Padding β€” Field Order Matters!
❌ Poorly ordered β€” 24 bytes!
struct {
  a bool    // 1 byte
  // 7 bytes PADDING
  b int64   // 8 bytes
  c bool    // 1 byte
  // 7 bytes PADDING
}
Total: 1+7+8+1+7 = 24 bytes!
βœ… Well ordered β€” 10 bytes!
struct {
  b int64   // 8 bytes
  a bool    // 1 byte
  c bool    // 1 byte
  // 0 bytes padding (aligned!)
}
Total: 8+1+1 = 10 bytes (16 w/align)
go
package main

import (
    "fmt"
    "unsafe"
)

// Bad ordering β€” wasted padding
type BadLayout struct {
    A bool    // 1 byte
    B int64   // 8 bytes β€” needs 8-byte alignment
    C bool    // 1 byte
    D int32   // 4 bytes
}

// Good ordering β€” fields sorted largest-to-smallest
type GoodLayout struct {
    B int64   // 8 bytes
    D int32   // 4 bytes
    A bool    // 1 byte
    C bool    // 1 byte
    // 2 bytes padding to make total a multiple of 8
}

func main() {
    fmt.Printf("BadLayout:  %d bytes\n", unsafe.Sizeof(BadLayout{}))   // 32 bytes!
    fmt.Printf("GoodLayout: %d bytes\n", unsafe.Sizeof(GoodLayout{}))  // 16 bytes

    // Check field offsets
    bad := BadLayout{}
    fmt.Printf("Bad: A@%d, B@%d, C@%d, D@%d\n",
        unsafe.Offsetof(bad.A), unsafe.Offsetof(bad.B),
        unsafe.Offsetof(bad.C), unsafe.Offsetof(bad.D))
    // A@0, B@8 (7 bytes padding!), C@16, D@20
}
βœ… Output
BadLayout: 32 bytes GoodLayout: 16 bytes Bad: A@0, B@8, C@16, D@20
πŸ’‘ Production Tip

In most production code, struct padding is a micro-optimisation not worth worrying about. However, for structs that are: (1) used in very large arrays (millions of instances); (2) passed between CPU cores; (3) serialised as binary, field ordering matters. Use fieldalignment from golang.org/x/tools to detect poorly ordered structs.

πŸ”— Struct Embedding β€” Composition over Inheritance

Go doesn't have inheritance. Instead, it uses embedding to achieve code reuse. An embedded struct's fields and methods are promoted to the outer struct.

go
package main

import "fmt"

// Base type
type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return fmt.Sprintf("I am %s, age %d", a.Name, a.Age)
}

func (a Animal) IsAlive() bool { return true }

// Dog embeds Animal β€” gets all its fields and methods
type Dog struct {
    Animal         // embedded (no field name β€” type name is used)
    Breed  string
}

func (d Dog) Speak() string {  // overrides Animal.Speak
    return fmt.Sprintf("Woof! I'm %s the %s", d.Name, d.Breed)
}

// Cat also embeds Animal
type Cat struct {
    Animal
    IndoorOnly bool
}

// Multiple embedding β€” embed several types
type Logger struct {
    Prefix string
}
func (l Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.Prefix, msg) }

type Server struct {
    Logger              // embedded logger
    Host    string
    Port    int
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Rex", Age: 3},
        Breed:  "Labrador",
    }

    // Access promoted fields directly
    fmt.Println(d.Name)     // Rex (promoted from Animal)
    fmt.Println(d.Age)      // 3   (promoted from Animal)
    fmt.Println(d.Breed)    // Labrador

    // Access promoted methods
    fmt.Println(d.IsAlive())  // true (Animal.IsAlive)
    fmt.Println(d.Speak())    // Woof! I'm Rex the Labrador (Dog.Speak overrides)

    // Access Animal.Speak directly (before override)
    fmt.Println(d.Animal.Speak())  // I am Rex, age 3

    // Embedding enables interface satisfaction
    // If Animal implements fmt.Stringer, Dog gets it too

    s := Server{
        Logger: Logger{Prefix: "APP"},
        Host: "localhost",
        Port: 8080,
    }
    s.Log("Server starting")  // [APP] Server starting (promoted from Logger)
    s.Logger.Log("direct")    // access explicitly

    c := Cat{Animal: Animal{Name: "Whiskers", Age: 5}, IndoorOnly: true}
    fmt.Println(c.Speak())  // I am Whiskers, age 5 (Animal.Speak, not overridden)
}
βœ… Output
Rex 3 Labrador true Woof! I'm Rex the Labrador I am Rex, age 3 [APP] Server starting [APP] direct I am Whiskers, age 5

🏷️ Struct Tags

Struct tags are metadata strings attached to fields. They are read at runtime via reflection and used by encoding packages (JSON, XML, YAML) and ORMs.

go
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    // json tag: sets JSON key name, omits if empty, omits if field is zero/nil
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Email    string  `json:"email,omitempty"`  // omit if empty string
    Password string  `json:"-"`                 // NEVER include in JSON
    Score    float64 `json:"score,omitempty"`
    IsAdmin  bool    `json:"is_admin"`

    // Multiple tags for different packages
    // db tag used by sqlx/gorm ORMs
    // validate tag used by go-playground/validator
    // yaml tag used by go-yaml
}

// Example with validation tags
type CreateUserRequest struct {
    Name     string `json:"name"     validate:"required,min=2,max=50"`
    Email    string `json:"email"    validate:"required,email"`
    Age      int    `json:"age"      validate:"required,min=18,max=120"`
    Password string `json:"password" validate:"required,min=8"`
}

func main() {
    user := User{
        ID:       1,
        Name:     "Alice",
        Email:    "alice@example.com",
        Password: "secret123",  // this will NOT appear in JSON output
        Score:    0,            // this will be omitted (omitempty + zero)
        IsAdmin:  true,
    }

    // Marshal to JSON
    data, _ := json.Marshal(user)
    fmt.Println(string(data))
    // {"id":1,"name":"Alice","email":"alice@example.com","is_admin":true}
    // Password is omitted (-), Score is omitted (omitempty + 0)

    // Unmarshal from JSON
    jsonStr := `{"id":2,"name":"Bob","is_admin":false}`
    var user2 User
    json.Unmarshal([]byte(jsonStr), &user2)
    fmt.Println(user2)

    // Pretty-print JSON
    pretty, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println(string(pretty))

    // Reading tags via reflection
    // (see the Reflection chapter for deep dive)
}
βœ… Output
{"id":1,"name":"Alice","email":"alice@example.com","is_admin":true} {2 Bob 0 false} { "id": 1, "name": "Alice", "email": "alice@example.com", "is_admin": true }

🏭 Constructor Patterns

Go doesn't have constructors, but uses factory functions by convention. The naming convention is New[TypeName](...).

go
package main

import (
    "fmt"
    "time"
)

// Simple constructor
type Config struct {
    Host    string
    Port    int
    Timeout time.Duration
    MaxConn int
}

// NewConfig validates and initialises a Config
func NewConfig(host string, port int) (*Config, error) {
    if host == "" {
        return nil, fmt.Errorf("host cannot be empty")
    }
    if port < 1 || port > 65535 {
        return nil, fmt.Errorf("invalid port: %d", port)
    }
    return &Config{
        Host:    host,
        Port:    port,
        Timeout: 30 * time.Second,  // sensible defaults
        MaxConn: 100,
    }, nil
}

// Options pattern β€” for structs with many optional fields
type ServerOption func(*Server2)

type Server2 struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
    tls     bool
}

func WithTimeout(d time.Duration) ServerOption {
    return func(s *Server2) { s.timeout = d }
}

func WithMaxConn(n int) ServerOption {
    return func(s *Server2) { s.maxConn = n }
}

func WithTLS() ServerOption {
    return func(s *Server2) { s.tls = true }
}

func NewServer(host string, port int, opts ...ServerOption) *Server2 {
    s := &Server2{
        host:    host,
        port:    port,
        timeout: 30 * time.Second,  // defaults
        maxConn: 50,
    }
    for _, opt := range opts {
        opt(s)  // apply each option
    }
    return s
}

func main() {
    // Simple constructor
    cfg, err := NewConfig("localhost", 8080)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("Config: %+v\n", cfg)
    }

    // Test validation
    _, err = NewConfig("", 8080)
    fmt.Println("Empty host:", err)

    // Options pattern β€” readable, extensible
    srv := NewServer("localhost", 9090,
        WithTimeout(60*time.Second),
        WithMaxConn(200),
        WithTLS(),
    )
    fmt.Printf("Server: %+v\n", *srv)

    // Anonymous struct β€” for one-off shapes
    point := struct {
        X, Y float64
    }{X: 3.14, Y: 2.71}
    fmt.Printf("Point: %+v\n", point)

    // Slice of anonymous structs β€” common for table-driven tests
    tests := []struct {
        input    int
        expected int
    }{
        {1, 1},
        {5, 120},
        {10, 3628800},
    }
    for _, tc := range tests {
        fmt.Printf("factorial(%d) == %d\n", tc.input, tc.expected)
    }
}
βœ… Output
Config: &{Host:localhost Port:8080 Timeout:30s MaxConn:100} Empty host: host cannot be empty Server: {host:localhost port:9090 timeout:1m0s maxConn:200 tls:true} Point: {X:3.14 Y:2.71}

🎀 Interview Questions

1
How does Go implement "inheritance"? What is struct embedding?
β–Ά
Detailed Answer

Go doesn't have inheritance. Instead it uses composition via embedding: you embed one struct type inside another without giving it a field name. The embedded struct's fields and methods are "promoted" β€” you can access them directly on the outer struct without the intermediate name.

Key difference from inheritance: embedding is delegation, not subtyping. A Dog embedding Animal doesn't make Dog an Animal at the type system level. You can't pass a *Dog to a function expecting *Animal. But you can: define methods on Dog that promote Animal's methods; override specific methods; have Dog satisfy interfaces that Animal satisfies.

Multiple embedding is allowed. If two embedded types have a method with the same name, accessing that method directly is a compile error β€” you must use the explicit path (outer.Inner1.Method()).

2
When should you pass a struct by value vs by pointer?
β–Ά
Detailed Answer

Pass by value when: (1) the struct is small (1–3 machine words β€” 8–24 bytes on 64-bit); (2) you want a copy and won't modify the original; (3) the struct has value semantics (like a mathematical point or a color).

Pass by pointer when: (1) the struct is large (copying would be expensive); (2) you need to modify the struct inside the function; (3) nil is a meaningful value (optional struct); (4) you need to implement mutating methods; (5) the struct contains a sync.Mutex (must not be copied!).

Rule of thumb: if in doubt, use a pointer. The Go team uses pointers for almost all methods on structs in the standard library. For very small immutable structs (like time.Time), value semantics are used to avoid heap allocation.

⚠️ Never copy a sync.Mutex β€” always pass structs containing mutexes by pointer.

πŸ§ͺ Quiz

🧩
Structs
4 questions
1. In Go, how do you embed a struct Animal into struct Dog?
2. What does the tag json:"-" do?
3. Why can field ordering affect struct size?
4. What does json:"name,omitempty" do when Name == ""?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Structs β€” Quick Reference
Definition
type T struct { Field Type }
Named init
T{Field1: v1, Field2: v2}
Zero value
var t T // all fields zeroed
Pointer
&T{Field: val}
Embedding
type Dog struct { Animal }
JSON rename
Field string `json:"field_name"`
JSON omit
Field string `json:"name,omitempty"`
JSON exclude
Password string `json:"-"`
Constructor
func NewT(args) (*T, error) { }
Options
type Option func(*T); NewT(opts...)
Anonymous
struct{ X, Y int }{X:1, Y:2}
Pass large
by pointer *T to avoid copying