Day 09: Interfaces
Core Concept: Implicit Satisfaction
In Go, a type satisfies an interface simply by implementing all its methods — no implements keyword, no explicit declaration. This is called duck typing or structural typing.
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
// Circle satisfies Shape automatically
var s Shape = Circle{Radius: 5}
fmt.Printf("Area: %.2f\n", s.Area())
Interface Composition
Interfaces can embed other interfaces:
type ReadWriter interface {
io.Reader
io.Writer
}
The Empty Interface: any
any (alias for interface{}) accepts a value of any type. Use it sparingly — it bypasses type safety.
func Print(v any) { fmt.Printf("%v (%T)\n", v, v) }
Type Assertions
Extract the underlying concrete type from an interface:
var s Shape = Circle{Radius: 3}
c, ok := s.(Circle) // safe — ok is false if wrong type
if ok { fmt.Println(c.Radius) }
c2 := s.(Circle) // panics if s is not a Circle
Type Switches
Dispatch on multiple concrete types:
func describe(s Shape) string {
switch v := s.(type) {
case Circle: return fmt.Sprintf("Circle r=%.1f", v.Radius)
case Rectangle: return fmt.Sprintf("Rect %gx%g", v.Width, v.Height)
default: return "unknown shape"
}
}
Common Standard-Library Interfaces
| Interface | Methods | Use |
|---|---|---|
fmt.Stringer |
String() string |
Custom fmt.Println output |
error |
Error() string |
Error values |
io.Reader |
Read([]byte) (int, error) |
Anything readable |
io.Writer |
Write([]byte) (int, error) |
Anything writable |
sort.Interface |
Len, Less, Swap |
Custom sort |
Labs
Lab 1: Define Shape and Three Concrete Types
What you’ll practise: declaring an interface and satisfying it implicitly with multiple concrete types.
Task:
Define a Shape interface with Area() float64 and Perimeter() float64. Implement Circle, Rectangle, and Triangle structs that satisfy it, plus fmt.Stringer on each.
Steps:
- Declare
type Shape interface { Area() float64; Perimeter() float64 } - Define
Circle{Radius float64},Rectangle{Width, Height float64},Triangle{A, B, C float64}(sides) - Implement
Area()andPerimeter()for each — usemath.Piand Heron’s formula for Triangle - Implement
String() stringon each sofmt.Printlnproduces a readable description - In
main, assign each to aShapevariable and printArea()andPerimeter()
import "math"
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle) String() string { return fmt.Sprintf("Circle(r=%.2f)", c.Radius) }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string { return fmt.Sprintf("Rect(%.2fx%.2f)", r.Width, r.Height) }
Expected output:
Circle(r=5.00) area=78.54 perimeter=31.42
Rect(3.00x4.00) area=12.00 perimeter=14.00
Checkpoint: All three types compile; assigning any of them to a Shape variable works without a cast.
Lab 2: TotalArea and LargestShape
What you’ll practise: writing functions that accept interface slices, demonstrating runtime polymorphism.
Task:
Write TotalArea(shapes []Shape) float64 and LargestShape(shapes []Shape) Shape using only the Shape interface — no type assertions.
Steps:
- Implement
TotalAreaby ranging over the slice and summings.Area() - Implement
LargestShapeby tracking the shape with the maximum area; returnnilfor an empty slice - Create a mixed
[]Shape{Circle{5}, Rectangle{3,4}, Triangle{3,4,5}}in main - Print the total area and the string representation of the largest shape
func TotalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func LargestShape(shapes []Shape) Shape {
if len(shapes) == 0 {
return nil
}
best := shapes[0]
for _, s := range shapes[1:] {
if s.Area() > best.Area() {
best = s
}
}
return best
}
Expected output:
Total area: 96.54
Largest: Circle(r=5.00)
Checkpoint: LargestShape returns the correct shape for any ordering of the input slice.
Lab 3: Type Assertions — Extract Only Circles
What you’ll practise: the comma-ok form of type assertions to safely extract concrete types from an interface slice.
Task:
Given a []Shape containing mixed types, use type assertion to collect only the Circle values into a []Circle. Handle the non-circle case gracefully.
Steps:
- Create
shapes := []Shape{Circle{1}, Rectangle{2,3}, Circle{5}, Triangle{3,4,5}} - Iterate and attempt
c, ok := s.(Circle)— collect only whenokis true - Print each extracted circle’s radius
- Also demonstrate the panicking form
s.(Circle)on a known Circle, then show what happens with the wrong type using a recover
func extractCircles(shapes []Shape) []Circle {
var circles []Circle
for _, s := range shapes {
if c, ok := s.(Circle); ok {
circles = append(circles, c)
}
}
return circles
}
Expected output:
Circles: [Circle(r=1.00) Circle(r=5.00)]
Non-circles skipped: 2
Checkpoint: The function never panics regardless of the input slice composition.
Lab 4: Type Switch — describe
What you’ll practise: type switches for dispatching on concrete types without explicit if-chains.
Task:
Write describe(s Shape) string that returns a human-readable sentence about the shape using a type switch.
Steps:
- Implement
describewithswitch v := s.(type) - Handle
Circle,Rectangle,Trianglewith shape-specific sentences - Add a
defaultcase returning “unknown shape” - Test with all three types and an unknown type wrapped in an interface
func describe(s Shape) string {
switch v := s.(type) {
case Circle:
return fmt.Sprintf("a circle with radius %.2f and area %.2f", v.Radius, v.Area())
case Rectangle:
return fmt.Sprintf("a %.2f by %.2f rectangle", v.Width, v.Height)
case Triangle:
return fmt.Sprintf("a triangle with sides %.2f, %.2f, %.2f", v.A, v.B, v.C)
default:
return fmt.Sprintf("unknown shape: %T", v)
}
}
Expected output:
a circle with radius 5.00 and area 78.54
a 3.00 by 4.00 rectangle
a triangle with sides 3.00, 4.00, 5.00
Checkpoint: describe handles all three types and the default case without any type assertions outside the switch.
Lab 5: Interface Composition — LabelledShape
What you’ll practise: embedding interfaces to compose richer interface types.
Task:
Define a Stringer interface (String() string), a Sizer interface (Size() float64), and a composed LabelledShape interface that embeds both plus Shape. Then write a function that accepts only LabelledShape.
Steps:
- Define
type Stringer interface { String() string }andtype Sizer interface { Size() float64 } - Define
type LabelledShape interface { Shape; Stringer; Sizer } - Add
Size() float64toCircle(returning diameter) andRectangle(returning diagonal) - Write
PrintLabelled(ls LabelledShape)that prints the label, size, area, and perimeter - Observe that
Triangledoes not implementLabelledShape(compile error if you try)
type Stringer interface{ String() string }
type Sizer interface{ Size() float64 }
type LabelledShape interface {
Shape
Stringer
Sizer
}
func PrintLabelled(ls LabelledShape) {
fmt.Printf("%s size=%.2f area=%.2f perim=%.2f\n",
ls.String(), ls.Size(), ls.Area(), ls.Perimeter())
}
Expected output:
Circle(r=5.00) size=10.00 area=78.54 perim=31.42
Rect(3.00x4.00) size=5.00 area=12.00 perim=14.00
Checkpoint: PrintLabelled compiles only when called with a type that satisfies all three embedded interfaces.
Lab 6: The Empty Interface — PrintAll
What you’ll practise: using any to accept heterogeneous values, and understanding why any loses type safety.
Task:
Write PrintAll(values []any) that prints each value using fmt.Sprint. Then demonstrate the loss of type safety by mixing shapes, integers, and strings in the same slice.
Steps:
- Implement
PrintAllusing a range loop andfmt.Sprintf("%v (%T)", v, v) - Call it with
[]any{Circle{3}, 42, "hello", true} - Try calling
.Area()on an element extracted from the[]any— observe the compile error (you must assert first) - Discuss in a comment why
[]Shapeis safer than[]anyfor shape collections
func PrintAll(values []any) {
for _, v := range values {
fmt.Printf("%v (type: %T)\n", v, v)
}
}
func main() {
PrintAll([]any{Circle{Radius: 3}, 42, "hello", true})
// To call .Area() you must assert — any loses the interface:
var v any = Circle{Radius: 3}
if s, ok := v.(Shape); ok {
fmt.Println("area:", s.Area())
}
}
Expected output:
Circle(r=3.00) (type: main.Circle)
42 (type: int)
hello (type: string)
true (type: bool)
area: 28.27
Checkpoint: You can explain in a comment why []any is appropriate here but []Shape would be better for a collection of shapes.
Final Lab (Project): Shape Library
What you’ll practise: combining interface design, type assertions, type switches, interface composition, and the empty interface into a coherent shape library.
Task:
Build a complete shape library that brings together all six labs: define the Shape interface, implement three concrete types, write TotalArea and LargestShape, add a type switch describe function, compose a LabelledShape interface, and demonstrate any.
Steps:
- Define
Shapeinterface withArea() float64andPerimeter() float64 - Implement
Circle,Rectangle,Triangleconcrete types withfmt.Stringer - Write
TotalArea(shapes []Shape) float64andLargestShape(shapes []Shape) Shape - Write
describe(s Shape) stringusing a type switch - Create a mixed
[]Shapeand print total area, largest shape, and descriptions
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
Triangle{A: 3, B: 4, C: 5},
}
fmt.Printf("Total area: %.2f\n", TotalArea(shapes))
fmt.Printf("Largest: %s\n", LargestShape(shapes))
for _, s := range shapes {
fmt.Println(describe(s))
}
Expected output:
Total area: 96.54
Largest: Circle(r=5.00)
a circle with radius 5.00 and area 78.54
a 3.00 by 4.00 rectangle
a triangle with sides 3.00, 4.00, 5.00
Checkpoint: go test ./... passes; go vet ./... is clean; each concrete type satisfies Shape with no explicit declaration.
Extension ideas: add a Scale(factor float64) Shape method; implement json.Marshaler.
Official Documentation
fmt— Stringer interface, Printf, Sprintfio— Reader and Writer interfacessort— sort.Interface for custom sortingmath—math.Piand other constants- Language Spec: Interface types — interface declarations
- Language Spec: Type assertions — safe type extraction
- Language Spec: Type switches — dispatch on concrete type
- Effective Go: Interfaces — interface design principles
- Go Tour: Interfaces — interactive interfaces tour