Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
77: Benchmarking Go Code
I was looking at some code in a project recently where we were building up a large configuration string by concatenating a bunch of small pieces together. It looked fine, but the logs showed this specific function was taking way longer than it should. My gut told me it was a memory allocation issue, but "gut feelings" don't get code merged in a professional environment. I needed numbers.
The "Quick and Dirty" timing trap
My first instinct—the lazy engineer instinct—was to just wrap the function call in a time.Since() block and print the result. I tried something like this:
func main() {
start := time.Now()
buildLargeString()
fmt.Printf("Took %v\n", time.Since(start))
}
I ran it three times. First time: 12ms. Second time: 15ms. Third time: 8ms. This is useless. Between background OS processes, CPU throttling, and the Go runtime's garbage collector kicking in at random intervals, a single execution is just noise. To get a real sense of performance, I need to run the code thousands of times and average the result. I could write a for loop to do that manually, but Go has a first-class way of handling this that's much more scientific.
Letting the testing package take the wheel
I moved the logic into a _test.go file. In Go, benchmarks are just functions in your test files that start with the word Benchmark and take a pointer to testing.B. Let's look at the version I wrote for the "naive" concatenation using the + operator:
func BenchmarkNaiveConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 1000; j++ {
s += "append this"
}
}
}
You'll notice the b.N. This is the part that trips people up at first. You don't decide what N is; the Go benchmark tool does. It starts by running the loop once. If that's too fast to measure accurately, it increases N to 100, then 10,000, and so on, until it has a statistically significant sample. It basically keeps running your code until it's confident in the timing.
I ran it using go test -bench=. and got this:
BenchmarkNaiveConcat-12 2000 540230 ns/op
That 540230 ns/op is the key. It took about 540 microseconds per operation. Now I have a baseline. If I change the code and that number goes down, I've actually improved it.
The battle of the string builders
Now I can actually test my hypothesis. I suspect strings.Builder will be faster because it avoids creating a new string in memory every single time we use +. I added a second benchmark to the same file:
func BenchmarkBuilderConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 1000; j++ {
sb.WriteString("append this")
}
_ = sb.String()
}
}
Running them side-by-side gave me a very clear winner:
BenchmarkNaiveConcat-12 2000 540230 ns/op
BenchmarkBuilderConcat-12 45000 23100 ns/op
The strings.Builder version is nearly 23 times faster. In a tight loop or a high-traffic API endpoint, that's the difference between a snappy app and one that spends all its time waiting for the garbage collector to clean up thousands of discarded string fragments.
Looking deeper with memory stats
The time difference is obvious, but I wanted to see why it was happening. I added the -benchmem flag to my command: go test -bench=. -benchmem.
Now the output looks like this:
BenchmarkNaiveConcat-12 2000 540230 ns/op 450120 B/op 999 allocs/op
BenchmarkBuilderConcat-12 45000 23100 ns/op 15200 B/op 12 allocs/op
This is the "aha!" moment. The naive version was performing 999 allocations per operation. Every single + was forcing Go to allocate a new, larger block of memory and copy the old string into it. The Builder version only allocated 12 times. It's not just that the code is "faster"; it's that it's doing drastically less work for the memory manager.
📋 Practical Task
Optimizing the Integer-to-CSV Converter
You have a function that takes a slice of integers and converts them into a single comma-separated string. Currently, it uses fmt.Sprintf inside a loop, which is notoriously slow for this kind of work.
Your goal:
- Create a
_test.gofile. - Write a benchmark for the existing
Sprintfimplementation. - Implement a second version of the function using
strconv.Itoaandstrings.Builder. - Write a benchmark for your new implementation.
- Run both benchmarks with the
-benchmemflag and verify that your new version reduces both thens/opand theallocs/op.
// The slow implementation to start with
func ConvertToCSV(nums []int) string {
var res string
for i, n := range nums {
if i > 0 {
res += ","
}
res += fmt.Sprintf("%d", n)
}
return res
}There are no comments for now.