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-arelationship. ADogis not anAnimalin Go's type system — you can't pass aDogwhere anAnimalis 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
| Gotcha | Detail |
|---|---|
| Value vs pointer receiver consistency | Mix of value/pointer receivers breaks interface satisfaction — stick to one |
| Embedded is not inheritance | Dog embedding Animal doesn't mean Dog satisfies Animal-typed parameters |
| Struct copy on assignment | b := a copies all fields — modifying b doesn't affect a |
| Pointer receiver on value | Go auto-takes address when calling pointer receiver on addressable value — r.Scale(2) works even if r is not a pointer |
| Unexported fields | Fields starting with lowercase are package-private — json.Unmarshal and other reflection tools can't set them |