curayious Language Lab

Go — lab notes

The toolchain, and why winget was the wrong road

$ winget install --id GoLang.Go --source winget --silent --disable-interactivity
(no output, no toolchain, hangs until killed)

winget wants a session it can elevate from. From a non-interactive automation context there is nothing to show the UAC prompt to, so it blocks forever and prints nothing at all — the worst possible failure shape, because it looks identical to "still working".

The official ZIP archive needs no elevation: unzip it into a directory you already own and put bin on your PATH. That is everything the MSI does that matters. Written up as tools/install-go.ps1, which:

  1. asks https://go.dev/dl/?mode=json which release is current, so the script does not go stale with a pinned version
  2. downloads the windows-amd64 archive and verifies the SHA-256 from that same JSON
  3. extracts to %LOCALAPPDATA%\Programs\Go
  4. appends …\Go\bin to the user PATH — no admin, nothing outside the profile

On the checksum, honestly: it comes from the same origin as the download, so it proves the bytes arrived intact, not that go.dev is honest. That is exactly the assurance the MSI gives too.

go version go1.27.1 windows/amd64

One gotcha that will bite again: a persistent PATH change is read at process start, so every terminal already open — including the one that ran the installer's parent — cannot see it. The script sets $env:PATH for its own session so the verification works immediately; other shells need restarting.

It passed first compile

PASS  c
PASS  java8
PASS  go
PASS  python

4 passed, 0 failed, 0 skipped

gofmt -l . printed nothing and go vet ./... was clean, both first time. That is worth recording rather than glossing: of the four tracks, Go is the only one where the first compilable version was also byte-exact against the golden file. The C track needed the %-20.20s precision fix and the _setmode call; Java needed the thenComparing type annotation and the byte-level stdout write.

That is not because Go is better. It is because Go has fewer places to make a decision, and three of the decisions the other tracks got wrong are ones Go does not offer you.

Checking the three things flagged before compiling

  1. strings.Fields on tabs — still unverified, and it is a fixture gap, not a track gap: access.log contains no tab-separated line, so no track's whitespace handling is actually exercised against one. Closing it means regenerating the fixture, which means regenerating the golden file, which means amending SPEC 1. Deliberately not done as a side effect of adding Go. Logged below.
  2. %-20s pads by runes in Go, by bytes in C — confirmed by reading, not by test, since SPEC 4 says ASCII and no fixture service is non-ASCII. Real divergence, currently masked.
  3. The dot < 1 guard — correct. strings.IndexByte returns -1 for absent and 0 for a leading dot, and both must be rejected. The fixture's 12.48 and -3.000 cases pass.

Cross-compilation, checked rather than claimed

GOOS=linux GOARCH=arm64 go build -o logagg-linux-arm64 .
ELF 64-bit LSB executable, ARM aarch64, statically linked

One command, from a Windows x64 box, no toolchain to install, statically linked. The Q2 claim about why infrastructure tooling is written in Go rests on this, so it was worth spending thirty seconds proving instead of asserting.


Three places Go's defaults removed work

No text mode

os.Stdout.WriteString writes the bytes given, on every platform. The other three tracks each needed a different fix for the same problem:

Track Fix
C _setmode(_fileno(stdout), _O_BINARY)
Java 8 System.out.write(s.getBytes(US_ASCII))
Python sys.stdout.buffer.write(s.encode("ascii"))
Go nothing

Four rows, and the best single illustration in the repo of what a language choosing a default for you is worth.

Randomised map iteration

Go does not merely leave map order unspecified — the runtime actively shuffles it per run, so a program depending on it fails on the first developer's machine rather than in production two years later.

Which means: without SPEC 3's mandatory tie-break, this program's output would differ between consecutive runs of the same binary. The other three tracks would have passed regardless — CPython's dict is insertion-ordered and Java's HashMap is at least stable within a JVM version. Go is the track that would have caught a missing tie-break, and it is the strongest retrospective argument for that clause being in the spec at all.

One os.Exit, in one place

func main() { os.Exit(run(os.Args[1:])) }

os.Exit skips deferred functions. Calling it inside aggregate would silently skip defer fh.Close(). Putting all the work in a function that returns a code, with exactly one os.Exit at the top, is the standard Go answer — and worth carrying back to the Python track, where sys.exit inside a with block has a milder version of the same shape.

Things not done yet