Structs & Interfaces
Structs, methods, pointer receivers, and how Go's implicit interfaces enable composition over inheritance.
Structs
Go has no classes — a struct groups related fields together, and methods are attached to it separately:
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Ali", Age: 22}
fmt.Println(u.Name, u.Age) // Ali 22
}
Methods
A method is a function with a receiver — the type it's attached to:
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
}
Pointer receivers
A value receiver (func (r Rectangle) ...) gets a copy — changes don't affect the original. A pointer receiver (func (r *Rectangle) ...) operates on the original struct:
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2) // Go automatically takes &rect here
fmt.Println(rect.Width, rect.Height) // 20 10
}
Rule of thumb: if a method needs to modify the struct, or the struct is large, use a pointer receiver. Be consistent — don't mix value and pointer receivers on the same type without a good reason.
Interfaces
An interface defines a set of method signatures. Crucially, in Go, a type satisfies an interface implicitly — there's no implements keyword:
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
// Rectangle from above already has an Area() float64 method,
// so both Circle and Rectangle automatically satisfy Shape.
func printArea(s Shape) {
fmt.Println("Area:", s.Area())
}
func main() {
printArea(Circle{Radius: 4})
printArea(Rectangle{Width: 10, Height: 5})
}
This is called structural typing — "if it has the right methods, it satisfies the interface," with zero explicit declaration required. It's a major reason Go code tends to be loosely coupled by default.
Composition over inheritance
Go has no class inheritance. Instead, structs embed other structs to reuse fields and methods:
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return a.Name + " is an animal"
}
type Dog struct {
Animal // embedded — Dog "has" all of Animal's fields and methods
Breed string
}
func main() {
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Describe()) // Rex is an animal — promoted from Animal
fmt.Println(d.Name) // Rex — field access is also promoted
}
The empty interface and any
interface{} (aliased as any since Go 1.18) can hold a value of any type — useful for generic-ish code before Go had real generics, and still common in APIs like encoding/json:
func describe(v any) {
fmt.Printf("value=%v type=%T\n", v, v)
}
describe(42) // value=42 type=int
describe("hello") // value=hello type=string
Common mistakes
- Mixing value and pointer receivers inconsistently on the same type, which can cause confusing bugs about whether a mutation "stuck."
- Forgetting that Go interfaces are satisfied implicitly — there's no compile error reminding you to "implement" one; a typo in a method name just silently fails to satisfy the interface.
- Overusing
any/interface{}where a concrete type or Go generics ([T any]) would be safer and clearer.
Interview questions
Q: How does Go achieve polymorphism without class inheritance? Through interfaces (any type with the right method set implicitly satisfies an interface) and struct embedding (composition), rather than a class hierarchy.
Q: When should a method use a pointer receiver instead of a value receiver? When the method needs to mutate the receiver, or when the struct is large enough that copying it on every call would be wasteful. For small, immutable-in-practice structs, value receivers are fine and simpler.
Q: What does it mean that Go interfaces are satisfied "implicitly"? A type doesn't declare which interfaces it implements — the compiler checks structurally, at the point of use, whether the type's method set matches what the interface requires. This decouples interface definitions from concrete types entirely.