Relearn Go without leaving the important parts behind.
A structured, searchable reference covering syntax, type semantics,
generics, the runtime, channels, thread safety, standard packages,
web/data engineering, testing, tools, and modern ecosystem choices.
75explained topics
100+coverage checklist items
Single fileworks locally; progress persists
Exhaustive content plan
This inventory was used before writing the lessons. The detailed
cards below implement the plan one topic at a time.
Language foundations
✓ program/package structure✓ declarations and zero values✓ constants and iota✓ numeric types and conversions✓ strings/bytes/runes/UTF-8✓ arrays✓ slices and backing arrays✓ maps and sets✓ if/for/range/switch✓ functions and closures✓ pointers✓ structs and tags✓ embedding✓ methods and receivers✓ method sets✓ interfaces and nil interfaces✓ type assertions/switches✓ generics and constraints✓ iterators✓ errors/wrapping/custom errors✓ defer/panic/recover
Concurrency and runtime
✓ concurrency vs parallelism✓ goroutines✓ channels✓ buffering and close semantics✓ select✓ timeouts✓ context cancellation✓ WaitGroup✓ errgroup✓ worker pools✓ semaphores✓ backpressure✓ pipelines/fan-in/fan-out✓ Mutex/RWMutex✓ Once/Cond/Pool✓ atomics✓ memory model✓ race detector✓ scheduler G-M-P✓ GOMAXPROCS✓ OS threads and blocking calls✓ multiprocessing✓ goroutine leak prevention
Use the left navigation as a refresher path, or search for a
symbol, package, or production concern. Mark topics complete as
you revisit them; progress is saved in your browser.
A practical sequence is: syntax → types → methods/interfaces →
errors/resources → concurrency → HTTP/data → testing/tooling →
production architecture.
Every example favors idiomatic, current Go. The ecosystem
recommendations are choices, not mandatory dependencies. Start
with the standard library and add packages when they remove
meaningful boilerplate or provide a capability you would
otherwise implement poorly.
roadmaplearning
Start
Install, verify, and create a module
A module is the dependency and versioning unit. A package is a
directory of Go files compiled together.
Common commands: • go version: verify the selected toolchain. •
go env: inspect environment and module settings. • go mod init:
create go.mod. • go run .: compile and run the current main
package. • go build ./...: build every package below the module.
• go test ./...: test every package. • go install
package@version: install a command without changing your module.
Go 1.21+ can select/download a suitable toolchain based on the
go and toolchain lines in go.mod.
mkdir inventory && cd inventory
go mod init example.com/inventory
go run .
go test ./...
go fmt ./...
go vet ./...
modulestoolchaincommands
Start
Project and package layout
Prefer simple, domain-oriented packages. Add layers only when the
project actually needs them.
Typical service: cmd/api/main.go — composition root and process
lifecycle. internal/order — domain/application code private to
the module. internal/httpapi — HTTP adapters. internal/postgres
— persistence adapter. migrations — SQL migrations. web/dist —
built frontend. api — OpenAPI/protobuf definitions.
There is no required src directory. The internal directory has
compiler-enforced import restrictions. Avoid generic dumping
grounds named utils, common, or helpers.
Go favors declarations with useful zero values. Package
initialization runs before main, but explicit construction is
easier to test.
var declares a value and receives its zero value unless
initialized. := is shorthand inside functions and requires at
least one new variable. const values are compile-time values and
can be untyped. iota creates successive integer constants.
Zero values: numeric 0, false, "", nil for
pointers/slices/maps/channels/functions/interfaces, and
recursively zeroed structs.
Go does not perform most implicit numeric conversions. Convert
deliberately and watch for overflow or truncation.
Integers: int/uint (machine word), fixed-width int8…uint64,
uintptr. Floats: float32/float64. Complex: complex64/complex128.
Aliases: byte = uint8, rune = int32. Strings are immutable byte
sequences, conventionally UTF-8 but not guaranteed valid UTF-8.
Conversions use T(x); they are not runtime type assertions.
var i int = 42
f := float64(i)
b := byte(255)
r := rune('界')
s := "Go 世界"
fmt.Println(len(s)) // bytes
fmt.Println(utf8.RuneCountInString(s)) // Unicode code points
fmt.Println(f, b, r)
typesconversionunicode
Language
Strings, bytes, runes, and UTF-8
Indexing a string returns a byte. Ranging over it decodes UTF-8
runes and reports byte offsets.
Use strings.Builder for incremental text, bytes.Buffer for bytes
plus io.Reader/io.Writer behavior, strconv for
parsing/formatting, and unicode/utf8 when validating or decoding
manually. Converting string ↔ []byte allocates in normal code.
s := "A界"
fmt.Println(len(s)) // 4 bytes
for byteIndex, r := range s {
fmt.Printf("%d: %c\n", byteIndex, r)
}
var b strings.Builder
b.Grow(32)
b.WriteString("hello")
b.WriteByte(' ')
b.WriteString("world")
result := b.String()
stringsbytesrunesUTF-8
Language
Arrays, slices, capacity, and append
An array owns fixed-size storage. A slice is a small descriptor
pointing to an array: pointer, length, and capacity.
append may reuse the backing array or allocate a new one.
Therefore, append's return value must be assigned.
Sub-slices can keep a large backing array alive; copy out a
small retained portion when necessary. Use full slice
expressions a[low:high:max] to limit capacity.
A nil slice and empty slice both have length zero, but can
encode differently in JSON.
a := [4]int{10, 20, 30, 40}
s := a[1:3] // len=2 cap=3
s = append(s, 99) // may modify a[3]
dst := make([]int, len(s))
copy(dst, s)
limited := a[1:3:3] // cap=2; append must allocate
limited = append(limited, 77)
arraysliceappendcapacity
Language
Maps and sets
Maps are reference-like hash tables. Reading a missing key returns
the value type's zero value; use the comma-ok form to
distinguish absence.
A nil map can be read but not written. Map iteration order is
unspecified. Concurrent read/write access requires
synchronization. A set is commonly map[T]struct{}.
Delete is safe for missing keys. clear(map) removes all entries
in modern Go.
Go has one loop keyword: for. Conditions have no parentheses.
Initializers can scope temporary values.
range works with arrays, slices, strings, maps, channels,
integers, and iterator functions supported by current Go. Be
aware whether you need the index, a copy of the value, or the
address of an actual element.
switch breaks automatically; fallthrough is explicit. Type
switches inspect interface dynamic types.
if n, err := strconv.Atoi(input); err != nil {
return fmt.Errorf("parse count: %w", err)
} else if n < 0 {
return errors.New("count must be non-negative")
}
for i := range 5 {
fmt.Println(i)
}
switch v := anyValue.(type) {
case string:
fmt.Println(strings.ToUpper(v))
case int:
fmt.Println(v * 2)
default:
fmt.Printf("%T\n", v)
}
ifforrangeswitch
Language
Functions, multiple returns, variadics, closures
Functions are values. Multiple returns make result-plus-error
idiomatic. Closures capture variables, not frozen values.
Named returns can help tiny functions but often reduce clarity
in larger ones. Variadic arguments arrive as a slice. Pass an
existing slice with values....
Closures are useful for handlers and options; be careful when
captured state is used concurrently.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func sum(values ...int) int {
total := 0
for _, v := range values {
total += v
}
return total
}
nums := []int{1, 2, 3}
fmt.Println(sum(nums...))
functionsclosuresvariadic
Language
Pointers and value semantics
Go passes every argument by value. Passing a pointer copies the
pointer, allowing the callee to modify the pointed-to value.
Use pointers when mutation, identity, avoiding a meaningful
copy, or representing optionality is required. Do not use
pointers merely because a struct is “large” without measurement.
new(T) allocates a zero T and returns *T; composite literals are
more common.
Go has no pointer arithmetic outside unsafe.
type Counter struct{ N int }
func Increment(c *Counter) {
if c == nil {
return
}
c.N++
}
c := Counter{}
Increment(&c)
fmt.Println(c.N)
pointersvalue semantics
Language
Structs, embedding, tags, and composition
Struct embedding promotes fields and methods; it is composition,
not inheritance.
Use named fields when clarity matters. Tags are string metadata
interpreted by packages such as encoding/json and validation
libraries. Exported identifiers begin with an uppercase letter.
Embedding an interface can define a larger interface; embedding
a concrete value reuses behavior but also exposes promoted
methods, so use it intentionally.
type Address struct {
City string `json:"city"`
}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Address Address `json:"address"`
}
type TimedUser struct {
User
CreatedAt time.Time `json:"created_at"`
}
structembeddingtagscomposition
Language
Methods, receivers, and method sets
A method belongs to a named receiver type. Pointer receivers can
mutate and avoid copying; value receivers operate on a copy.
Choose one receiver style consistently for a type. Use pointer
receivers if any method needs one, the type contains
synchronization primitives, or copying is unsafe/expensive.
Method sets matter for interface satisfaction: T has
value-receiver methods; *T has both value- and pointer-receiver
methods. The compiler often inserts & or * for addressable
direct calls, but interface assignment follows method-set rules.
type Account struct{ Balance int64 }
func (a Account) Empty() bool { return a.Balance == 0 }
func (a *Account) Deposit(cents int64) {
a.Balance += cents
}
type Depositor interface {
Deposit(int64)
}
var d Depositor = &Account{} // *Account satisfies it
methodsreceivermethod set
Language
Interfaces and structural satisfaction
A type satisfies an interface implicitly by having its methods.
Keep interfaces small and define them near the consumer.
Accept interfaces when multiple implementations or test
boundaries are useful; return concrete types by default. The
empty interface is any. Type assertions recover a dynamic value.
Interface values contain a dynamic type and dynamic value. An
interface holding a typed nil pointer is itself non-nil—the
classic nil-interface trap.
type Store interface {
Get(ctx context.Context, id string) (Item, error)
}
func Load(ctx context.Context, s Store, id string) (Item, error) {
return s.Get(ctx, id)
}
var p *bytes.Buffer = nil
var w io.Writer = p
fmt.Println(w == nil) // false: dynamic type is *bytes.Buffer
interfacestructural typingnil trap
Language
Generics: constraints, inference, and useful patterns
Generics parameterize functions and types over sets of types.
Prefer ordinary interfaces when behavior—not representation—is the
abstraction.
A constraint is an interface used at compile time. ~T admits
types whose underlying type is T. comparable permits == and map
keys. Type inference often removes explicit type arguments.
Good uses: containers, algorithms, typed helpers. Poor uses:
hiding domain distinctions or replacing simple interfaces with
elaborate constraints.
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
names := Set[string]{}
names.Add("gopher")
fmt.Println(Min(4, 9))
genericsconstraintstype inference
Language
Iterators and sequence-style APIs
Modern Go supports range-over-function iterator patterns through
iter.Seq and iter.Seq2.
An iterator calls a yield function until the sequence ends or
yield returns false. This allows lazy traversal while retaining
familiar range syntax. Keep ownership and cleanup obvious; for
I/O, document how iteration errors are surfaced.
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i >= 0; i-- {
if !yield(i) {
return
}
}
}
}
for n := range Countdown(3) {
fmt.Println(n)
}
iteratorsiter.Seqrange function
Language
Error values, wrapping, joining, and matching
Errors are ordinary values. Add context while preserving the cause
with %w.
Use errors.Is for sentinel/cause matching and errors.As for
typed errors. Avoid string matching. errors.Join combines
independent failures. Sentinel errors are useful when callers
need a stable category; custom types carry structured details.
Handle an error once: return it, translate it, or log at a
process boundary—not all three at every layer.
var ErrNotFound = errors.New("not found")
func Load(id string) error {
err := query(id)
if err != nil {
return fmt.Errorf("load item %q: %w", id, err)
}
return nil
}
if err := Load("42"); errors.Is(err, ErrNotFound) {
// map to HTTP 404
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}
errorswrappingerrors.Iserrors.As
Language
defer, panic, recover, and resource lifetime
defer schedules a call for the surrounding function's return,
in LIFO order. Use it immediately after acquiring a resource.
Deferred arguments are evaluated when defer executes; deferred
closures read captured variables later. panic is for
unrecoverable programmer/runtime conditions, not routine
validation. recover works only inside a deferred function in the
panicking goroutine.
Servers commonly recover at the request boundary so one bad
request does not terminate the process, while still logging the
stack.
Concurrency structures independent tasks. Parallelism executes
work simultaneously. Multiprocessing uses multiple OS processes.
Goroutines are user-space scheduled tasks multiplexed over OS
threads. They can run in parallel when GOMAXPROCS allows
multiple CPUs. Go programs are normally multithreaded without
you creating threads directly.
Use multiple processes for isolation, independent scaling,
privilege boundaries, or separate failure domains. Communicate
via HTTP/gRPC, queues, pipes, sockets, or shared external
storage—not Go channels, which are in-process.
func main() {
fmt.Println("logical CPUs:", runtime.NumCPU())
fmt.Println("parallelism:", runtime.GOMAXPROCS(0))
cmd := exec.CommandContext(context.Background(), "worker", "--job=42")
output, err := cmd.CombinedOutput() // a separate process
fmt.Println(string(output), err)
}
parallelismmultiprocessingthreadsscheduler
Concurrency
Goroutines and lifecycle ownership
The go statement starts a goroutine. Every goroutine should have a
clear termination condition and owner.
A goroutine leak is a task blocked forever on I/O, channel
operations, locks, or forgotten timers. Pass cancellation, close
owned resources, and bound concurrency. A process exits when
main returns; it does not wait for other goroutines.
Do not “fire and forget” important work. Tie it to a supervised
worker, durable queue, or shutdown protocol.
func poll(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
refresh()
}
}
}
ctx, cancel := context.WithCancel(context.Background())
go poll(ctx, time.Second)
cancel()
goroutinelifecycleleakcancellation
Concurrency
Channels: unbuffered, buffered, close, and direction
Channels synchronize and transfer values between goroutines. An
unbuffered send pairs with a receive; a buffered send waits only
when the buffer is full.
Only the sending/owning side should close. Closing means no more
values will be sent; it is not required for garbage collection.
Receiving from a closed channel yields buffered values, then the
zero value with ok=false. Sending to or closing a closed channel
panics.
Channel directions document APIs: chan<- T send-only,
<-chan T receive-only.
func producer(ctx context.Context) <-chan int {
out := make(chan int, 2)
go func() {
defer close(out)
for _, n := range []int{1, 2, 3} {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
for n := range producer(context.Background()) {
fmt.Println(n)
}
channelbufferclosedirection
Concurrency
select, timeouts, cancellation, and nil channels
select waits until a communication case can proceed. If several
are ready, one is chosen pseudo-randomly.
default makes select non-blocking and can accidentally create a
busy loop. time.After is convenient for one-off waits; reusable
loops should use Timer/Ticker and stop them. A nil channel
blocks forever, which can be useful for dynamically disabling a
select case.
timer := time.NewTimer(500 * time.Millisecond)
defer timer.Stop()
select {
case result := <-results:
fmt.Println(result)
case <-timer.C:
return errors.New("timed out")
case <-ctx.Done():
return ctx.Err()
}
selecttimeoutnil channeltimer
Concurrency
WaitGroup and structured waiting
sync.WaitGroup waits for a known group of goroutines. Add before
starting the goroutine; each worker calls Done.
A WaitGroup does not propagate errors or cancellation. For
groups of tasks that can fail, errgroup is often a better fit.
Never copy a WaitGroup after first use.
var wg sync.WaitGroup
for _, job := range jobs {
job := job
wg.Add(1)
go func() {
defer wg.Done()
process(job)
}()
}
wg.Wait()
WaitGroupjoinstructured concurrency
Concurrency
errgroup: cancellation plus first error
golang.org/x/sync/errgroup coordinates related goroutines and
cancels derived work when one returns an error.
SetLimit bounds active goroutines. Go waits for completion and
returns the first non-nil error. Use the group-derived context
for downstream operations; do not start unrelated lifetime work
in a request-scoped group.
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, id := range ids {
id := id
g.Go(func() error {
return fetchAndStore(ctx, id)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("sync records: %w", err)
}
errgroupcancellationerrorsbounded concurrency
Concurrency
Worker pools, semaphores, and backpressure
Bound concurrency when work consumes database connections, file
descriptors, memory, CPU, or remote-service capacity.
A worker pool uses fixed workers reading a queue. A semaphore
limits concurrent entries without prescribing a persistent pool.
Backpressure means producers eventually wait or reject work
rather than allowing an unbounded queue.
Choose queue size from service objectives, not guesswork. Expose
saturation metrics.
jobs := make(chan Job, 100)
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
handle(job)
}
}()
}
for _, job := range input {
jobs <- job // backpressure when queue is full
}
close(jobs)
wg.Wait()
Cancellation must reach every stage, especially send operations,
or downstream early exit can leak upstream goroutines. For
simple finite data, a bounded errgroup over a slice is often
easier than a channel pipeline.
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
pipelinefan-outfan-inleak prevention
Concurrency
Mutex, RWMutex, Cond, Once, and Pool
Use locks to protect invariants over shared memory. Keep critical
sections small and never copy a used lock.
Mutex is the default. RWMutex helps only for measured read-heavy
contention; it adds complexity. Once performs one successful
call attempt (a panic still marks it done). Cond coordinates
state changes when channels are awkward. Pool reuses temporary
objects and may discard them at any time—never store required
state there.
Document which fields a mutex protects.
type Cache struct {
mu sync.RWMutex
m map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
func (c *Cache) Put(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k] = v
}
mutexRWMutexOncePoolCond
Concurrency
Atomics and lock-free state
sync/atomic provides indivisible operations and memory-ordering
guarantees. Prefer locks unless a tiny independent state or
measured hot path justifies atomics.
Typed atomics such as atomic.Int64, Bool, Pointer[T], and Value
reduce misuse. Multiple atomic fields do not automatically form
one invariant. CompareAndSwap enables state transitions but
lock-free algorithms are subtle.
Use atomic counters for metrics and atomic.Value/Pointer for
read-mostly snapshots.
type ConfigStore struct {
current atomic.Pointer[Config]
}
func (s *ConfigStore) Load() *Config {
return s.current.Load()
}
func (s *ConfigStore) Replace(c *Config) {
clone := *c
s.current.Store(&clone)
}
var requests atomic.Uint64
requests.Add(1)
atomicCASlock-freememory ordering
Concurrency
Go memory model and happens-before
Race-free Go programs have predictable sequentially consistent
behavior. Synchronization establishes ordering between goroutines.
Examples of synchronizing events include mutex unlock→later
lock, channel send→corresponding receive, channel close→receive
observing closure, and atomic operations under their specified
ordering. Starting a goroutine does not make later
unsynchronized writes safely visible.
Do not use sleep as synchronization. Protect shared mutable data
or transfer ownership.
var (
mu sync.Mutex
ready bool
data string
)
go func() {
mu.Lock()
data = "published"
ready = true
mu.Unlock()
}()
mu.Lock()
if ready {
fmt.Println(data)
}
mu.Unlock()
memory modelhappens-beforevisibilityrace
Concurrency
Race detector and common race patterns
A data race occurs when concurrent accesses touch the same memory,
at least one is a write, and there is no synchronization.
Run go test -race ./... and exercise realistic paths. It detects
races that execute; it is not a proof of absence. Common races:
maps, counters, captured variables, lazy initialization, and
shared response/request objects.
Fix by confinement, channels, locks, atomics, immutability, or
copying—not by adding sleeps.
go test -race ./...
go run -race .
go test -race -count=10 ./internal/...
// Safe counter:
var mu sync.Mutex
count := 0
mu.Lock()
count++
mu.Unlock()
race detectorthread safetydata race
Concurrency
Scheduler, OS threads, blocking calls, and GOMAXPROCS
The runtime schedules goroutines over OS threads using a G-M-P
model: goroutines, machine threads, and logical processors.
Blocking network I/O integrates with the runtime poller.
Blocking syscalls or cgo calls may occupy threads; the runtime
can create others. GOMAXPROCS controls how many logical
processors execute Go code simultaneously, not the number of
goroutines or threads.
runtime.LockOSThread is rare: UI/event loops, thread-local
foreign APIs, or namespace/credential operations tied to a
thread.
Context: deadlines, cancellation, and request scope
context.Context carries cancellation, deadlines, and
request-scoped metadata across API boundaries.
Pass ctx as the first parameter; do not store it in structs for
normal request work. Always call the returned cancel function.
Values are for cross-cutting metadata such as trace IDs, not
optional function parameters. Libraries should accept
cancellation and return promptly.
Background starts a root; TODO marks an unresolved choice.
io.Reader and io.Writer are Go's central streaming
interfaces. Compose them instead of loading entire payloads.
Useful tools: io.Copy, CopyN, LimitReader, TeeReader,
MultiReader, MultiWriter, Pipe, ReadAll. bufio adds buffering,
Scanner, and tokenization. Scanner has a default token-size
limit; increase Buffer or use Reader for large records.
Always limit untrusted body sizes.
limited := io.LimitReader(resp.Body, 10<<20) // 10 MiB
hash := sha256.New()
n, err := io.Copy(io.MultiWriter(dst, hash), limited)
if err != nil {
return err
}
fmt.Printf("copied=%d sha256=%x\n", n, hash.Sum(nil))
io.Readerio.Writerstreamingbuffering
Standard Library
Files, paths, and filesystem APIs
Use os for files/process environment, filepath for OS paths, path
for slash-separated URL-style paths, and io/fs for abstract
filesystems.
Prefer os.ReadFile/WriteFile for small bounded files and
streaming for large ones. Use fs.ValidPath conventions for
embedded/sub filesystems. Atomic replacement usually means write
a temporary file in the same directory, sync as required, then
rename.
encoding/json is ubiquitous but reflection-based. Model wire
formats explicitly and validate after decoding.
Use Decoder for streams and DisallowUnknownFields where strict
contracts help. Decoder.Decode permits another JSON value unless
you explicitly check EOF. Use json.RawMessage for delayed
decoding. time.Time uses RFC3339 JSON by default.
CSV, XML, base64, hex, binary, pem, and gob cover common
formats. gob is Go-specific and not a public cross-language
protocol.
type CreateUser struct {
Email string `json:"email"`
Age int `json:"age"`
}
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
dec.DisallowUnknownFields()
var in CreateUser
if err := dec.Decode(&in); err != nil {
return fmt.Errorf("decode request: %w", err)
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
return errors.New("request must contain one JSON value")
}
jsonencodingdecodervalidation
Standard Library
Time, timers, tickers, and monotonic time
time.Time can carry wall-clock and monotonic readings. Use
durations for elapsed time and explicit locations for civil time.
time.Since(start) safely uses monotonic data when available.
Store timestamps in UTC and convert for presentation. Stop
tickers. Timer reset/drain details matter in reusable timer
loops.
Avoid comparing formatted strings. For database/API boundaries,
define precision and timezone expectations.
Use the smallest tool that fits. Prefix/suffix/split operations
are clearer and faster than regex for simple parsing.
regexp uses RE2-style linear-time matching and excludes features
such as backreferences. strconv handles numeric and boolean
parsing/formatting. strings and bytes provide search, trimming,
mapping, replacement, builders, and readers.
log/slog is the standard structured logging API. Emit stable
machine-queryable fields rather than interpolating everything into
text.
Create a logger at startup and inject it or derive request
loggers with With. Avoid secrets and high-cardinality noise. Use
context-aware methods when handlers need context. Handlers
control JSON/text output and filtering.
Do not log the same error at every layer; log where ownership
crosses a boundary.
Use crypto/rand for security-sensitive randomness; math/rand/v2 is
for simulation and non-secret randomized behavior.
Use password-hashing algorithms from maintained packages such as
bcrypt, scrypt, or Argon2 rather than a fast hash. Compare
authentication MACs with constant-time functions. Use crypto/tls
defaults unless protocol requirements demand careful
configuration.
Never invent encryption protocols or store plaintext passwords.
tokenBytes := make([]byte, 32)
if _, err := cryptorand.Read(tokenBytes); err != nil {
return err
}
token := base64.RawURLEncoding.EncodeToString(tokenBytes)
sum := sha256.Sum256(data) // integrity/fingerprint, not password hashing
fmt.Println(token, hex.EncodeToString(sum[:]))
cryptorandomhashsecrets
Standard Library
Reflection and unsafe
reflect enables runtime type inspection; unsafe bypasses Go's
type and memory safety.
Reflection powers serializers, validators, and dependency tools
but makes code harder to understand and moves errors to runtime.
Prefer generics, interfaces, and generated code when possible.
unsafe may be necessary for systems integration or measured
low-level optimization. Keep it isolated, tested across
architectures, and documented with lifetime/alignment
assumptions.
func fieldNames(v any) []string {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
names := make([]string, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
names = append(names, t.Field(i).Name)
}
return names
}
reflectunsaferuntime types
Web & APIs
net/http server fundamentals
The standard HTTP server is production-capable when configured
carefully. Handlers are called concurrently.
Set ReadHeaderTimeout, IdleTimeout, and sensible write/read
policies. Avoid http.ListenAndServe defaults for internet-facing
services. Request bodies must be bounded and closed as
appropriate. ResponseWriter usually must not be used after the
handler returns.
Go 1.22+ ServeMux supports method-aware patterns and path
parameters.
Standard middleware wraps http.Handler. Keep middleware focused on
transport concerns.
Typical middleware: request IDs, access logging, panic recovery,
authentication, authorization, CORS, compression, tracing, rate
limiting, body limits. Put domain validation and business rules
in services, not middleware.
Order matters: recovery should be outside code that might panic;
authentication must precede authorization.
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = newID()
}
ctx := context.WithValue(r.Context(), requestIDKey{}, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
handler := requestID(logging(auth(mux)))
middlewarehttp.Handlercomposition
Web & APIs
HTTP clients, transports, retries, and connection reuse
Reuse http.Client and Transport; they are safe for concurrent use
and manage connection pools.
Always set request deadlines through context or client timeout.
Read and close response bodies so connections can be reused.
Retry only safe/idempotent operations or operations protected
with idempotency keys, and use bounded exponential backoff with
jitter.
The default client has no overall timeout. Customize Transport
carefully rather than creating one per request.
Choose a protocol from communication shape and operational
constraints.
SSE: server→browser events over HTTP, automatic reconnect
semantics, simple proxies. WebSocket: full duplex, custom
framing/application protocol. HTTP streaming: files, chunks,
NDJSON. gRPC: typed service contracts, streaming, strong
internal-service tooling; browser use often needs gRPC-Web or a
gateway. Connect: RPC over familiar HTTP semantics with broad
client support.
For all streaming: propagate cancellation, handle slow
consumers, bound queues, send heartbeats when needed, and define
reconnection/resume semantics.
func events(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", 500)
return
}
w.Header().Set("Content-Type", "text/event-stream")
for {
select {
case <-r.Context().Done():
return
case event := <-updates:
fmt.Fprintf(w, "event: update\ndata: %s\n\n", event)
flusher.Flush()
}
}
}
SSEWebSocketgRPCstreaming
Web & APIs
Templates, static files, and frontend bundling
Go can render server HTML or serve a separately built SPA.
go:embed can package assets into one binary.
Server-rendered: html/template automatically context-escapes;
templ offers typed generated components; htmx can add
interaction with little JS. SPA: build React/Vue/Svelte with
Vite or the framework tool, then embed dist/ or serve it from a
CDN/object store. For development, run frontend and Go dev
servers separately with a proxy. For production, choose embedded
assets for simple deployment or CDN for independent releases and
global caching.
//go:embed web/dist/*
var assets embed.FS
func staticHandler() http.Handler {
dist, err := fs.Sub(assets, "web/dist")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(dist))
}
// Build:
// cd web && npm ci && npm run build
// go build ./cmd/api
embedfrontendVitetemplatesSPA
Web & APIs
Authentication and authorization
Authentication proves identity; authorization decides whether that
identity may perform an action.
Browser applications commonly use secure, HttpOnly, SameSite
cookies backed by a server session or carefully designed tokens.
OAuth 2.0 is authorization delegation; OpenID Connect adds
identity. Validate issuer, audience, signature algorithm,
expiry, nonce/state, and key rotation.
Do not implement password storage, OAuth, JWT verification, or
CSRF protection from scratch. Authorization belongs near use
cases/resources, not only at the router.
Validation, API errors, CORS, CSRF, and rate limits
Treat transport decoding, syntactic validation, domain validation,
and authorization as separate steps.
Return consistent machine-readable errors with safe messages.
CORS is a browser policy, not authentication.
Cookie-authenticated state changes need CSRF defenses. Rate
limits should identify the correct principal and account for
proxies.
Popular validation: go-playground/validator for tags;
hand-written validation for explicit domain rules. Consider RFC
9457 Problem Details for error shape.
type Problem struct {
Type string `json:"type,omitempty"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
}
func (in CreateUser) Validate() error {
if !strings.Contains(in.Email, "@") {
return errors.New("invalid email")
}
return nil
}
validationCORSCSRFrate limiterrors
Data
database/sql connections, pools, and cancellation
sql.DB is a concurrent-safe connection pool, not one connection.
Open usually does not verify connectivity; PingContext does.
Configure pool limits from database capacity and workload.
Always close Rows, inspect rows.Err, and use Context methods.
Transactions are bound to one connection. Prepared statements
can help but drivers/pools affect their behavior.
Never concatenate untrusted SQL values; use parameters.
db, err := sql.Open("pgx", dsn)
if err != nil { return err }
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
if err := db.PingContext(ctx); err != nil {
return err
}
rows, err := db.QueryContext(ctx, `SELECT id, name FROM users WHERE active = $1`, true)
if err != nil { return err }
defer rows.Close()
database/sqlpoolSQLcancellation
Data
Transactions and consistency
A transaction groups operations with atomicity and isolation
guarantees provided by the database.
Always defer rollback; a rollback after commit is harmless for
database/sql. Keep transactions short, avoid network calls
inside them, and understand isolation/anomaly requirements.
Retrying serialization failures means retrying the whole
transaction function.
For distributed workflows, consider outbox/inbox, idempotency,
sagas, and durable queues rather than pretending a local DB
transaction covers remote systems.
func transfer(ctx context.Context, db *sql.DB, from, to int64, cents int64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx,
`UPDATE account SET balance = balance - $1 WHERE id = $2`,
cents, from); err != nil {
return err
}
if _, err := tx.ExecContext(ctx,
`UPDATE account SET balance = balance + $1 WHERE id = $2`,
cents, to); err != nil {
return err
}
return tx.Commit()
}
transactionconsistencyisolationoutbox
Data
PostgreSQL: pgx, sqlc, ORM, and migrations
For PostgreSQL, pgx exposes PostgreSQL-specific capabilities and
can also back database/sql. sqlc generates typed Go code from SQL.
Common approaches: • pgx + sqlc: explicit SQL, strong types,
generated boilerplate; excellent default when the team knows
SQL. • database/sql + driver/sqlx: portable standard abstraction
plus conveniences. • GORM/Ent/Bun: ORM or generated entity model
when rapid CRUD/model tooling outweighs abstraction cost. •
Squirrel/goqu: query builders for highly dynamic SQL.
Migrations: goose, golang-migrate, Atlas, or a deployment
platform's migration step. Never run risky schema changes
blindly on every application startup.
-- name: GetUser :one
SELECT id, email, created_at
FROM users
WHERE id = $1;
-- sqlc generates a typed method similar to:
-- func (q *Queries) GetUser(ctx context.Context, id int64) (User, error)
-- Useful:
-- github.com/jackc/pgx/v5
-- sqlc.dev
-- github.com/pressly/goose/v3
pgxsqlcORMmigrationPostgreSQL
Data
Caching, Redis, and invalidation
Caching trades consistency and complexity for latency or load
reduction. Invalidation and stampede control are the hard parts.
Patterns: cache-aside, write-through, request memoization, TTL,
versioned keys, negative caching, singleflight. Cache only when
measurements show value. Include tenant/security boundaries in
keys. Treat Redis as a remote system with timeouts and failure
policies.
Popular clients include go-redis and rueidis.
x/sync/singleflight suppresses duplicate concurrent loads in one
process.
Use durable brokers when work must survive process crashes or
cross service boundaries.
Options depend on infrastructure: Kafka, NATS JetStream,
RabbitMQ, cloud queues/pub-sub, Redis-backed job systems, or a
transactional database outbox. Design for at-least-once
delivery: handlers must be idempotent, acknowledge only after
success, and send poison messages to a dead-letter path after
bounded retries.
Carry event schema versions and stable IDs; monitor lag,
retries, and age.
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Version int `json:"version"`
Occurred time.Time `json:"occurred_at"`
Payload json.RawMessage `json:"payload"`
}
// Consumer:
// 1. begin transaction
// 2. reject already-processed Event.ID
// 3. apply changes + record inbox ID
// 4. commit
// 5. acknowledge message
queueeventsidempotencyoutboxKafkaNATS
Testing
Unit tests, table tests, subtests, and helpers
The standard testing package is enough for most tests. Table tests
make cases explicit and compact.
Use t.Helper in assertion helpers, t.Cleanup for resources, and
t.Run for named subtests. Tests in package foo can access
internals; package foo_test tests the public API. Avoid brittle
tests of implementation details.
Run tests with randomized map iteration and consider
-shuffle=on.
Use httptest for handlers/servers. Integration tests should
exercise real boundaries where fakes would hide incompatibilities.
httptest.NewRecorder tests a handler in-process;
httptest.NewServer tests an HTTP client. Testcontainers-go can
launch real databases/brokers. Golden files work for stable
large outputs; support an explicit update flag and review diffs.
Use build tags or test flags to separate expensive suites, but
keep CI coverage clear.
Go's toolchain includes coverage-guided fuzzing and
benchmarks.
Fuzz parsers, validators, codecs, and boundary code. Seed
representative inputs, then assert invariants and prevent
excessive allocation/time. Benchmarks need representative setup,
b.ResetTimer when setup is excluded, and b.ReportAllocs. Compare
changes with benchstat rather than one noisy run.
Example functions can be compiled tests and documentation.
Prefer small interfaces and hand-written fakes for domain tests.
Generate mocks when interaction verification is genuinely useful.
A fake has working simplified behavior; a stub returns fixed
responses; a mock records/validates calls. Excessive mocks
couple tests to call sequences. Popular tools: GoMock, mockery,
testify/mock. Standard library fakes such as httptest servers
often test more realistically.
Inject clocks, random sources, repositories, and clients at
boundaries—not every function.
type FakeStore struct {
Items map[string]Item
Err error
}
func (f *FakeStore) Get(ctx context.Context, id string) (Item, error) {
if f.Err != nil { return Item{}, f.Err }
item, ok := f.Items[id]
if !ok { return Item{}, ErrNotFound }
return item, nil
}
mockfakedependency injectiontestability
Tooling
Modules, versions, workspaces, and private dependencies
go.mod records module path, language/toolchain requirements, and
dependencies. go.sum authenticates downloaded module content.
Use minimal version selection semantics. go mod tidy adds needed
and removes unused requirements. replace is useful locally but
should not casually leak into released libraries. go.work
coordinates multiple local modules without editing each go.mod.
For private modules configure GOPRIVATE and credentials; avoid
broad disabling of proxy/checksum protections.
go mod init example.com/app
go get github.com/go-chi/chi/v5@latest
go mod tidy
go mod verify
go list -m -u all
go work init ./services/api ./libs/domain
go work use ./libs/telemetry
# Private modules:
go env -w GOPRIVATE=github.com/my-company/*
go.modgo.sumworkspacedependencies
Tooling
Builds, tags, cross-compilation, cgo, and linking
Go usually produces a single native executable. GOOS/GOARCH select
targets; build tags select files.
Pure-Go cross-compilation is straightforward. cgo integrates C
but complicates cross-compilation, static linking, memory
ownership, callbacks, and thread behavior. Prefer pure Go unless
a native dependency is necessary.
Use -trimpath for reproducibility/privacy, ldflags -X for build
metadata, and buildvcs metadata where appropriate.
# Linux AMD64 from another OS:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o bin/api ./cmd/api
# Build-tagged feature:
go build -tags=enterprise ./cmd/api
# Version injection:
go build -ldflags="-X main.version=v1.2.3" ./cmd/api
buildcross compilecgobuild tagslinker
Tooling
Formatting, vetting, linting, and code generation
gofmt is non-negotiable. go vet finds suspicious constructs.
Staticcheck and golangci-lint aggregate deeper checks.
Use gofmt/goimports in the editor. Keep lint configuration
reviewed and version-pinned; enabling every linter creates
noise. go generate runs explicit source-generation commands but
is not run automatically by build/test.
Generated files should declare that they are generated and be
reproducible.
gofmt -w .
goimports -w .
go vet ./...
staticcheck ./...
golangci-lint run
//go:generate go tool stringer -type=Status
type Status int
gofmtgo vetstaticcheckgolangci-lintgenerate
Tooling
Profiling, tracing, escape analysis, and diagnostics
Measure before optimizing. Go includes CPU, heap, goroutine,
block, mutex, execution trace, and runtime metrics.
Expose net/http/pprof only on a protected internal listener. Use
go tool pprof for profiles and go tool trace for
scheduler/latency analysis. Escape analysis reports why values
move to the heap. GODEBUG exposes runtime diagnostics; use
documented settings carefully.
Memory profiles show allocation/live-object behavior—not simply
“memory leaks.” Goroutine profiles often reveal leaks.
go test -bench=. -benchmem -cpuprofile=cpu.out -memprofile=mem.out ./...
go tool pprof -http=:0 cpu.out
go test -trace=trace.out ./...
go tool trace trace.out
go build -gcflags="all=-m=2" ./cmd/api
go test -run=NONE -bench=. -blockprofile=block.out ./...
pproftraceescape analysisperformancediagnostics
Tooling
Security and supply-chain checks
Keep Go and dependencies patched, minimize dependency surface, and
verify what enters builds.
Use govulncheck to find known vulnerabilities reachable from
your code. Review module provenance, licenses, release activity,
and transitive dependencies. Pin CI tool versions and protect
build credentials. Generate SBOMs and sign artifacts where your
deployment environment requires it.
Treat deserialization, command execution, templates, SQL, paths,
SSRF, redirects, and archive extraction as security boundaries.
govulncheck ./...
go mod verify
go list -m -json all
go version -m ./bin/api
# Also common in CI:
# go test -race ./...
# staticcheck ./...
# gosec ./...
securitygovulnchecksupply chainSBOM
Ecosystem
HTTP routers and frameworks: decision guide
Start with net/http ServeMux. Add a router/framework for
middleware ecosystems, binding conventions, or team familiarity.
Recommended categories: • net/http ServeMux: minimal
dependencies, current method/path routing, maximum standard
compatibility. • chi: idiomatic net/http composition, subrouters
and middleware; strong standard-library-first choice. • Gin:
batteries-included request context, binding, middleware
ecosystem; common for API teams. • Echo: similarly full-featured
with a conventional framework API. • Fiber: Express-like API
built on fasthttp; choose only when its non-net/http trade-offs
are understood. • Connect/gRPC: contract-first RPC rather than
REST routing.
Benchmark your workload; router microbenchmarks rarely determine
system throughput.
Configuration should be explicit, validated at startup, and
separable from secrets.
Standard library + env flags is often enough. Popular packages:
• caarlos0/env: typed environment decoding. • koanf: composable
sources with a small core. • Viper: broad formats/sources and
Cobra integration, but more global/magic behavior. • envconfig:
simple env-to-struct mapping.
Do not silently accept invalid defaults. Secrets should come
from environment/secret managers/files with restricted
permissions, never committed configuration.
The standard flag package is good for small commands. Cobra is
common for nested commands and rich UX.
Alternatives include urfave/cli and Kong. Separate command
parsing from application logic, return errors instead of calling
os.Exit deep inside code, and direct stdout/stderr
intentionally. Add shell completion only when useful.
For terminal UI, Bubble Tea is a popular model-update-view
framework.
Use logs for events, metrics for aggregate trends/alerts, and
traces for request paths across components.
OpenTelemetry is the vendor-neutral tracing/metrics API and SDK
ecosystem. Prometheus client_golang is common for pull-based
metrics. slog handles structured logs; bridges/exporters can
correlate trace IDs.
Instrument inbound/outbound HTTP, database calls, queues, and
critical domain operations. Control cardinality: never use user
IDs or raw URLs as metric labels.
Manual constructors are idiomatic and transparent. A composition
root wires concrete infrastructure to interfaces.
For large compile-time graphs, Google Wire generates wiring
code. Runtime containers such as Uber Fx/Dig can manage
lifecycle but add indirection. Do not introduce a container
merely to avoid writing constructors.
Functional options are useful for optional construction
parameters; avoid making required dependencies optional.
Schema-first tooling keeps clients and servers aligned but
introduces generation workflows.
REST: OpenAPI with oapi-codegen, ogen, or Goa. RPC: Protocol
Buffers with gRPC or Connect; Buf manages linting, breaking
checks, and generation. GraphQL: gqlgen is a common schema-first
server. SQL: sqlc generates query code. Mocks/enums:
mockery/GoMock/stringer.
Pin generators, run them reproducibly, and decide whether
generated output is committed.
# Example tool pattern in Go 1.24+ modules:
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
go tool oapi-codegen -config cfg.yaml api/openapi.yaml
# Protobuf ecosystem:
buf lint
buf breaking --against '.git#branch=main'
buf generate
OpenAPIoapi-codegenprotobufBufGraphQLcodegen
Ecosystem
Useful golang.org/x packages
The x repositories contain widely used packages maintained
alongside the Go project but versioned outside the standard
library.
SPA: Go JSON API + OIDC/session auth + Vite React/Vue/Svelte
build + embedded dist for simple deployments or CDN for
independent frontend releases.
# Lean service dependency sketch:
github.com/go-chi/chi/v5
github.com/jackc/pgx/v5
github.com/pressly/goose/v3
go.opentelemetry.io/otel
golang.org/x/sync
# Add dependencies only when used:
go get package@version
go mod tidy
stackarchitecturerecommendation
Production
Graceful startup, shutdown, and signal handling
A production process should fail fast on invalid configuration and
shut down in a bounded, ordered manner.
Use signal.NotifyContext for SIGINT/SIGTERM. Stop accepting
traffic, allow in-flight requests to finish, stop producers,
drain workers where safe, flush telemetry, and close pools. Give
shutdown a deadline and return non-zero on startup/runtime
failure.
Readiness should become false before shutdown begins in
orchestrated environments.
Health endpoints should reflect actionable process and dependency
states without creating extra load.
Liveness: is the process stuck and should it restart? Readiness:
should it receive traffic? Startup: has initialization
completed?
Use timeouts everywhere, retries selectively, circuit breaking
when it improves failure containment, bulkheads/concurrency
limits, and load shedding. Avoid retry storms: combine
deadlines, bounded attempts, backoff, jitter, and budgets.
mux.HandleFunc("GET /livez", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
if !ready.Load() {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
healthreadinessresilienceoverloadcircuit breaker
Production
Containers, deployment, and static binaries
Go binaries fit small container images, but operational
correctness matters more than image size.
Build in one stage and copy into a minimal runtime. Include CA
certificates and timezone data when needed, run as non-root, use
read-only filesystems where feasible, and expose no debug
endpoints publicly. Distroless/scratch require knowing which
runtime files your app uses.
Cross-compiled CGO-disabled binaries are easiest; cgo needs
compatible runtime libraries.
# syntax=docker/dockerfile:1
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
ENTRYPOINT ["/api"]
Slices/maps/pointers may allocate when values escape. Interfaces
and closures can contribute allocations but should be measured.
Preallocate when sizes are known, reuse buffers cautiously,
stream large data, batch I/O, reduce lock contention, and tune
pools against downstream capacity.
Do not sacrifice correctness/readability for speculative
micro-optimizations. Benchmark representative workloads and
validate with profiles.
items := make([]Item, 0, len(rows))
for _, row := range rows {
items = append(items, convert(row))
}
var buf bytes.Buffer
buf.Grow(estimatedSize)
enc := json.NewEncoder(&buf)
_ = enc.Encode(payload)
// Then confirm:
// go test -bench=. -benchmem
// go tool pprof
performanceallocationscontentionbenchmark
Production
Common Go pitfalls checklist
Most production bugs come from ownership, lifetimes, ignored
errors, and incorrect assumptions—not syntax.
Review: • goroutine has an exit path and bounded concurrency. •
response/request bodies, rows, files, timers, and tickers are
closed/stopped. • errors preserve causes and are not
double-logged. • no unsynchronized map/slice/shared state. •
loop captures and addresses refer to intended values. • nil
interface vs typed nil handled. • append result assigned;
backing-array aliasing understood. • HTTP clients/servers have
timeouts. • database rows.Err and transaction rollback checked.
• contexts propagate and cancel functions are called. • secrets
are not logged. • JSON/body size bounded. • map order is never
relied upon. • copies do not duplicate mutexes. • tests run with
race detector and critical integrations.
# High-value pre-merge commands:
go fmt ./...
go vet ./...
go test ./...
go test -race ./...
staticcheck ./...
govulncheck ./...
pitfallschecklistreview
Production
Idiomatic design principles
Go code scales through simplicity, explicit dependencies, small
interfaces, and clear ownership.
Prefer: • clarity over cleverness. • concrete types until
abstraction is needed. • errors as values with context. •
composition over inheritance-style frameworks. • standard
library compatibility. • synchronous code first; concurrency
only for a reason. • immutable snapshots or owned mutation. •
package APIs that make invalid use difficult. • boring
deployment and observable failure.
“Do not communicate by sharing memory; share memory by
communicating” is guidance, not a ban on mutexes. Choose the
primitive that makes ownership and invariants clearest.
type Repository interface {
Save(context.Context, Order) error
}
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
idiomsdesignownershipinterfaces
No topics match the current search and category filter.