Go

Errors, runes, and delete in the Go 1 preview

I migrated a small text indexer to the Go 1 preview this week. Three changes looked cosmetic in the notes: the predeclared error interface, rune, and the delete operation for maps. Together they made the programme say what it was doing rather more clearly.

The resulting loop is simple:

for _, r := range text {
	counts[r]++
}

if counts['x'] == 0 {
	delete(counts, 'x')
}

r is a rune, which names the Unicode code point represented by the value. It is not a character object and it does not make every byte valid UTF-8. The range loop decodes the string and yields code points; indexing the same string still yields bytes. That distinction is now easier to see in a declaration.

My surprise was how much the word rune changed a code review. An int32 says how wide a number is. A rune says why the number exists. The underlying representation remains integer-sized and comparisons remain ordinary comparisons, but the intent no longer has to live in a nearby comment.

The predeclared error type has a similarly small definition: a value satisfying an interface with an Error() string method can be returned as an error. The important convention is that a nil error means success. Packages can expose useful concrete error values while callers that only need the message and success state depend on the common interface.

Map removal is now direct as well. delete(m, key) removes the entry if present and does nothing dramatic if it is absent. Assigning a zero value is not equivalent; the key would still be present. That matters when using the two-result map lookup to distinguish “missing” from “present with zero value.”

I prefer all three changes because they give common operations common names without introducing a class hierarchy or a collection method catalogue. The caveat is that names do not remove semantic work. Text code must still decide what invalid UTF-8 means, errors still need context, and deleting while iterating deserves a deliberate test rather than confidence by punctuation.

This is still a preview, so I am treating the migration as rehearsal rather than declaring the platform finished. Even so, these modest additions feel like the language settling into its vocabulary instead of shopping for a thesaurus.

Snapshots on the road to Go 1

I rebuilt three small programmes against a recent weekly snapshot today. Two compiled after mechanical fixes. The third exposed an assumption in my own package layout. That ratio feels like progress: the road to Go 1 is becoming less about chasing syntax and more about discovering what code actually depends upon.

I keep a tiny record beside each experiment:

snapshot: release.rNN from the weekly series
compiler: gc
system: linux/amd64
result: build, test, benchmark

rNN here is a placeholder for the precise release printed by the toolchain, not a new version naming proposal. Recording it prevents a December result from quietly becoming a claim about whichever snapshot happens to be current in January.

My surprise was a benchmark that improved after migration even though I had not optimized the programme. The toolchain and runtime are changing alongside the source interfaces. A source diff alone therefore cannot explain every performance change between snapshots. Rebuilding the old source with the old toolchain and the migrated source with the new one changes two variables at once.

For correctness, the weekly process is straightforward. The compiler checks the current language. gofix handles recognized source transformations. Formatting normalizes rewritten code. Tests then check the semantic assumptions neither compiler nor fixer can know. Each layer catches a different class of mistake.

Performance needs a stricter comparison. I preserve the old binary and input, warm up the machine, and run enough iterations to see variation. Linux 3.0, the scheduler, the garbage collector, and compiler output can all affect the number. One fast run is a pleasant event, not a conclusion.

I prefer following named snapshots during this final stretch because each migration stays small and attributable. Waiting for Go 1 might sound safer, but it accumulates every incompatible change into one rather theatrical weekend. The caveat is that preview code remains preview code. I keep important production builds pinned and use copies for migration practice.

The compatibility goal is the larger prize. Once Go 1 arrives, source written to its specification and standard packages should not require this weekly dance merely to remain source-compatible. Implementations can still become faster, schedulers can change, and bugs can be fixed. Stability is a promise about the platform, not the end of engineering.

For now, I will continue writing down the snapshot number. Future me is a notoriously unreliable eyewitness.

gofix is not a test suite

I ran gofix over an older utility and was rewarded with a clean build. Then its first real input produced different output. That was a useful correction to my optimism.

My migration loop is now deliberately boring:

copy or commit the old tree
run gofix
inspect the diff
format and build
run representative inputs

The surprise is how convincing syntactically correct output can be. gofix understands Go source and can perform precise, repeatable rewrites for known language and library changes. It cannot infer every assumption my programme made about the old operation.

That boundary follows from the mechanism. A fix rule recognizes a source pattern and substitutes the new form. It can use syntax and type context; it does not possess the history behind my data or know which edge cases matter to users.

I prefer the tool over hand-editing twenty copies of the same call. Mechanical work should be mechanical. I also prefer one rule or related group at a time, because a small diff tells me which migration changed behaviour.

The caveat is code generation. Fixing generated output while leaving the generator untouched guarantees an encore. Tests and examples may need migration too, and they are often the best record of old semantics.

In my utility the changed behaviour was easy to repair once a failing input exposed it. gofix had done its job. I had briefly promoted it to quality assurance director without discussing the salary.

Watching the Go garbage collector stop

I noticed a small server pausing at intervals under an allocation-heavy test. The average request time looked respectable, but individual requests occasionally took a visible detour. That pattern made the old garbage collector worth examining.

I reduced the trigger to a loop that replaces short-lived buffers:

for i := 0; i < n; i++ {
	b := make([]byte, 32*1024)
	consume(b)
}

The first surprise was that allocation speed and collection latency are separate observations. Allocation can be cheap while creating enough work for periodic collections. An average over the entire run smooths those pauses into a harmless-looking number.

The collector in this runtime is a stop-the-world collector. When a collection begins, Go execution is brought to a safe point and paused while the runtime determines which heap objects are reachable. It starts from roots such as stacks and global data, follows pointers through reachable objects, and eventually reclaims storage that was not found. Application goroutines do not continue doing ordinary work during that collection.

This explains why adding processors does not simply make the pause disappear. It may improve the application between collections, but the collector’s coordination and tracing remain a shared event. It also explains why retaining one pointer to a large structure matters: reachability, not whether I personally remember using the object, determines whether it survives.

I prefer reducing needless allocation only after a profile or latency measurement identifies it. Reusing a buffer at a clear ownership boundary can help. Converting every local variable into a global cache can help the heap grow mysterious hair. Smaller live data and fewer transient objects are good outcomes; cleverness measured only by line count is not.

The caveat is that stack and heap placement are compiler decisions, and the compiler is changing. I avoid writing code that depends on a guessed escape decision. I also avoid claims about a future collector when explaining this one. Today’s pauses are real even if the runtime will improve.

For the server, pooling two large scratch buffers around a serialized operation removed most of the allocation burst. I kept the simpler allocating version in a benchmark so the trade remains measurable. The garbage collector was doing exactly what it had been asked to do; I had simply been sending rather enthusiastic invitations.

The Go 1 preview and gofix

The Go 1 plan changed how I looked at a compiler error today. Until now, a broken build after updating was mostly the price of following a young language. With a compatibility target in sight, the churn starts to look like a migration with a destination.

I tried gofix on a copy of a small package before changing anything by hand. The useful workflow is pleasantly conservative:

$ gofix -r rule package.go
$ hg diff package.go

The available rules and invocation depend on the snapshot, so I check the tool’s help rather than preserving that command as scripture. The second command matters more: automated rewriting should produce a diff I can understand.

My surprise was that the mechanical part was the easy part. Renaming an API or adjusting a call shape can often be recognized from syntax and type information. Deciding whether the programme still means the same thing requires a person and tests. A clean compile is evidence, not absolution.

gofix works by parsing Go source rather than treating it as undifferentiated text. That lets a rule identify a particular construct and rewrite it while preserving valid Go structure. It can then format the result. This is much safer than a global search-and-replace that cannot distinguish a package name from a comment or an unrelated identifier.

The Go 1 preview is important because it proposes a stable base for source code and core packages. It does not mean every implementation detail freezes, nor does it make the current preview the final release. There is still work between a compatibility promise and a toolchain one can call 1.0 without crossing one’s fingers behind one’s back.

I prefer making these migrations promptly and in small commits. Old code plus several skipped snapshots turns a sequence of obvious edits into software archaeology. I run gofix, inspect every hunk, format, build, and exercise the behaviour that changed.

The caveat is generated source and unusual build arrangements. A rewrite may need to happen at the generator, not merely in its output. Local APIs with names resembling standard ones also deserve attention.

Tools cannot remove change, but they can make change reviewable. That seems a suitably Go-like ambition: less ceremony, still no magic.