Go toolchain, project layout, editor setup, and quick reference. Your cheat sheet for the course.
Your cheat sheet. Come back here whenever you need a quick lookup.
The course is five tracks. Each track builds on the last, but not every module within a track is required to move forward.
Track 1 (Modules 1-3) is drill-heavy on purpose. Slices, maps, structs, errors, testing — you do these until they're automatic. Don't rush through. If the warmups feel hard, re-read the lesson. If the warmups are easy but challenges are hard, that's working as intended — use the hints.
Track 2-3 (Modules 4-7) shift to building real things: CLIs, API clients, concurrency. Projects start appearing. Do the projects — they're portfolio pieces, not homework.
Track 4 (Modules 8-11) is the steepest ramp. HTTP servers → raw networking → container internals → K8s operators. If a module feels too hard, here's what's safe to skip or reorder:
Track 5 (Modules 12-13) is interview prep and open source. The algorithms plugin gives you practice throughout the course, so Module 12 is reinforcement, not a cold start.
Each module has a lesson, warmups, and challenges. Here's the flow:
Every exercise has multiple variants, so you can shuffle and get a fresh version of the same concept. Use the shuffle button liberally. If a challenge is too hard, hit "Get Easier Version" to step down a difficulty level (and "Get Harder Version" when you're ready to push).
When you open an exercise, you'll see a thinking timer option. It locks the hints and solution for 45 seconds, forcing you to actually think before reaching for help. Use it. The point of exercises is the struggle, not the answer.
Your self-ratings feed into a spaced repetition system. Exercises you struggled with come back sooner; ones you nailed fade into longer intervals. Use Daily Practice to stay sharp across modules — it pulls exercises that are due for review or that you've historically found hard.
Install from go.dev/dl or your package manager. Verify:
go version # go1.22+ recommended
which go # should be in $PATH
On Void Linux:
sudo xbps-install -S go
go mod init github.com/you/project # create go.mod (do this first in every project)
go mod tidy # add missing deps, remove unused ones
go get github.com/spf13/cobra@latest # add a dependency
go.mod tracks dependencies. go.sum locks checksums. Both get committed to git.
myproject/
├── go.mod # module definition + dependencies
├── go.sum # dependency checksums (auto-generated)
├── main.go # entry point (small projects)
├── cmd/ # entry points (multi-binary projects)
│ └── myapp/
│ └── main.go
├── internal/ # private packages — can't be imported by other modules
│ ├── config/
│ └── server/
├── pkg/ # public packages — can be imported (optional, some projects skip this)
└── Makefile # build/test/lint shortcuts
Rules of thumb:
main.go) until it gets painfulinternal/ when you have multiple packages that shouldn't be imported externallypkg/ when you intentionally want other projects to import your library codemain.go per binary, under cmd/ if you have multipleHelix has built-in LSP support. Just install gopls:
go install golang.org/x/tools/gopls@latest
Helix auto-detects gopls for .go files. Verify in ~/.config/helix/languages.toml:
[[language]]
name = "go"
auto-format = true
formatter = { command = "goimports", args = ["-local", "github.com/you"] }
Install goimports for auto-organizing imports:
go install golang.org/x/tools/cmd/goimports@latest
If using nvim-lspconfig:
require('lspconfig').gopls.setup{}
For format-on-save, add to your config:
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.go",
callback = function()
vim.lsp.buf.format({ async = false })
end,
})
Install the official Go extension by the Go team. It handles gopls, formatting, testing, and debugging out of the box.
| Command | What it does |
|---|---|
go run main.go |
Compile and run in one step |
go run . |
Run the package in the current directory |
go build -o myapp . |
Compile to binary |
go test ./... |
Run all tests recursively |
go test -v ./... |
Verbose test output (see each test name) |
go test -run TestName |
Run a specific test |
go test -count=1 ./... |
Skip test cache |
go fmt ./... |
Format all Go files |
go vet ./... |
Catch common mistakes (unused vars, printf mismatches) |
go mod tidy |
Sync go.mod with actual imports |
go doc fmt.Sprintf |
Show docs for a function |
go doc -all net/http |
Full package docs |
# Cross-compile for Linux (useful on Mac)
GOOS=linux GOARCH=amd64 go build -o myapp .
# Build with version info embedded
go build -ldflags "-X main.version=1.2.3" -o myapp .
# Race detector — catches data races in tests
go test -race ./...
| Resource | URL | When to use |
|---|---|---|
| Go Playground | go.dev/play | Quick experiments, sharing snippets |
| Package Docs | pkg.go.dev | Look up any package's API |
| Effective Go | go.dev/doc/effective_go | Go idioms and style |
| Go by Example | gobyexample.com | Concise examples of every feature |
| Go Wiki | go.dev/wiki | Community knowledge base |
| Go Blog | go.dev/blog | Official announcements and deep dives |
| Go Spec | go.dev/ref/spec | When you need the exact language rules |
Go won't compile with unused imports. Fix: remove the import, or use _ to suppress:
import _ "net/http/pprof" // side-effect import (registers handlers)
Better fix: use goimports — it adds/removes imports automatically on save.
Same deal — unused variables are compile errors in Go. Either use the variable or delete it. During debugging, _ = myVar suppresses the error temporarily.
Type mismatch. Go has no implicit conversions. Common cases:
var x int = 42
var y int64 = int64(x) // explicit conversion required
var s []byte = []byte("hello") // string → []byte
var t string = string(s) // []byte → string
You called a method or accessed a field on a nil pointer. Debug steps:
// Common cause: uninitialized map
var m map[string]int
m["key"] = 1 // PANIC — m is nil
// Fix: initialize with make
m := make(map[string]int)
m["key"] = 1 // fine
You're ignoring an error return:
// Wrong
file := os.Open("config.yaml")
// Right
file, err := os.Open("config.yaml")
if err != nil {
log.Fatal(err)
}
:= only works inside functions. At package level, use var:
// Package level
var version = "1.0.0"
func main() {
// Inside functions, := works
name := "myapp"
}
Things you'll reach for instinctively and need the Go equivalent.
if x in my_list:
print("found")
Go — no in keyword. Loop or use a map.
// Option 1: loop
for _, v := range mySlice {
if v == x {
fmt.Println("found")
break
}
}
// Option 2: use a set
seen := map[string]bool{"a": true, "b": true}
if seen["a"] { fmt.Println("found") }
nums.append(99)
last = nums[-1]
nums = append(nums, 99) // must reassign!
last := nums[len(nums)-1] // no negative indexing
", ".join(items)
s.split(",")
s.strip()
f"hello {name}"
strings.Join(items, ", ")
strings.Split(s, ",")
strings.TrimSpace(s)
fmt.Sprintf("hello %s", name)
try:
f = open("config.yaml")
except FileNotFoundError:
print("missing")
f, err := os.Open("config.yaml")
if err != nil {
fmt.Println("missing")
}
val = ages.get("dave", 0)
val, ok := ages["dave"]
if !ok {
val = 0 // handle missing key yourself
}
for i, v in enumerate(items):
print(i, v)
sorted(items, key=lambda x: x.age)
for i, v := range items {
fmt.Println(i, v)
}
sort.Slice(items, func(i, j int) bool {
return items[i].Age < items[j].Age
})
nil (works for pointers, interfaces, maps, slices, channels)type X struct { ... } and write your own constructorv, ok := x.(T) (type assertion)