Go Guide

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
  1. 01Structs
  2. 02Methods
  3. 03Embedding
  4. 04Interfaces
  5. 05More examples
  6. 06The full program
  7. 07Summary
  8. 08Check your understanding
  9. 09Practice in Go

Structs#

Go
type Point struct {
	X, Y int
}
 
type Order struct {
	ID       int
	Customer string
	Items    []string
	Paid     bool
}

Creating values

FormResult
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:

Go
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:

ReceiverMethod getsCan change the original?Use when
(p Point)a copynosmall, read-only methods
(p *Point)the addressyesthe 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:

Go
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 method

This 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:

Go
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:

Go
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

Go
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

InterfaceMethod(s)Used by
fmt.StringerString() stringfmt.Println prints your type nicely
errorError() stringevery error value
io.Reader / io.WriterRead / Writefiles, network, buffers
sort.InterfaceLen, Less, Swapsort.Sort
heap.Interfacesort's three plus Push, Poppriority 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.

shapes.go
Go
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()
}
Output
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.

embedding.go
Go
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
}
Output
Hi, my name is R2
Hi, my name is R2
R2 astromech
Hi, my name is Luke

The 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:

nilinterface.go
Go
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)
}
Output
buggy: true
fixed: false
true wrapped: my error

The 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#

structs.go
Go
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))
}
Output
(3,4) (4,5) 25 true
home 3 25
3 km: motor 7500
12 km: van 28000
int 7 stringer (3,4) other bool

Notice 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.

  1. Q1.A method with a value receiver changes a field. What happens to the caller's struct?

  2. Q2.How does a type declare that it implements an interface?

  3. Q3.Type Place embeds Point, which has method Dist2(). Can you call place.Dist2()?

  4. 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]int of remaining slots indexed by car type, and a pointer-receiver method that decrements.

  • Show hint

    Keep a Point struct and a Move method; return whether it ends at the zero value.

  • Shapes without the book's shapesbuild

    Define an interface Priced with Price() 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, Less and Swap on type ByAge []Person and sort with sort.Sort. Then do the same with slices.SortFunc and compare.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.