Skip to main content
Go: Structs, Methods & Interfaces
  1. Posts/

Go: Structs, Methods & Interfaces

·643 words·4 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Following the Learn Go with Tests guide, exploring Go’s core object-oriented concepts: structs, methods, and interfaces.

Functions
#

Let’s build a geometry calculator using TDD. Starting with a simple rectangle perimeter function:

func TestPerimeter(t *testing.T) {
    got := Perimeter(10.0, 10.0)
    want := 40.0

    if got != want {
        t.Errorf("got %.2f want %.2f", got, want)
    }
}

func Perimeter(width float64, height float64) float64 {
    return 2 * (width + height)
}

Format strings: %.2f prints floating-point numbers with 2 decimal places.

Structs
#

The problem with separate width and height parameters is lack of context. Let’s create a Rectangle type:

type Rectangle struct {
    Width  float64
    Height float64
}

Now our tests become more expressive:

func TestPerimeter(t *testing.T) {
    rectangle := Rectangle{10.0, 10.0}
    got := Perimeter(rectangle)
    want := 40.0

    if got != want {
        t.Errorf("got %.2f want %.2f", got, want)
    }
}

Struct Benefits:

  • Clarity: Rectangle{12, 6} vs Perimeter(12, 6)
  • Type Safety: Can’t accidentally pass wrong parameters
  • Extensibility: Easy to add new fields

Methods vs Functions
#

Instead of Area(rectangle), Go allows us to attach methods to types:

type Rectangle struct {
    Width  float64
    Height float64
} 

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Now we can call:
rectangle := Rectangle{12, 6}
area := rectangle.Area()

Method Syntax: The syntax for declaring methods is almost the same as functions. The only difference is the syntax of the method receiver func (receiverName ReceiverType) MethodName(args).

Defining receiver gives reference to the data, like in other langugages where this is doen implicitly via this.

  • func (r Rectangle) Area() float64 - method declaration
  • r is the receiver (like this in other languages)
  • Convention: receiver name = first letter of type

Multiple Types, Same Method
#

When we want to calculate area for different shapes:

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

Why not function overloading? Go doesn’t allow multiple functions with the same name in the same package. Methods solve this!

Interfaces
#

type Shape interface {
    Area() float64
}

Interface magic: Any type with an Area() float64 method automatically satisfies the Shape interface.

func checkArea(t testing.TB, shape Shape, want float64) {
    t.Helper()
    got := shape.Area()
    if got != want {
        t.Errorf("got %g want %g", got, want)
    }
}

// Works with any shape!
checkArea(t, Rectangle{12, 6}, 72.0)
checkArea(t, Circle{10}, 314.1592653589793)
  • Implicit satisfaction: No need to declare implements
  • Duck typing: “If it walks like a duck and quacks like a duck…”
  • Decoupling: Code depends on behavior, not concrete types

Table-Driven Tests
#

Perfect for testing multiple similar cases:

func TestArea(t *testing.T) {
    areaTests := []struct {
        name    string
        shape   Shape
        hasArea float64
    }{
        {
            name: "Rectangle", 
            shape: Rectangle{Width: 12, Height: 6}, 
            hasArea: 72.0,
        },
        {
            name: "Circle", 
            shape: Circle{Radius: 10}, 
            hasArea: 314.1592653589793,
        },
        {
            name: "Triangle", 
            shape: Triangle{Base: 12, Height: 6}, 
            hasArea: 36.0,
        },
    }

    for _, tt := range areaTests {
        // t.Run creates a subtest with the name from tt.name
        // This makes it easy to identify which test failed: 
        // "TestArea/Rectangle", "TestArea/Circle", etc.
        t.Run(tt.name, func(t *testing.T) {
            got := tt.shape.Area()
            if got != tt.hasArea {
                t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea)
            }
        })
    }
}

Table-Driven Benefits:

  • Scalable: Easy to add new test cases
  • Clear structure: Data separated from logic
  • Better errors: %#v shows struct values in failures
  • Focused testing: go test -run TestArea/Rectangle

Anonymous Structs
#

In our table tests, we used a slice (array) of anonymous structs:

areaTests := []struct {
    name    string
    shape   Shape
    hasArea float64
}{
    {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
    {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
    // ... more test cases
}

The []struct{...} creates a slice where each element is an anonymous struct with name, shape, and hasArea fields.

Use cases:

  • Test data that doesn’t need a named type
  • Grouping related data temporarily
  • Configuration structs