Go

What Go 1 compatibility buys

I rebuilt a March programme with a newer Go checkout today and it compiled unchanged. Six weeks ago that would not have been remarkable in most languages. After a year of weekly snapshots, it felt almost suspicious.

The practical test was modest:

$ go test example.org/...

I wanted source written for Go 1 and its standard packages to keep building as Go 1 implementations advance. That is the central value of the compatibility promise: users can update the toolchain for fixes and improvements without scheduling a language migration for ordinary conforming source.

My surprise was how much this changes maintenance planning. Before Go 1, I treated the compiler version as part of each source branch because language and package changes could require gofix. Under the Go 1 promise, the source version becomes a much less active participant. Tests still matter, but routine upgrades should not begin with syntax archaeology.

Compatibility applies to specified source behaviour and the documented standard library, not every observation a programme can make. Compiler diagnostics can improve. Binary formats, object files, and internal runtime structures can change. Performance can move in either direction. A programme depending on an undocumented detail has discovered a detail, not negotiated an additional promise.

Operating systems and external C libraries remain outside that promise as well. A cgo package can fail after a system header changes even when its Go source is perfectly compatible. Data formats and network protocols need their own stability policies. Go 1 cannot sign contracts on behalf of the rest of the machine.

I prefer writing against exported, documented APIs and testing behaviour rather than implementation trivia. If I inspect an internal representation for education or performance work, I label it with the exact toolchain and do not turn it into application logic. That keeps implementation freedom available to the people improving the compiler and runtime.

The caveat is bugs. Preserving an accidental result forever can be worse than correcting it, and the compatibility documentation leaves room for necessary fixes and clarifications. Security and correctness do not become subordinate to an interesting old mistake. Release notes remain worth reading.

This promise may be the least glamorous feature in Go 1, but it compounds. Every package that continues to build is time not spent on mandatory churn. My test command produced no story at all, which is exactly the story compatibility is supposed to produce.

GOPATH is a workspace, not a package

I set GOPATH to the directory containing one package and then wondered why go install produced a peculiar path. The variable was innocent. I had mistaken the whole workspace for one shelf in it.

A minimal workspace has a recognizable shape:

$GOPATH/
    src/example.org/hello/hello.go
    pkg/
    bin/

Source lives below src in directories corresponding to import paths. Installed package objects go below pkg, arranged for the target system. Installed commands go in bin. GOPATH names the root containing those areas, not src itself and not the package directory.

The surprise was that import paths and disk paths line up without making imports relative. Code can import example.org/hello; the go command searches the workspace’s src/example.org/hello. The source does not need to know how many .. components separate two packages on my machine.

This is a workspace convention, not a global registry. I can keep more than one workspace and select one through the environment. A GOPATH list can also provide multiple roots, although source and installation behaviour deserve attention when doing that. For a small project, one root is easier to reason about.

I prefer an import path that reflects where a package is expected to live, even before publishing it. It avoids renaming every import when the code leaves a scratch directory. I also keep third-party packages in the workspace layout rather than copying their files into my package and erasing their identity.

The caveat is that the workspace does not pin dependency versions. If I update a dependency in place, every package using that workspace sees the update on its next build. Reproducible releases therefore still require discipline about the exact source revisions checked out. The layout organizes code; it does not make dependency history disappear.

There is also no need to set GOROOT to the workspace. GOROOT identifies the Go installation and standard library. GOPATH identifies my code and other non-standard packages. Giving both variables the same value is less configuration than performance art.

After moving my package under src and pointing GOPATH at the parent workspace, go build, go test, and go install behaved as advertised. The directory tree is opinionated, but at least it has the courtesy to be visible.

Go 1 and the go command

Go 1 was released yesterday, so I took a small command-line programme out of its old build script and built it using only the new go command. The script did not put up much of a fight, which is usually a sign it had been waiting to retire.

From the package directory, the common operations are direct:

$ go build
$ go test
$ go install

go build compiles the package and its dependencies. go test builds and runs tests associated with it. go install places the resulting command or package object where the Go workspace expects it. Supplying an import path performs the same operation without first entering the directory.

The surprise was how much information the tool obtains from ordinary source. Import declarations already form the dependency graph. File names and build constraints identify operating-system or architecture variants. Package declarations establish the compilation unit. A separate build description for pure Go would repeat facts the compiler must know anyway.

This is not merely a shorter compiler command. The tool chooses and invokes the appropriate compiler, assembler, and linker, tracks dependencies, and gives packages a common vocabulary. That standard shape makes instructions from another project more useful: go test means the same kind of operation there as it does here.

I prefer package paths over lists of files. Building individual files can accidentally omit another file in the same package or bypass the conditions the package author expected. The package is the unit Go code is designed around, so it should also be the ordinary unit of building and testing.

There are caveats. cgo packages still bring a C toolchain and external libraries into the picture. Code generation and non-Go assets do not disappear because a pleasant command exists. The go command is deliberately opinionated, so a project that fights the package layout will not win by adding more shell around it.

The compatibility promise gives this tooling more weight. Go 1 source should remain source-compatible with later Go 1 releases, subject to the documented boundaries, while implementations improve underneath. That makes the command a stable front door rather than this week’s preferred route through the compiler tools.

My programme now builds with three unsurprising commands and no custom dependency list. Build engineering has not ended, but it has lost one excellent hiding place.

One tool for Go packages

I moved a test package this morning and expected to edit a build script. The go tool in a Go 1 development snapshot found the source by package path and did the ordinary work itself.

The basic loop is almost suspiciously short:

$ go build example/words
$ go test example/words
$ go install example/words

The surprise is not that one command has subcommands. It is that the tool understands Go packages instead of requiring me to list source files and their order. Imports describe dependencies, package directories group files, and the tool can decide what must be compiled.

This only works because the package layout is a convention shared by source and tool. A directory’s buildable Go files form a package, import paths identify dependencies, and build constraints can select platform-specific files. The command is small because the model underneath it is constrained.

I prefer this to maintaining a make rule for every package. Native libraries and generated files may still need external steps, but pure Go should not need a handwritten recital of its own import graph.

The caveat is that this is a preview from Go 1 development, not a retrospective about a finished Go 1 release. Tool behaviour and package APIs deserve checking against the snapshot in use. I am also resisting the urge to wrap the three commands in a larger script before there is any larger problem.

For this package, deleting build instructions was the feature. My build system now contains less personal folklore, which is generally where folklore is safest.

Learning the new time package

I updated a retry loop for the Go 1 preview and discovered that most of my old time code was really unit conversion wearing a wristwatch. The redesigned time package makes the unit part of the value.

The simple version now reads like the requirement:

deadline := time.Now().Add(250 * time.Millisecond)
for time.Now().Before(deadline) {
	if try() {
		return nil
	}
	time.Sleep(10 * time.Millisecond)
}
return errors.New("deadline exceeded")

The surprise was that 250 * time.Millisecond is not special parser syntax. time.Millisecond is a duration constant, and multiplication produces a time.Duration. The type gives nanoseconds a meaningful wrapper while ordinary arithmetic remains available. I no longer need a comment explaining whether an unadorned 250 is milliseconds, microseconds, or a small act of sabotage.

The package also separates an instant, a duration, and a location. A Time value identifies an instant and can be displayed in a location. A Duration is elapsed nanoseconds and has no calendar opinion. A Location contains the rules needed to interpret civil time. Treating these as distinct concepts prevents a surprising amount of accidental arithmetic.

Formatting takes a concrete reference layout rather than a string of percent directives. At first I found that peculiar. Then I noticed that the layout shows the desired result directly: year where a year goes, month where a month goes, and so on. It is memorable once learned, although “once learned” is performing useful labour in that sentence.

I prefer deadlines expressed by adding a duration to a starting time. Repeatedly subtracting hand-converted integer timestamps makes overflow and unit mistakes easy. I also prefer passing Duration through APIs instead of accepting an integer plus an implied unit.

The caveat is calendar time. Adding twenty-four hours is exactly twenty-four elapsed hours; it is not universally the same as “same local clock tomorrow” when location rules change. Parsing also needs an explicit layout and, when local interpretation matters, the appropriate location.

Because this is preview code, package details can still move before the release. The conceptual split already pays for the migration, though. My retry loop now contains time values instead of rumours about time values.