Go Refresher
Go programming language — concurrency, interfaces, and systems programming quick reference
Table of Contents
0. Setup & Environment
Install Go via Homebrew
The most common way to install Go on macOS:
brew install go
go version # verify: go version go1.22.x darwin/arm64
Go installs to $(brew --prefix)/opt/go. The toolchain (go, gofmt, vet) is available immediately after install.
GOPATH and Modules
Go modules (introduced in 1.11, default since 1.16) replaced GOPATH-based development. You no longer need to keep projects under $GOPATH/src.
# Start a new module anywhere on your filesystem
mkdir myproject && cd myproject
go mod init myproject # creates go.mod
# go.mod pins the module path and Go version
# go.sum records exact dependency checksums
$HOME/go/bin to your shell profile so binaries installed with go install are accessible:
export PATH=$PATH:$HOME/go/bin # add to ~/.zshrc or ~/.bashrc
Editor Setup
VS Code (most popular, free) — install the official Go extension by the Go team. On first open it auto-installs:
gopls— language server (autocomplete, go-to-def, references)dlv(Delve) — debuggerstaticcheck— linter
GoLand by JetBrains — commercial IDE with deep Go support built-in (no extensions needed).
gofmt is the community-wide standard formatter — there are no tabs-vs-spaces debates in Go.
Quick Verify
Confirm your install end-to-end with a minimal module:
mkdir ~/go-refresher && cd ~/go-refresher
go mod init go-refresher
cat > main.go << 'EOF'
package main
import "fmt"
func main() { fmt.Println("Hello, Go!") }
EOF
go run main.go # Hello, Go!
go build . # produces ./go-refresher binary
Program Basics
Every Go program belongs to a package. The main package is the entry point; all other packages are libraries.
Packages and Imports
// Every file starts with a package declaration
package main
// Import stdlib packages by path
import (
"fmt"
"os"
"strings"
// Aliased import
str "strings"
// Blank import — only runs init(), side effects only
_ "image/png"
// Dot import — brings names into current scope (avoid in production)
. "math"
)
// Exported names start with an uppercase letter
// Unexported names start with lowercase — package-private
func main() {
fmt.Println("Hello, Go!")
_ = str.ToUpper("hello") // use alias
_ = Sqrt(4) // from dot import
}
public/private keywords.
Go Modules
# Initialize a new module
go mod init github.com/yourname/myproject
# Add a dependency (updates go.mod + go.sum)
go get github.com/some/[email protected]
# Tidy: remove unused deps, add missing ones
go mod tidy
# Vendor dependencies into ./vendor/
go mod vendor
# Show dependency graph
go mod graph
# Upgrade all direct deps to latest minor/patch
go get -u ./...
# Download modules to local cache
go mod download
// go.mod — the module manifest
module github.com/yourname/myproject
go 1.22
require (
github.com/some/package v1.2.3
golang.org/x/sync v0.6.0
)
// go.sum — cryptographic checksums, commit to source control
// github.com/some/package v1.2.3 h1:abc...
// github.com/some/package v1.2.3/go.mod h1:xyz...
Project Structure
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point (package main)
├── internal/ # Cannot be imported by external packages
│ ├── auth/
│ │ ├── auth.go
│ │ └── auth_test.go
│ └── db/
│ └── db.go
├── pkg/ # Public library code (importable externally)
│ └── httputil/
│ └── httputil.go
├── api/ # OpenAPI/proto specs, generated code
├── scripts/ # Build/deploy scripts
├── Makefile
├── go.mod
└── go.sum
Compilation and Running
# Run without building a binary (development)
go run ./cmd/myapp/
# Build a binary
go build -o bin/myapp ./cmd/myapp/
# Install binary to $GOPATH/bin (or $GOBIN)
go install ./cmd/myapp/
# Cross-compile: GOOS and GOARCH env vars
GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux ./cmd/myapp/
GOOS=darwin GOARCH=arm64 go build -o bin/myapp-mac-arm ./cmd/myapp/
GOOS=windows GOARCH=amd64 go build -o bin/myapp.exe ./cmd/myapp/
# Format all code
go fmt ./...
# Vet for common mistakes
go vet ./...
# Show documentation
go doc fmt.Println
Type System
Basic Types
| Category | Types | Notes |
|---|---|---|
| Boolean | bool | true / false |
| Signed integers | int, int8, int16, int32, int64 | int is platform-width (32 or 64-bit) |
| Unsigned integers | uint, uint8, uint16, uint32, uint64, uintptr | uintptr for pointer arithmetic |
| Floating-point | float32, float64 | Prefer float64 by default |
| Complex | complex64, complex128 | Rare in practice |
| Byte / Rune | byte (= uint8), rune (= int32) | rune represents a Unicode code point |
| String | string | Immutable UTF-8 byte sequence |
Zero Values
Every type has a zero value — the default when declared without initialization.
| Type | Zero Value |
|---|---|
bool | false |
int, float64, etc. | 0 |
string | "" (empty string) |
| pointer, slice, map, channel, func | nil |
| struct | each field set to its own zero value |
| array | each element set to its own zero value |
Type Definitions vs Aliases
// Type definition — creates a NEW type with its own method set
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToF() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
// These are DIFFERENT types — no implicit conversion
var c Celsius = 100
var f Fahrenheit = Fahrenheit(c) // explicit conversion required
// Type alias — just another name, SAME type, same method set
type MyFloat = float64 // rarely needed, mostly for gradual refactoring
var x MyFloat = 3.14
var y float64 = x // no conversion needed — they ARE the same type
Type Assertions and Conversions
// Type conversion — between compatible types
var i int = 42
var f float64 = float64(i) // explicit cast
var u uint = uint(f)
// String conversions (not casts!)
s := string(rune(65)) // "A" — int to unicode char
n, _ := strconv.Atoi("42") // string to int
s2 := strconv.Itoa(42) // int to string
f2, _ := strconv.ParseFloat("3.14", 64)
// Type assertion — extract concrete type from interface
var val interface{} = "hello"
// Panics if val is not string
s3 := val.(string)
// Safe form — comma-ok idiom
s4, ok := val.(string)
if ok {
fmt.Println("It's a string:", s4)
}
// Type switch — multiple type assertions
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int: %d", v)
case string:
return fmt.Sprintf("string: %q", v)
case bool:
return fmt.Sprintf("bool: %t", v)
default:
return fmt.Sprintf("unknown: %T", v)
}
}
Variables & Constants
Variable Declaration
package main
// Package-level variables (var keyword required)
var globalCounter int = 0
var (
host = "localhost"
port = 8080
debug bool // zero value: false
)
func main() {
// Explicit type
var x int = 10
// Inferred type from initializer
var y = 3.14
// Short declaration (most common inside functions)
z := "hello"
// Multiple assignment
a, b := 1, 2
a, b = b, a // swap — no temp variable needed
// Blank identifier — discard a value
result, _ := someFunction()
// Multiple return values
q, r := divide(10, 3)
fmt.Println(x, y, z, a, b, result, q, r)
}
func someFunction() (int, error) { return 42, nil }
func divide(a, b int) (int, int) { return a / b, a % b }
- Only works inside functions — not at package level.
- Requires at least one NEW variable on the left side.
- In nested scopes,
:=creates a NEW variable that shadows the outer one — a common source of bugs.
Constants and Iota
// Constants are evaluated at compile time
const Pi = 3.14159
const MaxRetries = 3
const Greeting = "hello"
// Grouped constants
const (
StatusOK = 200
StatusNotFound = 404
)
// iota — auto-incrementing integer within a const block
type Direction int
const (
North Direction = iota // 0
East // 1
South // 2
West // 3
)
// iota with expressions
type ByteSize float64
const (
_ = iota // ignore first value (0)
KB ByteSize = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
TB
)
// iota resets to 0 in each const block
const (
A = iota // 0
B // 1
)
const (
C = iota // 0 again — new block
D // 1
)
// Untyped constants — flexible, fit into any compatible type
const UntypedInt = 1000 // can be used as int, int64, float64...
const UntypedFloat = 3.14 // can be used as float32 or float64
var x float32 = UntypedFloat // OK
var y float64 = UntypedFloat // also OK
Functions
Function Basics
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values — idiomatic Go error handling
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Named return values — useful for documentation, defer
func minMax(arr []int) (min, max int) {
min, max = arr[0], arr[0]
for _, v := range arr[1:] {
if v < min { min = v }
if v > max { max = v }
}
return // naked return — returns min, max
}
// Variadic function — last param can accept zero or more values
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Spreading a slice into variadic
nums := []int{1, 2, 3, 4}
result := sum(nums...) // spread with ...
First-Class Functions and Closures
// Functions as values
type Handler func(string) string
// Function variable
var greet Handler = func(name string) string {
return "Hello, " + name
}
// Passing functions as arguments
func apply(s string, fn func(string) string) string {
return fn(s)
}
result := apply("world", greet)
// Returning a function — closure captures surrounding variables
func makeCounter() func() int {
count := 0 // captured by the closure
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// Immediately invoked function expression (IIFE)
result2 := func(x int) int { return x * x }(5) // 25
Defer
// defer: schedules a function call to run when the enclosing function returns
// Arguments are evaluated IMMEDIATELY, execution is deferred
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // will run when readFile returns, even if it panics
// ... read from f
return nil
}
// Defers execute in LIFO (last-in, first-out) order
func lifoDemo() {
defer fmt.Println("first defer — runs LAST")
defer fmt.Println("second defer — runs SECOND")
defer fmt.Println("third defer — runs FIRST")
fmt.Println("function body")
}
// Output:
// function body
// third defer — runs FIRST
// second defer — runs SECOND
// first defer — runs LAST
// defer with named returns — can modify return value
func double(n int) (result int) {
defer func() {
result *= 2 // modifies the named return
}()
result = n
return // returns n*2
}
// Argument evaluation timing gotcha
x := 0
defer fmt.Println(x) // prints 0 — x is captured NOW
x = 42 // too late, defer already has x=0
defer inside a loop to close resources — the defers accumulate and only fire when the function returns, not each iteration. Use an inner function or close explicitly instead.
The init() Function
// init() runs automatically before main(), after all variable initialization
// A package can have multiple init() functions (even in different files)
// They run in the order they appear
package mypackage
var config map[string]string
func init() {
config = make(map[string]string)
config["env"] = os.Getenv("APP_ENV")
if config["env"] == "" {
config["env"] = "development"
}
}
// Execution order within a package:
// 1. Package-level variables initialized
// 2. init() functions run (in source file order, then declaration order)
// 3. main() runs (in main package)
Pointers
// & — address-of operator: gets the pointer to a variable
// * — dereference operator: gets the value at the pointer
x := 42
p := &x // p is *int, holds the memory address of x
fmt.Println(*p) // 42 — dereference to get the value
*p = 100 // modify x through the pointer
fmt.Println(x) // 100
// new() allocates zeroed memory and returns a pointer
p2 := new(int) // *int pointing to 0
*p2 = 7
// make() creates slices, maps, and channels (not a pointer)
s := make([]int, 5)
m := make(map[string]int)
ch := make(chan int, 10)
// No pointer arithmetic in Go (unlike C)
// *(p + 1) — ILLEGAL
// Nil pointer — zero value for any pointer type
var ptr *int // nil
// *ptr = 5 — PANIC: nil pointer dereference
if ptr != nil {
*ptr = 5
}
// Struct field access through pointer — automatic dereference
type Point struct{ X, Y int }
pt := &Point{1, 2}
fmt.Println(pt.X) // equivalent to (*pt).X
When to Use Pointers vs Values
| Situation | Use Pointer | Use Value |
|---|---|---|
| Mutate the argument in caller | Yes | No |
| Large struct (>64 bytes) | Yes (avoid copy) | No |
| Nullable / optional value | Yes (nil means absent) | No |
| Small struct / scalar | No | Yes (simpler) |
| Shared state across goroutines | Yes + sync | No |
| Method needs to modify receiver | Yes (pointer receiver) | No |
| Implementing an interface consistently | Yes (all methods) | Depends |
Structs & Methods
Struct Definition and Field Tags
// Struct definition
type User struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
password string // unexported — not accessible outside package
}
// Struct literal — named fields (preferred)
u := User{
ID: 1,
Name: "Alice",
Email: "[email protected]",
}
// Anonymous / positional (fragile, avoid)
u2 := User{1, "Bob", "[email protected]", time.Now(), "secret"}
// Zero value struct
var u3 User // all fields zero-valued
// Pointer to struct
up := &User{Name: "Carol"}
up.Email = "[email protected]" // auto-dereference
// Anonymous fields (embedding by type name)
type Address struct {
Street string
City string
}
type Employee struct {
User // promoted fields and methods
Address // another embedded type
Department string
}
e := Employee{}
e.Name = "Dave" // promoted from User
e.Street = "Main St" // promoted from Address
e.Department = "Eng"
Methods and Receivers
type Rectangle struct {
Width, Height float64
}
// Value receiver — operates on a copy, cannot mutate
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Pointer receiver — can mutate the original
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func (r *Rectangle) String() string {
return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.Width, r.Height)
}
// Usage
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50 — value receiver, called on value
rect.Scale(2) // Go auto-takes address: (&rect).Scale(2)
fmt.Println(rect.Width) // 20
Embedding vs Inheritance
// Go has NO inheritance. Use embedding for composition.
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // embedded — Logger's methods promoted to Server
host string
port int
}
s := &Server{
Logger: &Logger{prefix: "SERVER"},
host: "localhost",
port: 8080,
}
s.Log("starting up") // calls s.Logger.Log("starting up")
s.Logger.Log("direct") // also valid — explicit access
// Embedding resolves ambiguity at shallowest depth
// If two embedded types have the same method name, you must be explicit
Interfaces
Implicit Implementation
// Interface definition
type Animal interface {
Sound() string
Name() string
}
// Implicit implementation — no "implements" keyword
type Dog struct{ name string }
func (d Dog) Sound() string { return "Woof" }
func (d Dog) Name() string { return d.name }
type Cat struct{ name string }
func (c Cat) Sound() string { return "Meow" }
func (c Cat) Name() string { return c.name }
// Any value that has all the interface's methods satisfies the interface
func makeSound(a Animal) {
fmt.Printf("%s says %s\n", a.Name(), a.Sound())
}
makeSound(Dog{name: "Rex"})
makeSound(Cat{name: "Whiskers"})
Common Standard Library Interfaces
// io.Reader — anything that can be read from
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer — anything that can be written to
type Writer interface {
Write(p []byte) (n int, err error)
}
// error — the built-in error interface
type error interface {
Error() string
}
// fmt.Stringer — controls how a value is printed
type Stringer interface {
String() string
}
// sort.Interface — makes a type sortable
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// Example: custom sort
type ByLength []string
func (b ByLength) Len() int { return len(b) }
func (b ByLength) Less(i, j int) bool { return len(b[i]) < len(b[j]) }
func (b ByLength) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
words := ByLength{"banana", "apple", "kiwi"}
sort.Sort(words) // uses sort.Interface methods
Interface Composition
// Interfaces can embed other interfaces
type ReadWriter interface {
io.Reader
io.Writer
}
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
// Empty interface — satisfied by every type
// Use sparingly; prefer generics or concrete types
func printAnything(v interface{}) {
fmt.Printf("%T: %v\n", v, v)
}
// Go 1.18+ — 'any' is an alias for interface{}
func printAny(v any) {
fmt.Println(v)
}
Interface Values and Nil
// An interface value has two components: (type, value)
// It is nil only if BOTH type and value are nil
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
// GOTCHA: this function returns a non-nil interface holding a nil pointer
func problematic() error {
var err *MyError // nil pointer to MyError
// ... some logic that might set err
return err // returns interface{(*MyError, nil)} — NOT nil!
}
err := problematic()
fmt.Println(err == nil) // false! The interface is not nil
// Correct: return untyped nil directly
func correct() error {
var err *MyError
if err != nil {
return err
}
return nil // returns interface{nil, nil} — truly nil
}
nil directly. A typed nil pointer stored in an interface is NOT equal to nil.
Error Handling
Creating and Returning Errors
import (
"errors"
"fmt"
)
// errors.New — simple, static error message
var ErrNotFound = errors.New("not found") // sentinel error
var ErrUnauthorized = errors.New("unauthorized")
// fmt.Errorf — formatted error message
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user id: %d", id)
}
// ...
return nil, fmt.Errorf("findUser %d: %w", id, ErrNotFound) // %w wraps
}
// Check errors
user, err := findUser(0)
if err != nil {
// Check for specific sentinel error
if errors.Is(err, ErrNotFound) {
fmt.Println("user does not exist")
}
log.Printf("error: %v", err)
return
}
_ = user
Error Wrapping and Unwrapping
// errors.Is — checks if any error in the chain matches the target
// Walks the Unwrap() chain
err := fmt.Errorf("database: %w", ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound)) // true
// errors.As — finds the first error in chain matching the target type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s — %s", e.Field, e.Message)
}
wrappedErr := fmt.Errorf("handler: %w", &ValidationError{
Field: "email",
Message: "invalid format",
})
var ve *ValidationError
if errors.As(wrappedErr, &ve) {
fmt.Printf("field %q failed: %s\n", ve.Field, ve.Message)
}
// Custom error type with Unwrap()
type DBError struct {
Query string
Err error
}
func (e *DBError) Error() string { return fmt.Sprintf("db query %q: %v", e.Query, e.Err) }
func (e *DBError) Unwrap() error { return e.Err } // allows errors.Is/As to traverse chain
Panic and Recover
// panic — stops normal execution, begins unwinding the stack
// recover — captures a panic in a deferred function
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil // panics if b == 0
}
result, err := safeDivide(10, 0)
// err = "recovered from panic: runtime error: integer divide by zero"
// When to panic vs return error:
// panic: programmer error, invariant violation, init failures
// error: expected failure conditions, IO, user input, external calls
Concurrency
Goroutines
// A goroutine is a lightweight thread managed by the Go runtime
// Start with 'go' keyword before a function call
go someFunction()
go func(msg string) {
fmt.Println(msg)
}("hello from goroutine")
// Goroutines are cheap — Go programs can run millions
// They're multiplexed onto OS threads by the scheduler (M:N threading)
Channels
// Unbuffered channel — send blocks until receiver is ready
ch := make(chan int)
go func() { ch <- 42 }() // send
v := <-ch // receive
// Buffered channel — send only blocks when buffer is full
bch := make(chan string, 3)
bch <- "one" // doesn't block
bch <- "two" // doesn't block
bch <- "three" // doesn't block
// bch <- "four" // would block — buffer full
// Directional channel types (enforce at compile time)
func producer(out chan<- int) { // send-only
out <- 42
close(out)
}
func consumer(in <-chan int) { // receive-only
for v := range in { // range closes when channel is closed
fmt.Println(v)
}
}
// Closing a channel
close(ch) // signals no more values
v, ok := <-ch // ok=false when channel is closed and empty
for v := range ch {} // range exits when channel is closed
// Check if channel has value without blocking
select {
case v := <-ch:
fmt.Println("received:", v)
default:
fmt.Println("no value ready")
}
Select Statement
// select: like switch but for channel operations
// Blocks until one case is ready, then executes that case
// If multiple are ready, picks one at random
func fanIn(ch1, ch2 <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for {
select {
case v, ok := <-ch1:
if !ok { ch1 = nil } else { out <- v }
case v, ok := <-ch2:
if !ok { ch2 = nil } else { out <- v }
}
if ch1 == nil && ch2 == nil {
return
}
}
}()
return out
}
// Timeout with select
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
resultCh := make(chan string, 1)
go func() {
// ... do fetch
resultCh <- "result"
}()
select {
case result := <-resultCh:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("timeout after %v", timeout)
}
}
Sync Primitives
import "sync"
// WaitGroup — wait for a collection of goroutines to finish
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("worker %d done\n", id)
}(i)
}
wg.Wait() // blocks until all Done() called
// Mutex — mutual exclusion lock
var (
mu sync.Mutex
counter int
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
// RWMutex — multiple readers, single writer
var rwmu sync.RWMutex
var cache map[string]string
func readCache(key string) string {
rwmu.RLock() // multiple goroutines can hold RLock simultaneously
defer rwmu.RUnlock()
return cache[key]
}
func writeCache(key, val string) {
rwmu.Lock() // exclusive write lock
defer rwmu.Unlock()
cache[key] = val
}
// Once — run a function exactly once
var (
once sync.Once
instance *MyService
)
func getInstance() *MyService {
once.Do(func() {
instance = &MyService{} // runs only the first time
})
return instance
}
// sync.Map — concurrent-safe map (use when reads >> writes)
var sm sync.Map
sm.Store("key", "value")
v, ok := sm.Load("key")
sm.Delete("key")
sm.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // return false to stop iteration
})
errgroup and Context
import (
"context"
"golang.org/x/sync/errgroup"
)
// errgroup — WaitGroup + error collection
func fetchAll(ctx context.Context, urls []string) error {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // capture loop vars
g.Go(func() error {
result, err := fetch(ctx, url)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
results[i] = result
return nil
})
}
if err := g.Wait(); err != nil {
return err // returns first non-nil error
}
return nil
}
// Context for cancellation
func doWork(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
default:
// do some work
time.Sleep(100 * time.Millisecond)
}
}
}
Common Patterns
Worker Pool
func workerPool(numWorkers int, jobs <-chan Job, results chan<- Result) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() {
wg.Wait()
close(results)
}()
}
// Usage
jobs := make(chan Job, 100)
results := make(chan Result, 100)
go workerPool(5, jobs, results)
for _, j := range allJobs {
jobs <- j
}
close(jobs) // signal workers no more jobs
for r := range results {
// consume results
}
Pipeline Pattern
// Each stage takes an input channel and returns an output channel
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
// Compose the pipeline
naturals := generate(1, 2, 3, 4, 5)
squares := square(naturals)
for v := range squares {
fmt.Println(v) // 1, 4, 9, 16, 25
}
Rate Limiting
// Basic rate limiting with time.Ticker
limiter := time.NewTicker(200 * time.Millisecond) // 5 req/sec
defer limiter.Stop()
for _, req := range requests {
<-limiter.C // wait for tick
go handle(req)
}
// Bursty rate limiting with buffered channel
burstLimiter := make(chan time.Time, 3) // allow burst of 3
for i := 0; i < 3; i++ {
burstLimiter <- time.Now() // fill initial burst
}
go func() {
for t := range time.Tick(200 * time.Millisecond) {
burstLimiter <- t
}
}()
for _, req := range requests {
<-burstLimiter
go handle(req)
}
Generics Go 1.18+
Type Parameters
// Generic function — type parameter [T constraint]
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
lengths := Map([]string{"a", "bb", "ccc"}, func(s string) int { return len(s) })
// Generic function with comparable constraint
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "c")) // false
Type Constraints
import "golang.org/x/exp/constraints"
// Built-in constraints
// any — alias for interface{}, accepts all types
// comparable — types that support == and !=
// Custom constraint using type set
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.1, 2.2})) // 3.3
// ~ prefix: includes all types whose underlying type is T
type Ordered interface {
~int | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// Generic struct
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
v, _ := intStack.Pop() // 2
Collections
Slices
// Array — fixed length, value type (copied on assignment)
var arr [5]int // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3} // compiler counts
// Slice — dynamic, reference type (shares underlying array)
s := []int{1, 2, 3, 4, 5}
// make([]T, length, capacity)
s2 := make([]int, 3) // [0 0 0], cap=3
s3 := make([]int, 0, 10) // [], cap=10 (pre-allocated)
// Slicing — [low:high:max]
s4 := s[1:3] // [2 3] — shares underlying array!
s5 := s[2:] // [3 4 5]
s6 := s[:3] // [1 2 3]
s7 := s[:] // full slice
// append — may allocate new backing array
s = append(s, 6, 7)
s = append(s, []int{8, 9}...) // spread another slice
// copy — always copies min(len(dst), len(src)) elements
dst := make([]int, len(s))
n := copy(dst, s)
// Delete element at index i (order-preserving)
i := 2
s = append(s[:i], s[i+1:]...)
// Delete element (fast, doesn't preserve order)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Capacity growth: typically doubles until 1024, then ~1.25x
fmt.Printf("len=%d cap=%d\n", len(s3), cap(s3))
copy() or append(s[:i:i], ...) (three-index slice) to get an independent copy.
Maps
// Map — unordered key-value store
m := map[string]int{
"alice": 30,
"bob": 25,
}
// make
m2 := make(map[string][]int)
// Read — always safe; missing key returns zero value
age := m["alice"] // 30
age2 := m["nobody"] // 0 — no panic
// Comma-ok idiom — distinguish missing from zero
age3, ok := m["alice"]
if !ok {
fmt.Println("key not found")
}
// Write
m["carol"] = 35
// Delete
delete(m, "bob")
// Iteration — order is RANDOM each run
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
// Keys only
for k := range m {
fmt.Println(k)
}
// Nil map — reads return zero, writes panic
var nilMap map[string]int
_ = nilMap["key"] // ok, returns 0
// nilMap["key"] = 1 // PANIC: assignment to entry in nil map
// Maps are not safe for concurrent use
// Use sync.Mutex or sync.Map for concurrent access
Range Keyword
// range over slice — index, value
for i, v := range []int{10, 20, 30} {
fmt.Println(i, v)
}
// range over map — key, value (random order)
for k, v := range map[string]int{"a": 1, "b": 2} {
fmt.Println(k, v)
}
// range over string — index (byte position), rune (Unicode code point)
for i, r := range "Hello, 世界" {
fmt.Printf("%d: %c (%d)\n", i, r, r)
}
// Note: i advances by byte count, not character count
// range over channel — receive until closed
for v := range ch {
fmt.Println(v)
}
// Discard index or value
for _, v := range slice { } // value only
for i := range slice { } // index only
Strings & Runes
// Strings are immutable sequences of bytes (not characters)
s := "Hello, 世界"
fmt.Println(len(s)) // 13 — byte count, not character count
fmt.Println(s[0]) // 72 — byte value, not character
// Iterate by rune (Unicode code point)
for i, r := range s {
fmt.Printf("%d: %c\n", i, r)
}
// Convert to []rune for index-safe operations
runes := []rune(s)
fmt.Println(len(runes)) // 9 — character count
fmt.Println(string(runes[7])) // "世"
// rune literals
r := 'A' // rune (int32), value 65
r2 := '世' // rune, value 19990
// Raw string literals — backticks, no escape sequences
raw := `C:\Users\name\file.txt`
multiline := `line 1
line 2
line 3`
// strings package
import "strings"
strings.Contains("seafood", "foo") // true
strings.HasPrefix("seafood", "sea") // true
strings.HasSuffix("seafood", "food") // true
strings.Count("cheese", "e") // 3
strings.Index("chicken", "ken") // 4, -1 if not found
strings.ToUpper("hello") // "HELLO"
strings.ToLower("HELLO") // "hello"
strings.TrimSpace(" hello ") // "hello"
strings.Trim("--hello--", "-") // "hello"
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.Join([]string{"a", "b"}, "-") // "a-b"
strings.Replace("oink oink", "oink", "moo", 1) // "moo oink"
strings.ReplaceAll("oink oink", "oink", "moo") // "moo moo"
strings.Fields(" foo bar ") // ["foo", "bar"] — split on whitespace
// strings.Builder — efficient string concatenation
var b strings.Builder
for i := 0; i < 5; i++ {
fmt.Fprintf(&b, "item%d ", i)
}
result := b.String()
// fmt formatting verbs
fmt.Sprintf("%v", val) // default format
fmt.Sprintf("%+v", val) // struct: includes field names
fmt.Sprintf("%#v", val) // Go syntax representation
fmt.Sprintf("%T", val) // type
fmt.Sprintf("%d", 42) // decimal integer
fmt.Sprintf("%f", 3.14) // floating point
fmt.Sprintf("%e", 3.14) // scientific notation
fmt.Sprintf("%s", "hi") // string
fmt.Sprintf("%q", "hi") // quoted string: "hi"
fmt.Sprintf("%x", 255) // hexadecimal: ff
fmt.Sprintf("%b", 42) // binary: 101010
fmt.Sprintf("%p", &x) // pointer address
I/O
File Operations
import (
"bufio"
"encoding/json"
"io"
"os"
)
// Read entire file
data, err := os.ReadFile("file.txt")
if err != nil {
return err
}
fmt.Println(string(data))
// Write entire file
err = os.WriteFile("out.txt", []byte("hello\n"), 0644)
// Open file for reading
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close()
// Buffered reading line by line
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
_ = line
}
if err := scanner.Err(); err != nil {
return err
}
// Create / write file
f2, err := os.Create("new.txt")
if err != nil {
return err
}
defer f2.Close()
writer := bufio.NewWriter(f2)
fmt.Fprintln(writer, "line 1")
fmt.Fprintln(writer, "line 2")
writer.Flush() // must flush buffered writer!
// Append to file
f3, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f3.Close()
// Copy between reader and writer
n, err := io.Copy(dst, src) // copies until EOF, returns bytes written
JSON Encoding/Decoding
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"` // omit if zero value
private string // unexported — not marshaled
}
// Marshal (Go → JSON)
p := Person{Name: "Alice", Age: 30}
data, err := json.Marshal(p)
// data = []byte(`{"name":"Alice","age":30}`)
// Pretty print
data2, err := json.MarshalIndent(p, "", " ")
// Unmarshal (JSON → Go) — must pass pointer
var p2 Person
err = json.Unmarshal([]byte(`{"name":"Bob","age":25}`), &p2)
// Stream encoding/decoding (more memory efficient for large data)
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(p)
decoder := json.NewDecoder(os.Stdin)
var p3 Person
decoder.Decode(&p3)
// Dynamic JSON with map
var result map[string]interface{}
json.Unmarshal(data, &result)
// Use json.RawMessage to delay or preserve JSON
type Envelope struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
// Handling unknown fields
decoder2 := json.NewDecoder(strings.NewReader(jsonStr))
decoder2.DisallowUnknownFields() // return error for unknown fields
HTTP Basics
import "net/http"
// HTTP server
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
// Read query params
name := r.URL.Query().Get("name")
// Read request body
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
// Write response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"greeting": "Hello, " + name})
})
log.Fatal(http.ListenAndServe(":8080", nil))
// HTTP client
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.example.com/users")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var users []User
json.NewDecoder(resp.Body).Decode(&users)
// POST with JSON body
payload := map[string]string{"name": "Alice"}
buf, _ := json.Marshal(payload)
resp2, err := client.Post("https://api.example.com/users",
"application/json", bytes.NewReader(buf))
Testing
Test Functions
// File: math_test.go (same package or _test suffix package)
package math_test
import (
"testing"
"github.com/yourname/myproject/math"
)
// Test function must start with Test, take *testing.T
func TestAdd(t *testing.T) {
result := math.Add(2, 3)
if result != 5 {
t.Errorf("Add(2,3) = %d; want 5", result)
}
}
// t.Fatal — fails the test and stops execution of this test function
// t.Error — fails the test but continues execution
// t.Log — log without failing (use -v to see)
// t.Skip — skip the test
Table-Driven Tests
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"positive", 10, 2, 5, false},
{"negative dividend", -10, 2, -5, false},
{"divide by zero", 10, 0, 0, true},
{"float", 7, 2, 3.5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := math.Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("Divide() = %v, want %v", got, tt.want)
}
})
}
}
// Run tests
// go test ./... — all packages
// go test -v ./... — verbose
// go test -run TestDivide — specific test
// go test -run TestDiv/zero — specific subtest
// go test -count=1 ./... — disable caching
Parallel Tests and Benchmarks
func TestParallelSafe(t *testing.T) {
tests := []struct{ name, input, want string }{
{"upper", "hello", "HELLO"},
{"lower", "WORLD", "world"},
}
for _, tt := range tests {
tt := tt // capture loop variable — REQUIRED before Go 1.22
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // run subtests in parallel
// ...
})
}
}
// Benchmarks — measure performance
func BenchmarkFibonacci(b *testing.B) {
// b.N automatically adjusted for stable measurement
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// go test -bench=. -benchmem — run benchmarks, show memory
// go test -bench=BenchmarkFib — specific benchmark
// go test -benchtime=5s — run for 5 seconds
Testify and Mocking
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
// assert — continues on failure
assert.Equal(t, 5, math.Add(2, 3))
assert.NoError(t, err)
assert.Contains(t, "seafood", "foo")
assert.Len(t, slice, 3)
assert.Nil(t, ptr)
assert.True(t, condition)
// require — stops test on failure (use for preconditions)
require.NoError(t, err)
require.NotNil(t, result)
}
// Mocking with interfaces — no magic needed in Go
type EmailSender interface {
Send(to, subject, body string) error
}
// Mock implementation
type MockEmailSender struct {
mock.Mock
}
func (m *MockEmailSender) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}
func TestUserCreation(t *testing.T) {
mockSender := &MockEmailSender{}
mockSender.On("Send", "[email protected]", mock.Anything, mock.Anything).
Return(nil)
svc := NewUserService(mockSender)
err := svc.CreateUser("Alice", "[email protected]")
assert.NoError(t, err)
mockSender.AssertExpectations(t)
}
Fuzzing
// Fuzz testing — Go 1.18+
// go test -fuzz=FuzzReverse -fuzztime=30s
func FuzzReverse(f *testing.F) {
// Seed corpus
f.Add("hello")
f.Add("world")
f.Add("")
f.Add("!@#$")
f.Fuzz(func(t *testing.T, s string) {
rev := Reverse(s)
// Invariant: reversing twice gives original
if Reverse(rev) != s {
t.Errorf("double reverse of %q = %q", s, Reverse(rev))
}
// Invariant: same length
if len(rev) != len(s) {
t.Errorf("len mismatch: %d vs %d", len(rev), len(s))
}
})
}
Context
import "context"
// context.Background — top-level, never cancelled
ctx := context.Background()
// context.TODO — placeholder when unsure which context to use
ctx2 := context.TODO()
// WithCancel — manual cancellation
ctx3, cancel := context.WithCancel(ctx)
defer cancel() // always defer cancel to avoid goroutine leak
go func() {
// ... do work
cancel() // signal cancellation
}()
// WithTimeout — auto-cancel after duration
ctx4, cancel4 := context.WithTimeout(ctx, 5*time.Second)
defer cancel4()
// WithDeadline — auto-cancel at absolute time
deadline := time.Now().Add(5 * time.Second)
ctx5, cancel5 := context.WithDeadline(ctx, deadline)
defer cancel5()
// WithValue — pass request-scoped values (use sparingly)
type contextKey string
const userIDKey contextKey = "userID"
ctx6 := context.WithValue(ctx, userIDKey, 42)
userID := ctx6.Value(userIDKey).(int) // type assertion needed
// Check cancellation
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
default:
// continue
}
// HTTP handler example
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := database.Query(ctx, "SELECT ...")
if err != nil {
if errors.Is(err, context.Canceled) {
// client disconnected — normal, don't log as error
return
}
http.Error(w, "internal error", 500)
return
}
json.NewEncoder(w).Encode(result)
}
- Always pass
ctxas the first parameter of functions that do I/O. - Never store a context in a struct — pass it explicitly each call.
- Use
context.WithValueonly for request-scoped metadata (e.g., request ID, user ID), not for optional parameters. - Always call the cancel function returned by
WithCancel/WithTimeout— defer it immediately after creation.
Common Patterns
Functional Options
// Problem: configuring a struct with many optional parameters
// Solution: functional options — clean API, backwards-compatible
type Server struct {
host string
port int
timeout time.Duration
maxConn int
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) { s.host = host }
}
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithMaxConnections(n int) Option {
return func(s *Server) { s.maxConn = n }
}
func NewServer(opts ...Option) *Server {
s := &Server{ // sensible defaults
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConn: 100,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
s := NewServer(
WithPort(9090),
WithTimeout(10 * time.Second),
)
Middleware Pattern
// HTTP middleware wraps a handler
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call next handler
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValid(token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain middleware
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Usage
mux := http.NewServeMux()
mux.HandleFunc("/api/data", dataHandler)
handler := Chain(mux, Logging, Auth)
http.ListenAndServe(":8080", handler)
Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server in goroutine
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("server stopped accepting connections")
}()
// Wait for interrupt signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
log.Println("shutting down...")
// Give active requests 30 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
log.Println("server exited cleanly")
}
Dependency Injection via Interfaces
// Define interfaces for dependencies
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Save(ctx context.Context, user *User) error
}
type EmailService interface {
SendWelcome(ctx context.Context, email, name string) error
}
// Service depends on interfaces, not concrete types
type UserService struct {
repo UserRepository
email EmailService
log *slog.Logger
}
func NewUserService(repo UserRepository, email EmailService, log *slog.Logger) *UserService {
return &UserService{repo: repo, email: email, log: log}
}
func (s *UserService) Register(ctx context.Context, name, email string) error {
user := &User{Name: name, Email: email}
if err := s.repo.Save(ctx, user); err != nil {
return fmt.Errorf("save user: %w", err)
}
if err := s.email.SendWelcome(ctx, email, name); err != nil {
s.log.Warn("welcome email failed", "email", email, "err", err)
// non-fatal — don't return error
}
return nil
}
// Wire up in main
func main() {
db := mustConnectDB()
repo := postgres.NewUserRepo(db)
emailSvc := sendgrid.NewEmailService(os.Getenv("SENDGRID_KEY"))
logger := slog.Default()
userSvc := NewUserService(repo, emailSvc, logger)
_ = userSvc
}
Build & Tooling
Cross-Compilation
# Cross-compile
GOOS=linux GOARCH=amd64 go build -o bin/app-linux-amd64 ./cmd/app/
GOOS=linux GOARCH=arm64 go build -o bin/app-linux-arm64 ./cmd/app/
GOOS=darwin GOARCH=amd64 go build -o bin/app-darwin-amd64 ./cmd/app/
GOOS=darwin GOARCH=arm64 go build -o bin/app-darwin-arm64 ./cmd/app/
GOOS=windows GOARCH=amd64 go build -o bin/app.exe ./cmd/app/
# Common GOOS values: linux, darwin, windows, freebsd
# Common GOARCH values: amd64, arm64, 386, arm
# Inject version at build time with ldflags
go build -ldflags="-X main.Version=1.2.3 -X main.BuildTime=$(date -u +%Y%m%dT%H%M%S)" \
-o bin/app ./cmd/app/
# Strip debug info (smaller binary)
go build -ldflags="-s -w" -o bin/app ./cmd/app/
// Read version set by ldflags
var (
Version = "dev" // overridden at build time
BuildTime = "unknown"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "version" {
fmt.Printf("version %s built at %s\n", Version, BuildTime)
os.Exit(0)
}
// ...
}
Build Tags
// Go 1.17+ syntax (//go:build)
// Must be before package declaration, separated by blank line
//go:build linux || darwin
// +build linux darwin // old syntax (keep for compatibility pre-1.17)
package main
// Platform-specific file — only compiled on linux
// file: network_linux.go
//go:build !windows
package net
// Feature flags
//go:build integration
package integration_test
// Exclude from builds
//go:build ignore
package tools // won't be compiled normally
go generate
// go generate runs commands specified in source files
// Run with: go generate ./...
//go:generate stringer -type=Direction
type Direction int
const (
North Direction = iota
East
South
West
)
// stringer generates a String() method for the type
//go:generate mockgen -source=interfaces.go -destination=mocks/mock_interfaces.go
//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate
Linting and Formatting
# Format all code (always run before commit)
go fmt ./...
# Static analysis — catches common mistakes
go vet ./...
# golangci-lint — aggregates many linters
# Install: brew install golangci-lint
golangci-lint run ./...
golangci-lint run --fix ./... # auto-fix where possible
# .golangci.yml — configuration
# linters:
# enable:
# - errcheck # check that errors are handled
# - gosimple # simplification suggestions
# - govet # go vet checks
# - ineffassign # detect ineffectual assignments
# - staticcheck # advanced static analysis
# - unused # check for unused code
# Run specific linter
golangci-lint run --disable-all --enable errcheck ./...
# Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go test -cover ./... # quick summary
Module Management
# Initialize
go mod init github.com/you/project
# Add dependency (latest)
go get github.com/some/pkg
# Add specific version
go get github.com/some/[email protected]
# Add at commit
go get github.com/some/pkg@abc1234
# Upgrade to latest minor/patch for all direct deps
go get -u ./...
# Upgrade specific package
go get -u github.com/some/pkg
# Remove a dependency
go get github.com/some/pkg@none # then go mod tidy
# Clean up: remove unused, add missing
go mod tidy
# Vendor all dependencies
go mod vendor
go build -mod=vendor ./... # build using vendor dir
# Verify checksums
go mod verify
# Why is this dep needed?
go mod why github.com/some/pkg
# Replace a module (useful for local development)
# In go.mod:
# replace github.com/you/mylib => ../mylib
# Workspace mode (Go 1.18+): develop multiple modules together
go work init
go work use ./moduleA ./moduleB
# Creates go.work — do not commit to VCS
Structured Logging (slog)
import "log/slog" // Go 1.21+
// Default logger (text format to stderr)
slog.Info("server started", "port", 8080)
slog.Warn("high memory", "used_mb", 512)
slog.Error("request failed", "err", err, "url", r.URL.Path)
// JSON handler for production
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true, // includes file:line in output
}))
slog.SetDefault(logger)
// With common attributes (fields added to every log call)
requestLogger := logger.With("request_id", "abc-123", "user_id", 42)
requestLogger.Info("processing request")
// Grouped attributes
slog.Info("user action",
slog.Group("user",
"id", 42,
"name", "Alice",
),
"action", "login",
)
| Command | Purpose |
|---|---|
go run ./cmd/app | Compile and run without saving binary |
go build ./... | Build all packages (checks for errors) |
go test ./... | Run all tests |
go test -race ./... | Run tests with race detector |
go vet ./... | Static analysis |
go fmt ./... | Format source code |
go doc pkg.Symbol | Show documentation |
go env | Show Go environment variables |
go list -m all | List all modules (transitive) |
go clean -cache | Clear build cache |