Go, chapter by chapter · Lesson 7 of 62 · about 50 min
Structs & interfaces
Model data with structs, give it behavior with methods, reuse it with embedding, and write code that works with any type through interfaces.
On this page · 9 sections
Structs#
type Point struct {
X, Y int
}
type Order struct {
ID int
Customer string
Items []string
Paid bool
}Creating values
| Form | Result |
|---|---|
var p Point | {0 0}: every field at its zero value |
Point{3, 4} | fields in declaration order (fragile if fields change) |
Point{X: 3} | named fields; missing ones get zero values (preferred) |
&Point{X: 3} | a pointer to a new Point |
new(Point) | a pointer to a zero Point |
Fields are read and written with a dot: p.X = 10. The dot also works through a pointer; Go follows it for you, so ptr.X means (*ptr).X.
Structs are values: assigning one copies all its fields. Two structs are equal with == when all their fields are comparable and equal, which makes small structs like Point great map keys: visited := map[Point]bool{}.
Methods#
A method is a function with a receiver written before its name:
func (p Point) Dist2() int { // value receiver
return p.X*p.X + p.Y*p.Y
}
func (p *Point) Move(dx, dy int) { // pointer receiver
p.X += dx
p.Y += dy
}The difference matters:
| Receiver | Method gets | Can change the original? | Use when |
|---|---|---|---|
(p Point) | a copy | no | small, read-only methods |
(p *Point) | the address | yes | the method mutates, or the struct is large |
You can call a pointer method on an addressable value (pt.Move(1, 1)), and Go takes the address for you. Pick one receiver kind per type and stay consistent; if any method needs a pointer, make them all pointers.
Embedding#
Put a type inside a struct without a field name and its fields and methods are promoted to the outer type:
type Place struct {
Point // embedded
Name string
}
home := Place{Point: Point{3, 4}, Name: "home"}
home.X // 3, promoted from Point
home.Move(1, 1)
home.Dist2() // promoted methodThis is composition: a Place has a Point and gets its behavior for free. It is not inheritance; a Place is not a Point, and you cannot pass a Place where a Point is expected.
Interfaces#
An interface is a set of method signatures:
type Courier interface {
Cost(km float64) int // price in rupiah
}Any type with a matching Cost method automatically satisfies Courier. There is no implements keyword. Code that accepts a Courier works with every such type, including ones written later in other packages:
func cheapest(km float64, first Courier, rest ...Courier) Courier {
best := first
for _, c := range rest {
if c.Cost(km) < best.Cost(km) {
best = c
}
}
return best
}An interface value holds two things: the concrete value and its type. When you call c.Cost(12), Go looks up the method on whatever type is inside. That is called dynamic dispatch.
Getting the concrete type back
m, ok := c.(Motor) // type assertion: ok is false if c is not a Motor
switch v := c.(type) { // type switch
case Motor:
fmt.Println("motor at", v.PerKm)
case Van:
fmt.Println("van with base", v.Base)
}The empty interface any (an alias for interface{}) has no methods, so every type satisfies it. Use it sparingly; it throws away type safety.
Interfaces you will meet constantly
| Interface | Method(s) | Used by |
|---|---|---|
fmt.Stringer | String() string | fmt.Println prints your type nicely |
error | Error() string | every error value |
io.Reader / io.Writer | Read / Write | files, network, buffers |
sort.Interface | Len, Less, Swap | sort.Sort |
heap.Interface | sort's three plus Push, Pop | priority queues (Module 3) |
More examples#
The book's shapes: interfaces as fields
The book's central example builds Circle and Rectangle, gives them area and perimeter methods, and then defines a Shape interface. Its key insight: a struct can hold interfaces too. MultiShape contains a slice of Shape values and implements Shape itself, so a group of shapes can be used anywhere a single shape can.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ X, Y, R float64 }
type Rectangle struct{ X1, Y1, X2, Y2 float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func (r Rectangle) Area() float64 {
return math.Abs(r.X2-r.X1) * math.Abs(r.Y2-r.Y1)
}
func (r Rectangle) Perimeter() float64 {
return 2 * (math.Abs(r.X2-r.X1) + math.Abs(r.Y2-r.Y1))
}
// MultiShape is a Shape made of Shapes.
type MultiShape struct{ Shapes []Shape }
func (m MultiShape) Area() float64 {
total := 0.0
for _, s := range m.Shapes {
total += s.Area()
}
return total
}
func (m MultiShape) Perimeter() float64 {
total := 0.0
for _, s := range m.Shapes {
total += s.Perimeter()
}
return total
}
func (c Circle) String() string { return fmt.Sprintf("Circle(r=%g)", c.R) } // fmt.Stringer
// Compile-time checks: the build fails if a type stops satisfying Shape.
var (
_ Shape = Circle{}
_ Shape = Rectangle{}
_ Shape = MultiShape{}
)
func totalArea(shapes ...Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
c := Circle{0, 0, 5}
r := Rectangle{0, 0, 10, 10}
fmt.Printf("circle %.2f, rectangle %.2f, both %.2f\n", c.Area(), r.Area(), totalArea(c, r))
group := MultiShape{Shapes: []Shape{c, r, MultiShape{Shapes: []Shape{Circle{R: 1}}}}}
fmt.Printf("nested group: area %.2f, perimeter %.2f\n", group.Area(), group.Perimeter())
fmt.Println(c) // uses String()
}circle 78.54, rectangle 100.00, both 178.54
nested group: area 181.68, perimeter 77.70
Circle(r=5)Embedding: the book's Person and Android
Embedding a type promotes its fields and methods. The book's Android is a Person in the sense that it can Talk() directly, which is different from having a person as a named field.
package main
import "fmt"
type Person struct{ Name string }
func (p *Person) Talk() { fmt.Println("Hi, my name is", p.Name) }
type Android struct {
Person // embedded: Android gets Name and Talk
Model string
}
type Robot struct {
Owner Person // a named field: no promotion
}
func main() {
a := &Android{Person: Person{Name: "R2"}, Model: "astromech"}
a.Talk() // promoted method
a.Person.Talk() // still reachable explicitly
fmt.Println(a.Name, a.Model)
r := Robot{Owner: Person{Name: "Luke"}}
r.Owner.Talk() // must go through the field
}Hi, my name is R2
Hi, my name is R2
R2 astromech
Hi, my name is LukeThe nil interface trap
An interface value holds two things: a type and a value. It is nil only when both are nil. A nil pointer stored in an interface is a non-nil interface, which surprises everyone once:
package main
import (
"errors"
"fmt"
)
type MyErr struct{}
func (*MyErr) Error() string { return "my error" }
func mayFail(fail bool) error {
var p *MyErr // nil pointer
if fail {
p = &MyErr{}
}
return p // BUG: wraps a typed nil; the interface is never nil
}
func mayFailFixed(fail bool) error {
if fail {
return &MyErr{}
}
return nil // return a literal nil interface
}
func main() {
fmt.Println("buggy:", mayFail(false) != nil)
fmt.Println("fixed:", mayFailFixed(false) != nil)
var target *MyErr
err := fmt.Errorf("wrapped: %w", mayFailFixed(true))
fmt.Println(errors.As(err, &target), err)
}buggy: true
fixed: false
true wrapped: my errorThe rule that follows: functions returning error should return a literal nil on success, never a nil pointer of a concrete error type.
The full program#
package main
import (
"fmt"
"strings"
)
type Point struct{ X, Y int }
func (p Point) Dist2() int { return p.X*p.X + p.Y*p.Y }
func (p *Point) Move(dx, dy int) {
p.X += dx
p.Y += dy
}
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }
type Place struct {
Point
Name string
}
type Courier interface {
Cost(km float64) int
}
type Motor struct{ PerKm int }
type Van struct{ Base, PerKm int }
func (m Motor) Cost(km float64) int { return int(km * float64(m.PerKm)) }
func (v Van) Cost(km float64) int { return v.Base + int(km*float64(v.PerKm)) }
var _ Courier = Motor{} // compile-time checks
var _ Courier = Van{}
func cheapest(km float64, first Courier, rest ...Courier) Courier {
best := first
for _, c := range rest {
if c.Cost(km) < best.Cost(km) {
best = c
}
}
return best
}
func describe(v any) string {
switch x := v.(type) {
case int:
return "int " + fmt.Sprint(x)
case fmt.Stringer:
return "stringer " + x.String()
default:
return "other " + strings.ToLower(fmt.Sprintf("%T", x))
}
}
func main() {
a := Point{X: 3, Y: 4}
b := a // copy
b.Move(1, 1)
fmt.Println(a, b, a.Dist2(), a == Point{3, 4})
home := Place{Point: Point{1, 2}, Name: "home"}
home.Move(2, 2)
fmt.Println(home.Name, home.X, home.Dist2())
for _, km := range []float64{3, 12} {
c := cheapest(km, Motor{PerKm: 2500}, Van{Base: 10000, PerKm: 1500})
if m, ok := c.(Motor); ok {
fmt.Println(km, "km: motor", m.Cost(km))
} else {
fmt.Println(km, "km: van", c.Cost(km))
}
}
fmt.Println(describe(7), describe(a), describe(true))
}(3,4) (4,5) 25 true
home 3 25
3 km: motor 7500
12 km: van 28000
int 7 stringer (3,4) other boolNotice fmt.Println(a) printed (3,4): because Point has a String() method, it satisfies fmt.Stringer, and fmt uses it automatically. For 3 km the motor costs 7,500 against the van's 14,500; at 12 km the van's per-km rate wins.
Summary#
- Structs group fields; create them with named fields, and use
&T{}when you need a pointer. - Methods attach behavior. Pointer receivers mutate; value receivers work on copies.
- Embedding promotes an inner type's fields and methods: composition, not inheritance.
- Interfaces are satisfied implicitly by having the methods; keep them small.
- Type assertions and type switches recover the concrete type from an interface.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.A method with a value receiver changes a field. What happens to the caller's struct?
Q2.How does a type declare that it implements an interface?
Q3.Type
PlaceembedsPoint, which has methodDist2(). Can you callplace.Dist2()?Q4.What does
v, ok := x.(Motor)do when x holds a Van?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
Show hint
A struct with a
[4]intof remaining slots indexed by car type, and a pointer-receiver method that decrements.Show hint
Keep a
Pointstruct and aMovemethod; return whether it ends at the zero value.- Shapes without the book's shapesbuild
Define an interface
PricedwithPrice() int, three types implementing it, and a function returning the total of a[]Priced.Show hint
Write
var _ Priced = Coffee{}lines to make the compiler check each type. - Sort with sort.Interfacebuild
Implement
Len,LessandSwapontype ByAge []Personand sort withsort.Sort. Then do the same withslices.SortFuncand compare.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.