Part I — Foundations: the Go way
The language itself, in order: what problem it was built to solve, the toolchain, and then the pieces every Go program is made of — types, control flow, the error-handling pattern that shapes all Go code, and just enough of slices and maps' internals to explain the bugs you'll eventually hit. Two terminal projects at the end put it all to work before Part II adds concurrency on top.
1. What Go is, and why it exists
1.1 The problem being solved
Go was created at Google in 2007–2009 by Robert Griesemer, Rob Pike, and Ken Thompson, in response to a very specific pain: Google's C++ services took tens of minutes to build, dependency graphs were tangled, and the languages available (C++, Java) made concurrent, networked systems hard to write safely. They wanted a language with the safety and readability of a garbage-collected language, the fast compilation of an interpreted one, and concurrency as a first-class citizen instead of a library bolted on top.
1.2 The design goals, and how they show up in the syntax
| Goal | Consequence in the language |
|---|---|
| Fast compilation | No header files, no generics until 1.18 (kept simple even then), strict unused-import/variable errors so the compiler never guesses |
| Readability at scale | One way to format code (gofmt), no operator overloading, no implicit conversions |
| Safe concurrency | Goroutines + channels as language primitives, not a library; the race detector is built into the toolchain |
| Simple deployment | Single-file binaries are common; pure-Go builds are often static, while cgo builds may depend on system libraries |
| Explicitness | Errors are values, not exceptions; nothing happens "automatically" except defer, GC, and slice growth |
1.3 What Go deliberately does not have
No exceptions (only panic/recover, reserved for truly exceptional
conditions), no classes or inheritance (only structs, interfaces, and
composition), no operator overloading, no implicit numeric conversions, and
—until you explicitly opt in with generics—no generic containers. This is
not an oversight; it is the single biggest reason a large, multi-team Go
codebase stays readable after five years. A Go file you've never seen still
looks like every other Go file.
1.4 War story
A team migrating a Python API gateway to Go expected the rewrite to take
three months. It took six weeks, and the resulting binary went from a
150MB Docker image with a Python interpreter and dependencies to an 18MB
FROM scratch image with a single static binary. The surprise wasn't
performance — it was that code review got faster, because there was
no longer a debate about "should this be a decorator, a metaclass, or a
mixin." In Go there's usually one obvious way to write a given piece of
logic, and reviewers spend their time on the logic instead of the style.
# The whole "hello world" loop
mkdir hello && cd hello
go mod init example.com/hello
cat > main.go <<'EOF'
package main
import "fmt"
func main() {
fmt.Println("hello, go")
}
EOF
go run main.go