> series: learn go from 0 to 100 | article 5 of 19
Go has no classes, no inheritance either, and yet massive systems are written in it. The trick, Go structs for data, methods for behavior, composition for reuse. Step by step.
Structs: data, period
A struct is a collection of named fields (A Tour of Go, structs):
type User struct {
Name string
Email string
Active bool
}
// Literals: positional or named (named is better)
u := User{Name: "Jenn", Email: "jenn@example.dev", Active: true}
// Direct dot access. No getters, no setters.
u.Active = false
Juicy detail: visibility in Go goes by capitalization. A field or function starting with an uppercase letter is exported (public outside the package), lowercase is package private. There’s no public or private: the letter itself says it (A Tour of Go, exported names).
Methods: functions with a receiver
A method is a function with a special parameter up front, the receiver (A Tour of Go, methods):
type Rectangle struct {
Width, Height float64
}
// (r Rectangle) is the receiver: it turns
// the function into a method of the type.
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
r := Rectangle{Width: 3, Height: 4}
fmt.Println(r.Area()) // 12
Coming from Python, the receiver is your self with a type and a short name. Coming from Java, it’s this lifted into the signature. The kicker: you can define methods on ANY type you own, not just structs. That type Level int from article 2 can have methods.
Value receiver or pointer receiver
The most important decision in this article (A Tour of Go, pointer receivers):
// Value receiver: works on a COPY.
func (r Rectangle) Double() {
r.Width *= 2 // lost on return
}
// Pointer receiver: mutates the original.
func (r *Rectangle) DoubleForReal() {
r.Width *= 2
}
r.DoubleForReal() // Go takes (&r) for you
Rule of thumb for Go structs: if the method mutates the receiver or the struct is big, pointer. Read-only and small, value. And for consistency: if one method of a type uses a pointer, they all should. Pointers get their own full article (number 10).
Composition: embedding
Go has no inheritance and says so with zero complexes (FAQ, inheritance). What it offers is embedding: placing one type inside another without a field name, promoting its methods and fields to the container (Effective Go, embedding):
type Engine struct {
HP int
}
func (e Engine) Start() string {
return "vroom"
}
type Car struct {
Engine // embedded: no field name
Brand string
}
c := Car{Engine: Engine{HP: 90}, Brand: "Retro"}
fmt.Println(c.Start()) // promoted method
fmt.Println(c.HP) // promoted field
This is not inheritance in a trench coat: a Car is not an Engine, it HAS one and delegates to it. No fragile hierarchies, no diamond of death, and the coupling sits visibly in the struct.
Whether Go is object oriented has an official answer: yes and no (FAQ).

The constructor that isn’t one
No classes means no constructors. The convention: a NewType function that validates and returns a pointer (Effective Go, composite literals):
// Idiomatic constructor: a New function that validates.
func NewUser(name, email string) (*User, error) {
if email == "" {
return nil, errors.New("email is required")
}
return &User{Name: name, Email: email, Active: true}, nil
}
Weekly challenge: put Go structs to work
- Model Rectangle and Circle with Area() and Perimeter() methods. Keep them: we’ll reuse them in article 6.
- Create an Account type with a balance and Deposit / Withdraw methods. Choose value or pointer receiver and justify it in a comment.
- Bonus: embed a Logger struct with a Log(msg string) method inside Account and call account.Log(…) directly.
Go structs: level complete.
Next article: interfaces, or how Go does duck typing with compile-time checking.
> exit