Back to Notes

Go Structs

Overview

A struct is Go's primary way to group related data. It's equivalent to a class without inheritance — you get data fields and methods attached to the type. Go favours composition over inheritance through embedded structs.


Struct Definition

Define a named type with type Name struct { ... }. Fields are accessed with .

type Person struct {
    Name string
    Age  int
    Email string
}

p := Person{Name: "Vatsal", Age: 25, Email: "v@example.com"}
fmt.Println(p.Name)   // Vatsal
p.Age = 26
fmt.Println(p.Age)    // 26

Struct Literals

Three ways to create a struct value. Named fields are always preferred — positional breaks when you add/reorder fields.

type Point struct{ X, Y int }

p1 := Point{X: 1, Y: 2}   // named — preferred
p2 := Point{1, 2}          // positional — fragile, avoid for exported structs
p3 := Point{}              // zero value: X=0, Y=0

ptr := &Point{3, 4}        // pointer to struct, often used with methods

Nested Structs

Structs can contain other structs as fields. Access nested fields by chaining .

type Address struct {
    City    string
    Country string
}

type Employee struct {
    Name    string
    Age     int
    Address Address   // nested struct
}

e := Employee{
    Name: "Alice",
    Age:  30,
    Address: Address{City: "Mumbai", Country: "India"},
}

fmt.Println(e.Address.City)  // Mumbai

Anonymous Structs

A struct defined inline without a type name. Useful for one-off data groupings (test cases, JSON payloads) where creating a named type would be overkill.

car := struct {
    Brand string
    Year  int
}{
    Brand: "Toyota",
    Year:  2023,
}

fmt.Println(car.Brand)  // Toyota

// Common for table-driven tests
tests := []struct {
    input    int
    expected int
}{
    {1, 1},
    {5, 120},
    {10, 3628800},
}

Embedded Structs (Composition)

Go's answer to inheritance. Embed a struct by including its type name without a field name. The embedded struct's fields and methods are promoted — accessible directly on the outer struct.

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal          // embedded — not a named field
    Breed string
}

d := Dog{
    Animal: Animal{Name: "Buddy"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)     // Buddy   — promoted field
fmt.Println(d.Speak())  // Buddy makes a sound — promoted method
fmt.Println(d.Animal.Name)  // also valid — explicit access

Composition, not inheritance: Embedded types promote fields/methods but there is no is-a relationship. A Dog is not an Animal in Go's type system — you can't pass a Dog where an Animal is expected.


Methods

A method is a function with a receiver — it belongs to a type. Two kinds of receivers:

  • Value receiver (r Rect) — operates on a copy, cannot modify the original
  • Pointer receiver (r *Rect) — operates on the original, can modify it
type Rect struct {
    Width, Height float64
}

// Value receiver — read-only, works on a copy
func (r Rect) Area() float64 {
    return r.Width * r.Height
}

func (r Rect) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Pointer receiver — modifies the original
func (r *Rect) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

r := Rect{Width: 5, Height: 10}
fmt.Println(r.Area())       // 50
r.Scale(2)
fmt.Println(r.Area())       // 200

Receiver naming convention

Use the first one or two letters of the type name:

func (r Rect) Area() float64    { ... }   // r for Rect
func (p *Person) SetName(s string) { ... } // p for Person

Rule: If any method has a pointer receiver, all methods should use pointer receivers for consistency (otherwise interfaces won't work correctly).


Pointer to Struct

Go automatically dereferences struct pointers — p.X is shorthand for (*p).X. You rarely need to write (*p).X explicitly.

p := &Point{X: 1, Y: 2}
p.X = 10            // same as (*p).X = 10 — Go handles it
fmt.Println(*p)     // {10 2}

Struct Tags

Tags are metadata attached to fields, read at runtime via reflection. Most commonly used for JSON marshalling and ORM field mapping.

import "encoding/json"

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`  // omit if empty
    Password string `json:"-"`               // always omit
}

u := User{ID: 1, Name: "Vatsal", Email: "v@example.com"}
data, _ := json.Marshal(u)
fmt.Println(string(data))
// {"id":1,"name":"Vatsal","email":"v@example.com"}

Memory Layout

Structs are laid out in a contiguous memory block. The compiler may add padding between fields to satisfy alignment requirements. Group fields by size (largest first) to minimise padding.

// Efficient — 4 bytes total
type Good struct {
    A uint16  // 2 bytes
    B uint8   // 1 byte
    C uint8   // 1 byte
}

// Inefficient — 6 bytes (padding added)
type Bad struct {
    B uint8   // 1 byte + 1 byte padding
    A uint16  // 2 bytes
    C uint8   // 1 byte + 1 byte padding
}

// Check size at runtime
import "reflect"
fmt.Printf("Good: %d bytes\n", reflect.TypeOf(Good{}).Size())  // 4
fmt.Printf("Bad:  %d bytes\n", reflect.TypeOf(Bad{}).Size())   // 6

Empty Struct

struct{} occupies zero bytes of memory. Used as a signal/marker value — most commonly as the value type in a set (map with no meaningful value).

// Set of unique strings
seen := make(map[string]struct{})
seen["alice"] = struct{}{}
seen["bob"]   = struct{}{}

_, exists := seen["alice"]  // true
_, exists  = seen["carol"]  // false

// Channel signal (signal without data)
done := make(chan struct{})
go func() { done <- struct{}{} }()
<-done

Gotchas

GotchaDetail
Value vs pointer receiver consistencyMix of value/pointer receivers breaks interface satisfaction — stick to one
Embedded is not inheritanceDog embedding Animal doesn't mean Dog satisfies Animal-typed parameters
Struct copy on assignmentb := a copies all fields — modifying b doesn't affect a
Pointer receiver on valueGo auto-takes address when calling pointer receiver on addressable value — r.Scale(2) works even if r is not a pointer
Unexported fieldsFields starting with lowercase are package-private — json.Unmarshal and other reflection tools can't set them