The Ecosystem & Tooling - Batteries Included
Picking up a new language usually means a second, miserable project: choosing a build tool, a test runner, a formatter, a linter, a package manager - and making them all agree. Go's answer is one of its best features:
The toolbox comes in the box. Install Go and you already have the build tool, the test runner, the formatter, the vetter, and the dependency manager - one command,
go, with subcommands.
Each tool is a go <verb>, each does one job, and together they're why an unfamiliar Go project is usually trivial to build, test, and read.
go run and go build - execute vs. produce
go run compiles your code to a temporary binary and runs it immediately - the fast feedback loop while working. go build compiles and keeps the result: a single, self-contained executable you can ship.
📝 Terminology. Go compiles to a single native binary with no separate runtime to install on the target machine - build it once and copy that one file. That's why "deploy a Go service" is often "scp one file," and why Docker images for Go are tiny.
$ go run .
Hello, Missing Manual!
$ go build -o hello .
$ ls -lh hello
-rwxr-xr-x 1 you staff 2.1M Jun 19 10:22 hello
$ ./hello
Hello, Missing Manual!
go run . compiled the package in the current directory and ran it on the spot, leaving no file behind - perfect for iterating. go build -o hello . did the compile but wrote the result to a file named hello, which we then ran directly. The binary is a couple of megabytes because Go statically bundles everything it needs - no dependencies to chase on the server.
go fmt - the end of formatting debates
go fmt (which runs the gofmt tool) rewrites your code into Go's one true canonical format: tabs, brace placement, spacing, alignment - all non-negotiable. There's nothing to argue over because there are essentially no options.
Why this is a feature, not a constraint. Every other language community has burned years on tabs-vs-spaces, brace style, line length. Go ended the war by decree: one format, tool-enforced, so all Go code - yours, the standard library's, a stranger's on GitHub - looks the same. Code review stops being about style and starts being about substance. Most editors run gofmt on save.
$ cat messy.go
package main
import "fmt"
func main(){
fmt.Println( "hi" )
}
$ gofmt -w messy.go
$ cat messy.go
package main
import "fmt"
func main() {
fmt.Println("hi")
}
The original file had crooked indentation, a cramped import, and stray spaces inside the parentheses. gofmt -w (-w writes changes back to the file) rewrote it into the canonical layout - blank line after the package clause, tab indentation, tidy spacing. One correct answer, and the tool applied it.
💡 Key point. Don't hand-format Go and don't argue about style in review - let gofmt do it. "Run it through gofmt" is the entire formatting policy of the Go world.
go vet - catches suspicious code the compiler allows
go vet is a built-in static analyzer that flags code that compiles fine but is probably a bug - the classic example being a Printf whose format verbs don't match its arguments.
$ cat main.go
package main
import "fmt"
func main() {
name := "Ada"
fmt.Printf("hello %d\n", name) // %d expects an int, but name is a string
}
$ go vet .
# example/hello
./main.go:7:2: fmt.Printf format %d has arg name of wrong type string
The code compiled - Printf takes any arguments - but it's wrong: %d is for integers and name is a string, so at runtime you'd get garbled output like hello %!d(string=Ada). go vet spotted the mismatch before you ran it - the cheap safety net for bugs the compiler is too permissive to reject.
go test - testing is built in
Go's test runner is part of the toolchain. Write tests in files named *_test.go, in functions named TestXxx(t *testing.T), and run them with go test. No framework to install, no config file.
Given a function and a test beside it:
// math.go
package mathx
func Double(n int) int
// math_test.go
package mathx
import "testing"
func TestDouble(t *testing.T)
$ go test ./...
ok example/mathx 0.003s
go test ./... found every test in the module (./... means "this directory and all subdirectories"), compiled them with their packages, ran each TestXxx, and reported ok because nothing called t.Errorf. A test "fails" by reporting through t - no assert library, just a plain if and t.Errorf when reality doesn't match expectations.
📝 Terminology. ./... is Go's wildcard for "the current directory and everything beneath it recursively." You'll use it constantly - go test ./..., go vet ./..., go build ./....
go mod - dependencies, the modern way
A module is the unit Go uses to track your project and its dependencies, defined by a go.mod file at the root. go mod init creates it, go get adds a dependency, and go mod tidy syncs go.mod to exactly what your code actually imports.
$ go mod init example/hello
go: creating new go.mod: module example/hello
$ go get github.com/google/uuid
go: added github.com/google/uuid v1.6.0
$ go mod tidy
$ cat go.mod
module example/hello
go 1.25
require github.com/google/uuid v1.6.0
go mod init created a go.mod declaring your module's name. go get downloaded the uuid package, recorded the exact version (v1.6.0), and wrote a go.sum file with cryptographic checksums so future downloads are verifiably identical. go mod tidy reconciled go.mod with your real imports - adding what you started using, removing what you stopped. (Project layout and go.mod in depth: Phase 5; this is the tooling side.)
💡 Key point. go mod tidy is the one to run before you commit - it guarantees go.mod/go.sum exactly match your imports, so a fresh clone builds with no surprises.
The standard library - more "batteries" than you expect
Go's standard library is unusually broad and production-grade. A real HTTP server, JSON encoding/decoding, cryptography, file and OS access, regular expressions, templating, and the testing tools above - all in the box, no third-party packages required.
An HTTP server in the standard library alone:
package main
import (
"fmt"
"net/http"
)
func main()
$ go run . &
$ curl localhost:8080
Hello from net/http!
With nothing but net/http, we registered a handler for / and started a real web server on port 8080 - no framework, no dependencies in go.mod. The curl hit it and got the response. This is why a lot of Go services run on the standard library for a long time before reaching for anything external. (Popular web frameworks built on top of this are signposted in Phase 18.)
golangci-lint - the one tool worth adding
The toolchain covers almost everything, but the community standard for deeper linting is golangci-lint - a fast runner bundling dozens of linters (including go vet and the errcheck "you ignored an error" check from Phase 7) behind one command.
$ golangci-lint run ./...
main.go:12:2: Error return value of `f.Close` is not checked (errcheck)
f.Close()
^
golangci-lint run ./... ran its whole battery of linters in one pass and flagged an unchecked error - exactly the silent-failure trap from the errors phase. It's the single external tool most Go teams install; everything else they need already shipped with Go. (Install from the official instructions at golangci-lint.run - versions and install methods change, so check the source rather than copying a command that may be stale.)
Recap
go run/go build- run on the spot vs. produce a single self-contained binary you can ship.go fmt- one canonical format, enforced; it ends style debates so review is about substance.go vet- flags compiles-but-probably-wrong code (likePrintfverb mismatches).go test ./...- built-in test runner; tests areTestXxx(t *testing.T)in*_test.go, no framework needed.go mod-init,get, and especiallytidyto keep dependencies exactly matching your imports.- Standard library +
golangci-lint- batteries likenet/httpandencoding/jsonare built in;golangci-lintis the one external tool most teams add.
You've got the language and the toolbox. What's left is a handful of conventions and gotchas that bite everyone once.
← Phase 7: Errors & I/O · Phase 9: Idioms & Common Gotchas →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. `go fmt` (gofmt)...
2. Go's testing is...