Programming

Method values remember the receiver

Go 1.1 permits a useful expression I kept trying to write before it existed: a method value.

type counter struct{ n int }

func (c *counter) add(n int) { c.n += n }

c := &counter{}
add := c.add
add(3)
add(4)
fmt.Println(c.n) // 7

The expression c.add produces a function value with c bound as its receiver. Its type is func(int). This is handy when an API wants a callback and the work naturally belongs to an object.

It differs from a method expression:

add := (*counter).add
add(c, 3)

Here the receiver is explicit, so the function type is func(*counter, int). The distinction is captured receiver versus receiver as the first argument.

The receiver is evaluated when the method value is created. For a pointer receiver, the saved pointer continues to identify the same object. For a value receiver, the receiver value is copied, just as it is for an ordinary value-receiver call. This can matter for large values and for anyone expecting later replacement of a variable to retarget an already-created callback.

Method values may require storage for the bound receiver, so I would measure before putting their creation in a very hot loop. Their main virtue is not speed. It is that callback code can say server.handle instead of wrapping the same call in an otherwise pointless closure.

Counting allocations in Go 1.1

I had two versions of a formatter with nearly identical benchmark times. One looked cleaner, so naturally I distrusted it.

Go 1.1’s benchmark allocation reporting made the difference visible. Benchmarks can call b.ReportAllocs(), or the test command can request memory statistics for benchmarks. The output includes allocations per operation and bytes allocated per operation alongside timing.

func BenchmarkLabel(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_ = label(42)
	}
}

My first formatter repeatedly concatenated strings:

func join(parts []string) string {
	var out string
	for _, part := range parts {
		out += part
	}
	return out
}

As out grows, concatenation creates new strings and copies prior contents. A bytes.Buffer version reduced allocation for larger inputs. For two tiny pieces, however, the direct expression remained clearer and entirely adequate. Allocation counts are measurements, not commandments delivered from a heap-shaped mountain.

The numbers need careful interpretation. “Bytes per operation” is an average over repeated benchmark iterations. Setup performed inside the timed loop counts against the operation, including test data accidentally rebuilt every time. Use b.ResetTimer() after setup when the setup is not part of what should be measured.

The benchmark result also depends on escape analysis and compiler decisions. A value that remains on the stack does not appear as a heap allocation merely because source code takes its address. Conversely, converting values to interfaces or retaining pointers can move data to the heap in ways that are not obvious from syntax. Compiler diagnostics can explain candidates after the benchmark shows there is a problem.

Allocation count and allocated bytes tell different stories. Many tiny objects increase allocator and collector work. One large object can dominate memory traffic while counting as a single allocation. Live heap is different again: an object retained for minutes affects collection even if it was allocated only once.

I now include allocation reports for benchmarks on hot parsing, formatting, and request paths. I first verify the output cannot be optimized away, keep setup honest, and compare behaviour as well as speed. Reducing allocations often improves performance, but an obscure zero-allocation function can still be a bad bargain.

I keep the benchmark input representative as well. A formatter tested only with an empty string can truthfully report zero allocations while answering a question no caller was likely to ask.

In this case the cleaner formatter also allocated less after a small adjustment. I was forced to accept the pleasant result. Software occasionally lacks respect for a good suspicion.

Reading lines with bufio.Scanner

Every small text-processing program I write begins with the same lie: “I only need to read a file line by line.” Five minutes later I am handling buffering, end-of-file, and the final line without a newline.

Go 1.1 added bufio.Scanner, which packages that loop neatly:

scanner := bufio.NewScanner(r)
for scanner.Scan() {
	line := scanner.Text()
	fmt.Println(line)
}
if err := scanner.Err(); err != nil {
	return err
}

By default Scanner splits input into lines and removes the line ending. Scan advances to the next token. Text returns that token as a string; Bytes returns the token as bytes. After the loop, Err distinguishes a clean end-of-input from a read or tokenization error.

That final check is easy to omit because the loop looks complete without it. It is not. An I/O error halfway through a configuration file should not quietly turn the remaining configuration into an unusually convincing empty section.

Scanner is more general than lines. Split accepts a split function, and the package supplies functions for words, bytes, and runes. A split function receives available data and whether the underlying reader has reached its end. It reports how far to advance, which token to return, and any error. This supports simple lexical work without writing the buffering loop again.

There is an important limit: Scanner uses a maximum token size. It is intended for reasonably sized tokens, not arbitrary enormous records. A log format that permits a single multi-megabyte line may exceed the limit and stop scanning with an error. For such input, bufio.Reader and an explicit loop provide more control. Choosing Scanner means accepting its token-oriented contract, not merely admiring its shorter spelling.

Bytes also deserves care. The returned slice may refer to storage Scanner reuses on the next call to Scan. Process it before advancing, or copy it if it must survive. Text gives a string representation that is safer to retain, with the corresponding conversion and allocation considerations.

I also avoid mixing direct reads from the underlying reader with Scanner. Scanner buffers ahead, so the reader’s apparent position need not correspond to the end of the last token I processed. One owner for the stream is a much less surprising arrangement.

I replaced my hand-written line reader with Scanner and deleted more edge-case code than I added. That is a good trade when its token limits fit the data. When they do not, the lower-level reader remains available. Go’s I/O packages are at their best when the simple case is genuinely simple and the escape hatch is still visible.

Two small Go 1.1 speedups

I rebuilt a little indexing program with Go 1.1 and it became faster without receiving any of my expert optimization, which is generally the safest kind.

The program mostly fills maps and then produces enough temporary objects to keep the garbage collector socially engaged:

for _, word := range words {
	counts[word]++
}

Go 1.1 includes a new map implementation and substantial runtime improvements. Map operations are faster, particularly for common key types, and the garbage collector does less work in several important paths. Programs heavy on either can improve simply by recompiling.

This is not a universal percentage discount. Key sizes, hit rates, map growth, allocation rate, live heap, processor count, and workload shape all matter. My toy index is evidence about my toy index.

The right comparison is to build the same source with both releases, run multiple times on an otherwise quiet machine, and look at distributions rather than choosing the friendliest result. If the program is a service, latency and pauses may matter more than total runtime.

Runtime speedups are welcome because they improve ordinary code without making it strange. I will still provide map size hints when I know them and avoid pointless allocation. A faster collector is not a licence to create garbage professionally.

The race detector found my clever cache

Go 1.1’s race detector found a bug in a cache I had described as “effectively read-only.” Adverbs remain an important source of software defects.

The cache was reduced to this:

var cached map[string]string

func lookup(key string) string {
	if cached == nil {
		cached = loadAll()
	}
	return cached[key]
}

Two handlers could observe cached == nil and assign it concurrently. Worse, one could read the map while another was publishing it. Running tests with go test -race reported the conflicting accesses and the goroutine stacks involved.

The detector instruments memory accesses in the compiled program and tracks synchronization events. When two goroutines access the same location concurrently, at least one access is a write, and no synchronization orders them, it can report a data race. The stack traces are the useful part: they connect an abstract memory-model violation to the two paths I actually wrote.

I fixed the cache with a mutex and a clear initialization path. A channel-based owner goroutine would also have worked, but building a miniature cache server around one assignment would have been enthusiasm rather than design.

The detector only sees executions that happen. A test that never drives two requests through initialization cannot reveal this race. I added a test that starts several goroutines together and exercises the path repeatedly. Even then, a clean run is evidence, not proof that every possible schedule is safe.

Race-enabled binaries are slower and use more memory because instrumentation and bookkeeping are not free. I would use them in tests and representative staging workloads rather than deciding the overhead is a mysterious production regression. The point is to make races observable, not to benchmark the detector against uninstrumented code and appear shocked.

A race report is also not permission to sprinkle locks around the named lines. The conflicting accesses may reveal confused ownership, publication without synchronization, or a larger invariant spanning several fields. Protecting one variable while leaving the invariant split can silence one report and preserve the bug.

Nor should the test ignore a report because the final value happened to be correct. The detected execution violated the memory model. A passing assertion after that is luck arriving late and asking to be mistaken for synchronization.

The race detector does not find deadlocks, logical races built from properly synchronized but wrongly ordered operations, or races in code it never executes. It finds data races, and that is already an excellent service. Mine was real, small, and previously certified by the prestigious It Seems Fine Institute.