Structs & methods
Structs hold data. Methods attach to types. There are no classes, and yet Go's type system is more flexible than most class-based ones, because methods can attach to any named type (even type Celsius float64) and composition (struct embedding) replaces inheritance.
1 · The intuition
Go has no class keyword. A type is either a primitive (int, string), a composite (struct, map, slice), or a named type (type Foo X). Methods attach to any named type. Inheritance doesn't exist: you compose by embedding one type into another and inherit its method set.
NewFoo(...)) creates instances. Methods are functions with a "receiver" argument that lives between func and the function name.2 · Try it: struct + method
package main
import "fmt"
// Type definition — capitalised fields are exported.
type User struct {
Name string
Age int
email string // lowercase = package-private
}
// Constructor — idiomatic name is "NewUser" returning *User
func NewUser(name string, age int) *User {
return &User{Name: name, Age: age}
}
// Method on User (value receiver). u is a copy.
func (u User) Greet() string {
return "Hello, " + u.Name + "!"
}
// Method on *User (pointer receiver). u points to the original.
func (u *User) SetEmail(e string) {
u.email = e
}
func main() {
alice := NewUser("Alice", 30)
fmt.Println(alice.Greet())
alice.SetEmail("[email protected]")
fmt.Printf("%+v
", alice)
}3 · Value vs pointer receivers
Two ways to attach a method: func (u User) F() or func (u *User) F(). The first gets a copy; the second gets a pointer. The choice matters for mutation and for sharing.
package main
import "fmt"
type Counter struct{ n int }
// Value receiver — works on a copy
func (c Counter) IncrCopy() { c.n++ }
// Pointer receiver — mutates the original
func (c *Counter) Incr() { c.n++ }
func main() {
c := Counter{}
c.IncrCopy()
c.IncrCopy()
fmt.Println(c.n) // 0 — the copy was mutated, not c
c.Incr()
c.Incr()
fmt.Println(c.n) // 2 — pointer receiver actually mutates
}4 · Embedding: composition over inheritance
Embedding lets one struct include another anonymously. The embedded type's fields and methods become "promoted": accessible directly on the outer struct.
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Greet() string { return "Hi, I am " + a.Name }
type Dog struct {
Animal // embedded — anonymous field
Breed string
}
func main() {
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Name) // promoted from Animal
fmt.Println(d.Greet()) // promoted method
fmt.Println(d.Breed)
}Embedding is not inheritance. Dog is not a subtype of Animal. You can't pass a Dog where an Animal is expected unless an interface mediates. But the syntactic affordance (d.Greet()) feels like inheritance and saves a lot of forwarding code.
5 · Methods on non-struct types
You can attach methods to any named type, not just structs. This is the trick behind time.Duration, http.Status, and dozens of standard library types.
package main
import "fmt"
type Celsius float64
func (c Celsius) Fahrenheit() float64 {
return float64(c)*9/5 + 32
}
type ID int
func (i ID) String() string {
return fmt.Sprintf("ID-%06d", i)
}
func main() {
body := Celsius(37)
fmt.Println(body.Fahrenheit()) // 98.6
user := ID(42)
fmt.Println(user) // ID-000042 (Println calls String())
}6 · Struct tags: the metadata layer
Tags are string literals after struct fields. They're metadata read at runtime via reflection. The standard use is JSON, XML, ORM mapping.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
SecretKey string `json:"-"`
}
func main() {
u := User{Name: "Alice", Age: 30, SecretKey: "shhh"}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}json:"name": rename the field. json:"age,omitempty": omit if zero. json:"-": never serialize. The encoding/json package reads these via reflection.7 · From the wild
From Docker's container engine: classic struct + methods + embedding.
type Container struct {
StreamConfig *stream.Config
State *State
Root string
BaseFS containerfs.ContainerFS
RWLayer layer.RWLayer
ID string
Created time.Time
Managed bool
Path string
Args []string
Config *containertypes.Config
NetworkSettings *network.Settings
HostConfig *containertypes.HostConfig
AppliedVolumesFrom map[string]struct{}
LogPath string
Name string
Driver string
sync.Mutex // <-- embedded mutex, promotes Lock()/Unlock() to Container
}
// State management methods all live on *Container — they mutate.
func (c *Container) IsRunning() bool {
c.Lock()
defer c.Unlock()
return c.State.Running
}8 · Coming from another language?
| If you know… | The bridge |
|---|---|
| Python | struct ≈ @dataclass. No __init__: use a NewFoo function. No inheritance: use embedding. |
| Java / C# | ≈ a class with all-final fields and no constructor. Methods are written outside the type definition. Embedding ≠ extends; it's syntactic composition. |
| JavaScript / TS | ≈ interface + a "class" that implements it. Tags ≈ @serializable decorators. |
| Rust | ≈ struct + impl. The pointer/value receiver distinction is Rust's &self vs self vs &mut self simplified. |
| C++ | ≈ a POD struct with member functions. Embedding ≠ public inheritance; it's more like composition with method-name promotion. |
9 · Common mistakes
- Mixing value and pointer receivers on one type. Pick one and stick to it. The official advice: if any method needs
*T, use*Tfor all. - Returning a struct by value when it has a mutex. Copying a
sync.Mutexproduces an unlocked copy: silent bug.go vetcatches this. - Forgetting that struct equality requires comparable fields. A struct with a slice field can't be compared with
==. - Capitalisation mismatches.
namewon't marshal to JSON unless you tag it;Namewill, even without a tag. - Methods on slice types: fine. Methods on map types: also fine. Embedding via pointer (
*Animal): works, but the embedded value can be nil; check for it.
10 · Exercises (~15 min)
- Build a Stack[int]. Struct with a slice field. Methods:
Push,Pop,Peek,Len. Use pointer receiver for mutators, value receiver forLen. - Add JSON tags. Take your
Stackfrom #1 and add tags so it serializes as{"items":[1,2,3]}. Test withjson.Marshal. - Embed a Logger. Define
type Logger struct{ prefix string }withLogfmethod. Embed it intoService. Callservice.Logf(...)directly. - Method on a named int.
type Status int. Add aString() stringmethod that returns"active","pending", etc. Use it withfmt.Println(Status(1)).
11 · When it clicks
- You write
NewX()by reflex; the constructor returns*X. - You can predict whether a method mutates from its receiver signature alone.
- You reach for embedding when you'd reach for inheritance in another language.
- You add
String() stringto your custom types so they print nicely.