Programming

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.

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.