4 min read

Structs, Methods & Interfaces in Go with Tests

Table of Contents

Hey there! 👋 Let’s break down Go’s structs, methods, and interfaces in a way that won’t make your brain hurt.

Think of this as a friendly chat over coffee rather than a lecture.

Let’s Talk Shapes (Literally)

Imagine you’re building a geometry app.

First task: calculate the perimeter of a rectangle. Easy, right?

Step 1: Write the Test (Because TDD is Life)

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) // %.2f = "show me 2 decimal places, please"
    }
}

Step 2: Watch It Fail (Embrace the Red)

Run the test. It’ll scream:

undefined: Perimeter

Step 3: Write Just Enough Code to Fail Better

func Perimeter(width float64, height float64) float64 {
    return 0 // "I'm a minimalist function."
}

Now the test says:

got 0.00 want 40.00

Step 4: Make It Pass (Finally, Some Logic!)

func Perimeter(width, height float64) float64 {
    return 2 * (width + height) // Math! *airhorn noise*
}

🎉 Test passes! High five yourself.

Now, Let’s Add Area

Same drill:

  1. Write the test.
  2. Watch it fail.
  3. Write the dumbest code that could work.
  4. Fix it properly.

You’ll end up with:

func Area(width, height float64) float64 {
    return width * height
}

But wait… what if someone tries to calculate the area of a triangle with this?

Chaos!

Enter: Structs (Your Data’s New Best Friend)

Instead of loose width and height, let’s bundle them into a Rectangle type:

type Rectangle struct {
    Width  float64
    Height float64
}

Now our functions look cleaner:

func Perimeter(rect Rectangle) float64 {
    return 2 * (rect.Width + rect.Height)
}

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

Circles Hate Being Left Out

What if we want Area() for circles too?

Step 1: Test It

t.Run("circles", func(t *testing.T) {
    circle := Circle{10} // Radius: 10
    got := circle.Area()
    want := 314.1592653589793 // πr², baby!

    if got != want {
        t.Errorf("got %g want %g", got, want) // %g = "give me all the precision"
    }
})

Step 2: Define the Circle

type Circle struct {
    Radius float64
}

Step 3: Add a Method

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

Wait, what’s (c Circle)? That’s a method receiver. It means:

“Hey, this function is a method of Circle. Call it like myCircle.Area().”

The Magic of Interfaces

Now we have:

  • Rectangle.Area()
  • Circle.Area()

But what if we want a single function that works with any shape? Enter interfaces:

type Shape interface {
    Area() float64 // "If you have an Area() method, you're a Shape in my book."
}

Now we can write a helper:

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

    t.Run("rectangle", func(t *testing.T) {
        rectangle := Rectangle{12, 6}
        checkArea(t, rectangle, 72.0)
    })

    t.Run("circle", func(t *testing.T) {
        circle := Circle{10}
        checkArea(t, circle, 314.1592653589793)
    })
}

Table-Driven Tests (Because DRY is Good)

Instead of repeating ourselves, let’s use a table:

func TestArea(t *testing.T) {
    areaTests := []struct {
        name    string
        shape   Shape
        hasArea float64
    }{
        {name: "Rectangle", shape: Rectangle{12, 6}, hasArea: 72.0},
        {name: "Circle", shape: Circle{10}, hasArea: 314.1592653589793},
        {name: "Triangle", shape: Triangle{12, 6}, hasArea: 36.0}, // Wait, what’s a Triangle?
    }

    for _, tt := range areaTests {
        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)
            }
        })
    }
}

Key Takeaways

  1. Structs bundle related data (Rectangle{Width, Height}).
  2. Methods add behavior to types (func (r Rectangle) Area()).
  3. Interfaces define what a type can do, not how (type Shape interface).
  4. Table tests keep your code clean and scalable.

Now go forth and architect some beautiful Go code! 🚀

Next Up: Pointers & Errors

Support my open source work 🚀

Let's Connect

I'm open to discussing new opportunities. Feel free to connect with me or send me an email at [email protected]