Go ships a new minor every six months and the language is stable — old code keeps compiling. Recent additions worth using:
generics (1.18), structured logging via log/slog (1.21),
HTTP method routing + path variables in net/http (1.22),
and range-over-func / iterators (1.23). GOPATH-style projects are dead — everything new is a module.
Install · modulesSetup
bash
# Install (macOS / Linux). Prefer the official tarball; package managers lag.
brew install go # macOS
sudo apt install golang-go # Debian/Ubuntu
# Or pin a version with the go command itself
go install golang.org/dl/go1.22@latest
go1.22 download
# Project setup — modules, no GOPATH needed
mkdir myapp && cd myapp
go mod init github.com/me/myapp
go get github.com/spf13/cobra@latest
go mod tidy # prune unused, add missing
# Daily commands
go run ./... # run all packages
go build -o bin/app ./cmd/app # build a binary
go test ./... # run all tests
go vet ./... && gofmt -w . # static checks + format
Where things liveStandard library map
Go’s stdlib is unusually rich. Reach for it before the ecosystem — you almost never need a framework for HTTP, JSON, or testing.
fmt
Formatted I/O: Println, Sprintf, Errorf.
errors
Is / As / Join / New.
log/slog
Preferred Structured logging. Replaces log for new code.
type Reader interface { Read(p []byte) (int, error) }
Behavior set. Methods only.
Implicit satisfaction
Any type with the methods is a Reader. No declaration needed.
var r io.Reader = &myReader{}
Assignment proves it implements.
v, ok := i.(*Concrete)
Type assertion with comma-ok.
switch v := i.(type) { case A: … case B: … }
Type switch.
any = interface{}
Empty interface alias. Matches anything.
Small interfaces > big interfaces
Idiomatic: 1–3 methods.
Goroutines · channels · selectConcurrency
Goroutines
go fn(arg)
Launch a goroutine. Returns immediately.
go func(){ … }()
Inline anonymous form. Mind variable capture.
runtime.NumGoroutine()
Live count. Watch for leaks.
Channels
ch := make(chan int)
Unbuffered. Send blocks until receive.
ch := make(chan int, 8)
Buffered. Send blocks only when full.
ch <- v
Send.
v := <-ch
Receive.
v, ok := <-ch
ok is false when ch is closed and drained.
close(ch)
Signal "no more values". Sending to a closed channel panics.
for v := range ch
Receive until close.
chan<- T / <-chan T
Send-only / receive-only types. Use in signatures.
select & sync
select { case v := <-a: … case b <- 1: … default: … }
Wait on multiple channels. default is non-blocking.
case <-time.After(2*time.Second)
Per-iteration timeout.
case <-ctx.Done()
Cancellation arm. Always include in long-running loops.
sync.Mutex { Lock, Unlock }
Mutual exclusion. defer m.Unlock().
sync.WaitGroup { Add, Done, Wait }
Wait for N goroutines.
sync.Once.Do(fn)
Run once across goroutines. Idiomatic singleton init.
errgroup.Group.Go / Wait
Concurrent fan-out with first-error semantics.
Worked example
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs { // ranges until jobs is closed
time.Sleep(50 * time.Millisecond)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5) // buffered → non-blocking up to 5
results := make(chan int, 5)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ { // fan out 3 workers
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // signal "no more work"
go func() { wg.Wait(); close(results) }()
for r := range results {
fmt.Println(r)
}
}
Wrap · Is · AsErrors
errors.New("…")
Sentinel error value.
fmt.Errorf("ctx: %w", err)
Preferred Wrap with context. %w preserves the chain.
errors.Is(err, target)
Sentinel comparison through the chain.
errors.As(err, &ptr)
Walk the chain for a typed error.
errors.Join(e1, e2)
Combine multiple errors into one.
defer func(){ if r := recover(); r != nil { … } }()
Catch a panic. Last resort, not flow control.
return nil instead of empty struct
Idiomatic "no error".
Worked example
go
package main
import (
"errors"
"fmt"
"os"
)
// Custom error with structured context
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %q not found", e.Resource, e.ID)
}
// Wrapping: %w preserves the chain so errors.Is / errors.As can unwrap
func loadUser(id string) error {
_, err := os.ReadFile("users/" + id + ".json")
if err != nil {
return fmt.Errorf("loadUser(%s): %w", id, err)
}
return nil
}
func main() {
err := loadUser("u_42")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file missing — fall back to cache")
}
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("typed error: %+v\n", nfe)
}
}
Type parametersGenerics
func Map[T, U any](xs []T, fn func(T) U) []U
Generic function. [T, U any] = type params.
type Stack[T any] struct { xs []T }
Generic type.
cmp.Ordered
Built-in constraint: types with <.
type Numeric interface { int | float64 | … }
Union constraint. Pipe-separated types.
~int
Allow any type whose underlying type is int.
slices.Index, slices.Contains, slices.Sort
Generic stdlib helpers. Use these.
No method type parameters
Methods can’t introduce new type params — only the receiver type can.
Worked example
go
package main
import (
"cmp"
"fmt"
"slices"
)
// Generic function — works for any ordered type
func Max[T cmp.Ordered](xs []T) T {
m := xs[0]
for _, x := range xs[1:] {
if x > m { m = x }
}
return m
}
// Generic constraint with a method set
type Stringer interface { String() string }
func Join[T Stringer](xs []T, sep string) string {
out := ""
for i, x := range xs {
if i > 0 { out += sep }
out += x.String()
}
return out
}
func main() {
fmt.Println(Max([]int{3, 7, 2})) // 7
fmt.Println(Max([]string{"go", "is", "fun"})) // "is"
nums := []int{5, 1, 4, 2, 3}
slices.Sort(nums) // generic stdlib
fmt.Println(slices.Contains(nums, 4)) // true
}
Dependency · toolchainModules & tools
go mod init <path>
Create go.mod. Path is the import path.
go get pkg@v1.2.3
Add or upgrade a dependency. Version is optional.
go mod tidy
Add missing, drop unused. Run before commit.
go mod vendor
Copy deps into vendor/. Optional, useful for builds offline.
go work init & go work use ./mod-a ./mod-b
Multi-module workspace.
go build / go install
Compile / install to $GOBIN.
go run ./cmd/app
Compile + run.
go test -race -run TestX ./pkg
Race detector + test filter.
go test -bench=. -benchmem
Run benchmarks. Watch allocs.
go vet ./...
Static checks for common mistakes.
gofmt -w . / go fmt ./...
Format. Non-negotiable, no config.
go generate ./...
Run //go:generate directives.
HTTP service · ~45 linesEnd-to-end · HTTP service
Go 1.22 ServeMux with method routing + path variables, structured logging via
slog, graceful shutdown on SIGINT / SIGTERM. No framework needed.
Always check the second return value. Go’s idiom is paired returns — v, err := f() and
v, ok := m["k"]. Ignoring either is the most common source of nil deref and silent missing keys.
Pass context.Context as the first argument. Any function that does I/O, blocks, or could be canceled should take ctx and check <-ctx.Done(). Skip it only for pure CPU functions.
Don’t over-design with interfaces. Define an interface where it’s consumed, not where it’s implemented. One- or two-method interfaces (io.Reader, io.Writer) compose; large ones don’t.
Common trapsWatch out for
Variable capture in loops — fixed in 1.22.
Before 1.22, for _, v := range xs { go fn(v) } would close over the same v across iterations. 1.22 per-iteration scopes the variable. Old codebases still have the trap.
Append can alias.b := append(a, x) may return a slice sharing a’s backing array. Mutating b then touches a. Use slices.Clone when you need independence.
Nil interface != nil concrete pointer.
A var p *T wrapped in an interface is non-nil even though p == nil. Returning a typed nil from a function returning error hides bugs.
Go (Golang) is a statically typed, compiled language designed for simplicity, fast builds, and efficient concurrency. It is widely used for cloud infrastructure, CLI tools, microservices, network servers, and DevOps tooling. Notable projects built in Go include Docker, Kubernetes, Terraform, and CockroachDB.
What is the difference between a goroutine and a thread?
A goroutine is a lightweight, user-space concurrent function managed by the Go runtime — you can run millions of them. OS threads are much heavier (1 MB+ stack, kernel scheduling). The Go runtime multiplexes goroutines onto a small number of OS threads (GOMAXPROCS, defaulting to the number of CPUs). Start a goroutine with go fn().
How do channels work in Go?
Channels are typed conduits for sending values between goroutines: ch := make(chan int) creates an unbuffered channel, and make(chan int, 10) creates a buffered one with capacity 10. Send with ch <- v (blocks until a receiver is ready on unbuffered), receive with v := <-ch. Close a channel with close(ch) to signal completion to range loops.
What are interfaces in Go?
An interface is a set of method signatures. Any type that implements all the methods implicitly satisfies the interface — there is no implements keyword. This implicit satisfaction makes Go interfaces lightweight and composable. The io.Reader and io.Writer interfaces from the standard library are the canonical examples.
How does error handling work in Go?
Go functions return errors as explicit return values: func f() (result, error). Check with if err != nil. Wrap errors to add context using fmt.Errorf('doing X: %w', err), and unwrap with errors.Is or errors.As. There is no exception mechanism — panics are reserved for unrecoverable situations.
Is Go open source and free?
Yes. Go is BSD-licensed and developed by Google with an active open-source community. The compiler, standard library, and toolchain are all free. The official Go installer from go.dev is the standard distribution; no commercial licence is required.