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:
- Write the test.
- Watch it fail.
- Write the dumbest code that could work.
- 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
- Structs bundle related data (
Rectangle{Width, Height}
). - Methods add behavior to types (
func (r Rectangle) Area()
). - Interfaces define what a type can do, not how (
type Shape interface
). - Table tests keep your code clean and scalable.
Now go forth and architect some beautiful Go code! 🚀
Next Up: Pointers & Errors