Skip to main content

Command Palette

Search for a command to run...

Learning Go: Iteration and Performance Testing with TDD

Published
5 min read
Learning Go: Iteration and Performance Testing with TDD

Introduction

Coming from JavaScript and Python, I was at first skeptical of only using the for keyword to iterate through data. However, it only took a few attempts before I was pleasantly surprised by its ease of use. The Go for loop is a refreshing change if you're transitioning from another language: it is the only looping construct you'll encounter. Go intentionally omits keywords like while, do, or until.

Why Use Tests First?

Integrating tests into your development workflow—especially using the Test-Driven Development (TDD) cycle—offers several key advantages:

  • Defines Behavior: A well-written test serves as the explicit specification, outlining precisely what the code must achieve.

  • Ensures Stability (Prevents Regressions): By running tests, you build a safety net that immediately flags if recent changes have broken existing functionality.

  • Promotes Good Design: Writing code that is easily testable naturally leads to more modular, decoupled, and reusable components.

To start, let's create a test that specifies our simple requirement: a function that takes a single character and repeats it five times.

package iteration

import "testing"

func TestRepeat(t *testing.T) {
    // 1. Arrange: Setup the expected and actual values
    repeated := Repeat("a")
    expected := "aaaaa"

    // 2. Assert: Check if the function output matches the requirement
    if repeated != expected {
        // The %q verb wraps the string in double quotes for clarity
        t.Errorf("expected %q but got %q", expected, repeated)
    }
}

Iteration

Go is unique in how it handles looping constructs.

The for Keyword

The fundamental looping mechanism you'll ever need in Go is the for keyword. It gracefully handles the requirements typically covered by for, while, and do-until loops in languages like C, Java, or Python.

We'll implement our first passing version of the Repeat function using the three-component for loop, a structure commonly found in C-style programming:

func Repeat(character string) string {
    var repeated string // Declaring a string variable

    // The 'traditional' loop format:
    // 1. Initialization (i := 0)
    // 2. Condition (i < 5)
    // 3. Post-statement (i++)
    for i := 0; i < 5; i++ {
        repeated = repeated + character // String concatenation
    }

    return repeated
}

The Usefulness of Only Using for

This singular approach to iteration reduces language complexity. The for keyword's flexibility allows it to adapt to various control flow needs by omitting components:

The "While" Style: For a loop that runs indefinitely until a condition is met internally: for condition { ... }

The Infinite Loop: Used for continuous background tasks (often paired with a break statement): for { ... }

Examples of Iteration (Refactoring)

For cleaner code and easier maintenance, we should refactor our function by defining the repeat count as a constant and utilizing the compound assignment operator +=.

const repeatCount = 5

func Repeat(character string) string {
    var repeated string
    for i := 0; i < repeatCount; i++ {
        // += is the shorthand operator for 'repeated = repeated + character'
        repeated += character 
    }
    return repeated
}

Note on Variable Declaration: The line var repeated string uses explicit declaration to initialize the variable to its zero value (""). This differs from the shorthand operator :=, which requires both declaration and initial value assignment simultaneously.

Benchmarking for Performance

Once a function is proven correct, benchmarking is the next step to ensure it runs efficiently. Go provides integrated benchmarking tools that mirror the structure of unit tests.

We'll create a benchmark function that repeatedly executes the Repeat function:

func BenchmarkRepeat(b *testing.B) {
    // b.Loop() controls the number of iterations (b.N) the Go framework determines necessary.
    for b.Loop() { 
        Repeat("a")
    }
}

Running the benchmark with go test -bench=. provides our baseline measurement: goos: darwin goarch: amd64 pkg: github.com/quii/learn-go-with-tests/for 10000000 136 ns/op PASS

Our function currently takes an average of 136 nanoseconds per operation (ns/op).

The Performance Problem with Strings

The naive method using += is inefficient because strings in Go are immutable. Each time we concatenate (+= or +), the following costly operations occur:

  1. A new memory block must be allocated.

  2. The existing string content is copied into this new block.

  3. The new character is appended.

This continuous re-allocation and copying severely degrades performance in iteration-heavy tasks.

The Solution: strings.Builder

To solve this memory allocation overhead, the Go standard library offers the strings.Builder type. It's specifically optimized for efficient string construction by managing a mutable internal buffer, drastically reducing memory copies.

import "strings" // Don't forget to import this!

const repeatCount = 5

func Repeat(character string) string {
    var repeated strings.Builder // Use the Builder type

    for i := 0; i < repeatCount; i++ {
        repeated.WriteString(character) // Use the WriteString method
    }

    return repeated.String() // Retrieve the final string result
}

Running the benchmark again with the -benchmem flag (which tracks memory statistics) shows the dramatic improvement:

goos: darwin goarch: amd64 pkg: github.com/quii/learn-go-with-tests/for 10000000 25.70 ns/op 8 B/op 1 allocs/op PASS

  • Speed Boost: The function is now dramatically faster, at only 25.70 ns/op.

  • Memory Efficiency: The critical metric is 1 allocs/op (one memory allocation per operation), showing the strings.Builder successfully reduced the memory overhead.

Conclusion

Go’s singular for loop provides a simple yet powerful foundation for iteration. By implementing the TDD methodology and leveraging Go's built-in benchmarking, you can write code that is not only correct and readable but also highly performant. For any task involving heavy string concatenation within a loop, remembering to use strings.Builder is essential for avoiding performance pitfalls related to string immutability.