Why here, and why before Rust. Go is the cleanest possible contrast to Java 8 on the two axes that matter most in Arc III: what a concurrent program looks like, and what an error is. It is also easy enough that the concurrency model itself is the only hard thing in the track — which is exactly what you want before Rust, where ownership and concurrency arrive together and you need one of them to already be familiar.
Robert Griesemer, Rob Pike, and Ken Thompson at Google, announced 2009, 1.0 in 2012. The founding grievance is well documented and unusually specific: C++ build times at Google, and the complexity cost of a language whose feature set no single engineer held entirely in their head.
So Go is a language designed by subtraction. No inheritance. No exceptions. No generics until 1.18, thirteen years in, and then only reluctantly. No operator overloading, no implicit conversions, no ternary operator, and — famously — an unused import or variable is a compile error, not a warning.
The consistent theme: optimise for the reader, and specifically for the reader who joined the team last month and has to fix something today. That is a different target from Python's reader (who is writing the code now) and from Java's (who is maintaining 500k lines in five years), and the three languages are shaped accordingly.
Who pays for it today: cloud infrastructure, almost totally — Docker, Kubernetes, Terraform, Prometheus, etcd, Consul, and most of the CNCF landscape. Also CLI tooling, network services, and a growing share of backend APIs. Go took much of this territory from Python after 2013 for one unglamorous reason covered in Q2.
go build -o build/logagg.exe .
go run . ../../bench/data/access.log
.\build.ps1
Three things here are the whole story of why Go spread through infrastructure:
A single static binary, cross-compiled trivially.
GOOS=linux GOARCH=arm64 go build -o logagg-linux-arm64 .
No runtime to install, no interpreter version, no JVM, no libc to match. Copy the file to the
target and it runs. This is the reason kubectl and terraform are Go programs and not Python
ones — the Python distribution problem covered in that track's Q2 is
simply not a problem Go has.
Modules, and the toolchain that comes with them. go.mod is three lines. go test,
go vet, gofmt, go doc, and the race detector are all in the box — there is no separate
linter, formatter, test runner, or build tool to choose. Compare with the Java track's
Maven-versus-Gradle decision and the Python track's five-way environment-manager table.
gofmt is not configurable. Formatting is not a matter of team preference because the
tool does not accept preferences. An enormous category of code review argument simply does not
exist in Go, and it is worth noticing how much of a language feature that turns out to be.
A variable is a typed slot holding a value, and — unlike Java — user-defined types can be
values, not just references. Stats here could have been stored by value in the map; it is
stored as *Stats so that s.add(...) mutates the real thing rather than a copy. That choice
does not exist in Java and cannot be expressed in Python.
Static and strong, with no implicit conversion at all. int and int64 are different
types and mixing them is a compile error. Verbose, and it makes SPEC 2's integer arithmetic
unambiguous in a way it is not in C, where promotion rules quietly do work for you.
Garbage collected, with a collector tuned hard for low pause times rather than peak throughput — the opposite of the JVM's historical default, and the right trade for a network service.
The things Go gives you that Java does not:
&x and pass it around; you cannot
add 4 to it. Most of C's power at the API boundary, none of the buffer overruns.(result, error) idiomatic rather than
awkward.nil is a real value for pointers, slices, maps, channels, interfaces, and functions —
and this is the source of Go's worst footgun (Q8).Zero values are a design pillar: every type has a usable zero. A nil map reads as empty, a
zero sync.Mutex is an unlocked mutex, a zero bytes.Buffer is an empty buffer. It removes a
whole class of constructor ceremony — and it is why res.services[f[1]] returning nil for a
missing key is idiomatic rather than a bug.
The package, and the interface — and the interface is the interesting half.
Go interfaces are structural and satisfied implicitly. A type implements an interface by
having the methods. There is no implements keyword and, critically, the interface can be
declared by the consumer after the fact:
type Reader interface { Read(p []byte) (n int, err error) }
Anything with that method is an io.Reader — including types written years before io.Reader
existed, in packages that have never heard of it. Java's BufferedReader must declare its
interfaces up front, so retrofitting a contract onto someone else's type requires an adapter.
Go's approach is duck typing with compile-time checking, and it is the single most-copied idea
in the language.
The convention that follows: "accept interfaces, return structs", and keep interfaces tiny.
io.Reader has one method. error has one method. The Go proverb is "the bigger the
interface, the weaker the abstraction" — which is the exact inverse of the instinct a Java
codebase trains.
Composition instead of inheritance, via embedding: put a type in a struct without a field
name and its methods are promoted. It looks like inheritance and is not — there is no virtual
dispatch to a subclass and no super.
Errors are values. No exceptions, no unwinding. error is an ordinary interface with one
method, returned alongside the result:
res, err := aggregate(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "logagg: cannot read %s: %v\n", args[0], err)
return 12
}
The much-mocked if err != nil appears three times in this program. The case for it, which is
stronger than the mockery suggests:
Files.newBufferedReader throws; here the signature says so and the compiler enforces that
you name the variable.fmt.Errorf("reading %s: %w", path, err) wraps, and
errors.Is / errors.As inspect. Wrapping with %w builds a chain that carries context
without a stack trace.The cost is real: it is repetitive, and there is nothing stopping you writing res, _ := and
discarding the error entirely. Go's protection is cultural plus go vet, not the type system
— Rust's Result with #[must_use] is the same idea with teeth, and reading them in this
order makes that obvious.
panic/recover exist and are not the error mechanism. They are for programmer error —
index out of range, nil dereference — and a library that panics across its API boundary is
considered broken.
Note that classify here returns a string, not an error, for the same reason it did in
Java and Python: a malformed line is expected input, not a failure. Three tracks, three
error models, same judgement. That convergence is worth more than any of the three mechanisms.
CSP: goroutines and channels. This is the reason Go is in Arc III and the reason to learn it at all.
A goroutine is a function call with go in front of it. It is not an OS thread — it starts
at a ~2 KB growable stack and is multiplexed onto a small pool of OS threads by the runtime
scheduler. A million of them is routine. A million Java 8 platform threads is not possible;
this is precisely the gap Java 21's virtual threads were built to close, and the Java 21 track
should be read directly against this section.
A channel is a typed, synchronised queue. The slogan — "do not communicate by sharing memory; share memory by communicating" — means the default answer to "how do these two pieces of work coordinate" is a channel, not a mutex.
For --shard N (SPEC 6) the Go version is genuinely the shortest of any track:
partials := make(chan map[string]*Stats, n)
for i := 0; i < n; i++ {
go func(chunk []string) { partials <- aggregateChunk(chunk) }(chunks[i])
}
for i := 0; i < n; i++ { merge(total, <-partials) }
No lock, because no memory is shared — each goroutine owns its map until it hands it over.
select waits on several channels at once and is what makes timeouts and cancellation
expressible without a callback. context.Context is built on it and is how cancellation
propagates through every real Go codebase.
What Go makes easy to get wrong: goroutine leaks. A goroutine blocked forever on a channel
nobody will write to is not collected and not reported. It is the Go equivalent of a memory
leak, and context plus disciplined channel closing is the defence. Run anything concurrent
under go test -race — the race detector is excellent and built in.
logagglogagg.go · lab notes in NOTES.md
~250 lines — between Python's 190 and C's 330, and notably longer than Java 8's 210 despite Go being the simpler language. Where the lines go:
| Java 8 | Go | |
|---|---|---|
| Map insert-or-create | computeIfAbsent(k, Stats::new) |
5 lines: read, check nil, construct, store |
| Sort | Comparator.comparingLong(...).reversed().thenComparing(...) |
sort.Slice with an explicit two-branch closure |
| Error handling | one try block |
if err != nil × 3 |
Go has fewer conveniences on purpose. The trade is that every one of those Go lines says
exactly what it does, and none of them required knowing which of thenComparing's two
overloads the compiler would pick.
Three things Go made easier than every other track:
No CRLF problem. Go has no text mode; os.Stdout.WriteString writes the bytes given. The
C track needed _setmode, Java needed System.out.write(bytes), Python needed
sys.stdout.buffer. Go needed nothing, and that is a deliberate design position about
platform differences rather than an accident.
strings.Fields is SPEC 1's whitespace rule exactly, same as Python's str.split().
Map iteration is randomised, not merely unspecified:
sort.Slice(ordered, func(i, j int) bool { ... })
Go actively shuffles map iteration order on every run so that a program depending on it fails on your laptop immediately rather than in production in two years. Without SPEC 3's mandatory tie-break this program's output would differ between consecutive runs of the same binary — which is the single best argument for that clause in the spec, and it only became visible in this track.
One structural thing worth copying into other languages:
func main() { os.Exit(run(os.Args[1:])) }
os.Exit does not run deferred functions. Calling it from inside aggregate would skip
defer fh.Close() silently. Keeping os.Exit to exactly one line, with all the work in a
function that returns a code, is the standard Go answer and a genuinely good habit
everywhere.
nil *MyError stored in an error interface
makes err != nil true, because the interface holds a type and a value and only the
value is nil. This is Go's worst wart, it has caused real outages, and go vet does not
catch every case.for _, v := range xs { go func() { use(v) }() }
captured one shared v and every goroutine saw the last value. Fixed in 1.22 by making the
variable per-iteration — worth knowing because an enormous amount of pre-1.22 code and
tutorial material is still out there.append. append may or may not allocate. Two slices backed by the
same array, one appended to, is either aliasing or not depending on capacity. The failure
shows up as data mysteriously changing.bufio.Scanner's 64 KB token limit. Silently stops on a long line and sets
sc.Err() to bufio.ErrTooLong — which you only see if you check it. logagg.go does
check; a great deal of production Go does not.res, _ := doThing() compiles. errcheck and
go vet help; nothing enforces it.Recommended: the standard library's net/http/server.go, specifically (*conn).serve. It
is the canonical example of one-goroutine-per-connection, and it shows the real-world shape of
defer, panic recovery at a boundary, and context cancellation in code that has been beaten
on by everyone.
Shorter, and read it first: sort/sort.go and errors/wrap.go. errors/wrap.go is about
forty lines and is the entire error-wrapping mechanism — Is, As, Unwrap. Reading it makes
%w stop being magic.
For the concurrency model specifically: sync/errgroup (in golang.org/x/sync). Small,
and it is the pattern every Go codebase reaches for when it needs the --shard N structure.
Result stop looking like unrelated mechanisms and start looking like three answers
to one question: should the type system force you to acknowledge failure? Go says yes but
does not enforce it; Java said yes and enforced it badly; Rust says yes and enforces it well.done. gofmt -l clean, go vet clean, and byte-exact against the golden file on the first
compile — the only track so far where that happened, for the reasons in NOTES.md.
PASS c
PASS java8
PASS go
PASS python
Outstanding: --shard N (SPEC 6), which for this track is the actual content rather than an
extra, and a repo-level fixture gap — access.log has no tab-separated line, so no track's
whitespace handling is exercised against one.