PidokuInfra

Why Go, and the Toolchain

Foundations Beginner 30 min Difficulty 1/5 Topic 01 of 05

The idea in one minute#

Go is a compiled, statically typed, garbage-collected language built for writing servers and tools. The compiler turns your whole program — and a copy of the Go runtime — into one executable file with no dependencies. The runtime is not a virtual machine: it is ordinary compiled code inside your binary that schedules goroutines and manages memory.

Three design choices explain most of Go’s character: simplicity (one obvious way to do things, a language you can hold in your head), fast builds, and concurrency built in.

An analogy#

A food truck. Everything needed to serve — kitchen, fridge, generator — travels as one unit. Park it anywhere and it works. A Go binary is the truck: copy one file to a server and run it. Many other languages are a restaurant that needs the right building, gas line and staff (an interpreter, a VM, a folder of libraries) to exist before the first dish.

A picture#

flowchart TB
  SRC["main.go + packages"] --> GOC["go build<br/>parse, type-check, optimize"]
  MOD["go.mod, go.sum<br/>dependencies, pinned"] --> GOC
  GOC --> OBJ["machine code<br/>for one OS and CPU"]
  RT["Go runtime<br/>scheduler, allocator, GC"] --> LNK["linker"]
  OBJ --> LNK
  LNK --> BIN["one static executable"]
  BIN -->|"GOOS=linux GOARCH=arm64"| X["cross-compiled<br/>from any machine"]
  class SRC,MOD neutral
  class GOC,LNK compute
  class OBJ,RT memory
  class BIN,X io

How it really works#

What you get, and what you give up#

You getBecauseYou give up
One binary, trivial deploymentStatic linking, runtime includedSmall binaries (a “hello” is ~1.5–2 MB)
Fast compile timesSimple grammar, no header files, strict dependency rulesSome compiler cleverness
Memory safetyBounds checks, no pointer arithmetic, garbage collectionManual control of every allocation
Cheap concurrencyGoroutines scheduled by the runtimeHard real-time guarantees
Readable code across teamsgofmt, few featuresExpressiveness: no inheritance, no macros, exceptions replaced by error values

Go is the language of much of today’s infrastructure: Docker, Kubernetes, Prometheus, Terraform, etcd, and — relevant to the rest of this library — NVIDIA’s Kubernetes device plugin and GPU operator, and Ollama.

The toolchain is one command#

CommandDoes
go run .Compile and run the package in this directory
go buildCompile to a binary
go test ./...Run tests in this module (-race, -bench, -cover, -fuzz add more)
go vet ./...Report suspicious constructs
gofmt -w . / go fmt ./...Format — there is one style, and nobody argues about it
go mod init, go mod tidyCreate a module; sync dependencies
go get pkg@versionAdd or change a dependency
go doc fmt.PrintlnRead documentation
go fix ./...Rewrite code to use newer language and library features
go tool pprof, go tool traceProfile and trace (module V)
go envShow the build environment

Building#

Shell
go build -o app .                              # for this machine
GOOS=linux GOARCH=arm64 go build -o app .      # for another OS and CPU: no extra toolchain
go build -gcflags=-m .                         # print the compiler's escape and inlining decisions (III.02)
go build -race .                               # with the data race detector (IV.05)
go version -m ./app                            # which Go and which module versions built this binary

Cross-compilation is a pair of environment variables because the compiler and the standard library are written in Go and carry every target with them. It stops being that easy only when the program uses cgo to call C (V.04).

Releases and compatibility#

  • A new release every six months (February and August). The two most recent are supported.
  • The Go 1 compatibility promise: a program that compiles with one Go 1.x compiles with later ones. Behaviour changes are gated by the go line in go.mod, so upgrading the toolchain does not silently change old modules.
  • The go line in go.mod is the minimum language version; an optional toolchain line names the toolchain to use, and the go command will download it if needed.

The shape of a program#

Go
package main // an executable is package main...

import "fmt" // ...that imports other packages...

func main() { // ...and starts here.
	fmt.Println("hello")
}

Capitalized names (Println) are exported from a package; lower-case names are private to it. That is the whole visibility system.

Code#

Go
// env.go — what am I, and what did the toolchain build?
package main

import (
	"fmt"
	"os"
	"runtime"
	"runtime/debug"
)

func main() {
	fmt.Println("Go version     ", runtime.Version())
	fmt.Println("target         ", runtime.GOOS+"/"+runtime.GOARCH)
	fmt.Println("compiler       ", runtime.Compiler)
	fmt.Println("CPUs visible   ", runtime.NumCPU())
	fmt.Println("GOMAXPROCS     ", runtime.GOMAXPROCS(0), "(threads running Go code at once)")
	fmt.Println("goroutines     ", runtime.NumGoroutine(), "(just main, so far)")

	// The runtime is already working before main starts: it has reserved memory.
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("heap in use     %d kB\n", m.HeapInuse/1024)
	fmt.Printf("obtained from OS %d kB\n", m.Sys/1024)

	if exe, err := os.Executable(); err == nil {
		if st, err := os.Stat(exe); err == nil {
			fmt.Printf("this binary     %.1f MB — your code plus the runtime\n", float64(st.Size())/1e6)
		}
	}
	if info, ok := debug.ReadBuildInfo(); ok {
		fmt.Println("module         ", info.Main.Path)
		for _, s := range info.Settings {
			if s.Key == "GOARCH" || s.Key == "GOOS" || s.Key == "CGO_ENABLED" {
				fmt.Printf("  %-12s %s\n", s.Key, s.Value)
			}
		}
	}
}

Remember this#

  • Go compiles to one static binary containing your code and the runtime.
  • The runtime is a library in your process, not a virtual machine.
  • One tool, go, builds, tests, formats, vets, profiles and manages dependencies.
  • New releases keep old code compiling; the go line in go.mod gates behaviour changes.

Try it#

  1. Run env.go with go run, then go build it and run the binary. Is the reported binary path different? Why?
  2. Cross-compile it for linux/amd64 and for windows/amd64. Compare the file sizes.
  3. Build with -ldflags="-s -w" (strip symbol and debug tables). How much smaller is it, and what did you lose?

Check yourself#

  1. What is the Go runtime, and where does it live?
  2. Why is cross-compiling a Go program usually trivial?
  3. What does the go line in go.mod control?

↑↓ navigate↵ openesc close