Quick Reference
Essential Go commands, declaration patterns, and common idioms at a glance.
Common Go Commands
| Command | Description |
|---|---|
go run main.go |
Compile and execute a Go program in one step |
go build |
Compile the current package into an executable binary |
go test ./... |
Run all tests recursively in the current module |
go fmt ./... |
Format all Go source files with canonical style |
go vet ./... |
Report suspicious constructs and likely bugs |
go mod init module/path |
Initialize a new module in the current directory |
go mod tidy |
Add missing and remove unused module dependencies |
go get pkg@version |
Add or update a dependency in go.mod |
go install pkg@latest |
Build and install a Go binary to $GOBIN |
go doc fmt.Println |
Show documentation for a symbol or package |
go generate ./... |
Run code generation directives in source files |
go work init |
Initialize a multi-module workspace (Go 1.18+) |
Variable Declaration Quick Reference
name := "gopher"
Infers type. Only inside functions. Most common form.
var count int = 10
Explicit type. Works at package level and inside functions.
var active bool
Declares with zero value (false for bool, 0 for int, "" for string).
const Pi = 3.14159
Compile-time constant. Untyped unless explicitly typed.
const ( A = iota; B; C )
Auto-incrementing integer constants: A=0, B=1, C=2.
x, y := 10, "hello"
Declare multiple variables at once with different types.
Common Patterns
Error Check
result, err := doSomething()
if err != nil {
return fmt.Errorf("failed: %w", err)
}
The canonical Go error-handling pattern. Check errors immediately after every fallible call.
Goroutine Launch
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
process(item)
}()
wg.Wait()
Launch concurrent work with a WaitGroup to synchronize completion.
Defer Close
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close()
// use f...
Defer cleanup immediately after acquiring a resource. Deferred calls run in LIFO order when the function returns.
Struct Literal
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
Initialize structs with named fields. Unset fields receive their zero value.
Slice Operations
items := []string{"a", "b", "c"}
items = append(items, "d")
sub := items[1:3] // ["b", "c"]
cp := make([]string, len(items))
copy(cp, items)
Slices are Go's dynamic arrays. Append grows, slicing shares memory, copy for safe duplication.
Map Operations
m := map[string]int{"a": 1, "b": 2}
m["c"] = 3
val, ok := m["a"] // ok == true
delete(m, "b")
for k, v := range m { ... }
Maps are hash tables. Always use the comma-ok idiom to distinguish missing keys from zero values.
Core Syntax
Fundamental building blocks of the Go language: packages, types, variables, and control flow.
Package Structure & Imports
Every Go file begins with a package declaration. The main package with a main() function is the entry point for executables. All other packages are libraries.
package main
import (
"fmt"
"net/http"
"os"
"github.com/gorilla/mux" // third-party import
"mymodule/internal/auth" // internal package
)
func main() {
fmt.Println("Hello, Go!")
}
goimports tool handles this automatically.
// Package-level declarations (visible across all files in the package)
package config
var DefaultPort = 8080 // exported (uppercase)
var defaultHost = "localhost" // unexported (lowercase)
// init() runs once when the package is first imported
func init() {
if port := os.Getenv("PORT"); port != "" {
DefaultPort, _ = strconv.Atoi(port)
}
}
Variable Declarations
// Short variable declaration (inside functions only)
name := "gopher"
x, y := 10, 20
// Var declaration (works at package level too)
var age int = 30
var city string // zero value: ""
var (
width float64 = 1920
height float64 = 1080
)
// Constants
const Pi = 3.14159265 // untyped constant
const MaxRetries int = 3 // typed constant
// Iota — auto-incrementing constant generator
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// Iota with expressions
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
TB // 1099511627776
)
:= inside functions for brevity. Use var when you need to specify a type explicitly, declare at package level, or want the zero value without assigning.
Basic Types & Zero Values
| Type | Zero Value | Description |
|---|---|---|
bool |
false |
Boolean true/false |
int, int8 ... int64 |
0 |
Signed integers (int is platform-dependent: 32 or 64 bit) |
uint, uint8 ... uint64 |
0 |
Unsigned integers; byte is alias for uint8 |
float32, float64 |
0 |
IEEE-754 floating point; prefer float64 |
complex64, complex128 |
(0+0i) |
Complex numbers with float32/float64 parts |
string |
"" |
Immutable UTF-8 byte sequence |
rune |
0 |
Alias for int32; represents a Unicode code point |
uintptr |
0 |
Integer large enough to hold any pointer value |
Composite Types
// ── Arrays (fixed size, rarely used directly) ──
var grid [3][3]int // 3x3 matrix of zeros
primes := [5]int{2, 3, 5, 7, 11}
// ── Slices (dynamic, backed by an array) ──
nums := []int{1, 2, 3, 4, 5}
nums = append(nums, 6, 7) // grow slice
sub := nums[2:5] // [3, 4, 5] — shares memory
fresh := make([]int, 0, 100) // len=0, cap=100
// ── Maps (hash table) ──
scores := map[string]int{
"alice": 95,
"bob": 87,
}
scores["carol"] = 91
val, ok := scores["dave"] // ok is false, val is 0
delete(scores, "bob")
// ── Structs ──
type Point struct {
X, Y float64
}
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
admin bool // unexported field
}
p := Point{X: 3.0, Y: 4.0}
u := User{Name: "Alice", Email: "alice@example.com"}
// Struct embedding (composition over inheritance)
type Admin struct {
User // embedded struct
Level int
}
a := Admin{User: User{Name: "Bob"}, Level: 1}
fmt.Println(a.Name) // promoted field
Pointers
Go has pointers but no pointer arithmetic. Pointers are used to share data and avoid copying large structures.
x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 — dereference
*p = 100 // x is now 100
// new() allocates and returns a pointer
ptr := new(int) // *int, points to 0
// Structs are commonly used via pointers
func newUser(name string) *User {
return &User{Name: name} // safe: Go escapes to heap
}
// Nil pointer — the zero value for pointer types
var np *int // nil
if np != nil {
fmt.Println(*np)
}
Control Flow
// ── If / Else (no parentheses, braces required) ──
if x > 10 {
fmt.Println("big")
} else if x > 5 {
fmt.Println("medium")
} else {
fmt.Println("small")
}
// If with init statement
if err := validate(input); err != nil {
return err // err scoped to this if/else
}
// ── For (the only loop keyword) ──
for i := 0; i < 10; i++ { // classic C-style
fmt.Println(i)
}
for condition { // while-style
// ...
}
for { // infinite loop
break // exit
}
// ── Range (iterate slices, maps, strings, channels) ──
for i, v := range []string{"a", "b", "c"} {
fmt.Printf("%d: %s\n", i, v)
}
for key, val := range myMap { // map iteration (random order)
fmt.Printf("%s = %d\n", key, val)
}
for i := range 10 { // Go 1.22+ range over integer
fmt.Println(i) // 0, 1, 2, ... 9
}
// ── Switch (no fall-through by default) ──
switch day {
case "Monday", "Tuesday":
fmt.Println("early week")
case "Friday":
fmt.Println("TGIF")
fallthrough // explicit fall-through
default:
fmt.Println("another day")
}
// Type switch
switch v := x.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s\n", v)
default:
fmt.Printf("unknown: %T\n", v)
}
// ── Defer, Panic, Recover ──
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil // panics if b == 0
}
for i := range n, which iterates from 0 to n-1. This replaces the verbose for i := 0; i < n; i++ pattern for simple counting loops.
Functions & Interfaces
Functions, methods, closures, interfaces, and generics: how Go composes behavior.
Function Syntax
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values (useful for documentation, short functions)
func coordinates(addr string) (lat, lng float64, err error) {
// ... geocoding logic ...
return lat, lng, nil
}
// Blank identifier to ignore return values
_, err := divide(10, 0)
// Functions are first-class values
var op func(int, int) int = add
result := op(3, 4) // 7
Variadic Functions
// Variadic parameter (must be last parameter)
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum() // 0
// Spread a slice into variadic args
values := []int{10, 20, 30}
sum(values...) // 60
// Common stdlib example
fmt.Println("a", "b", "c") // Println(a ...any)
Methods: Value vs Pointer Receivers
type Rect struct {
Width, Height float64
}
// Value receiver — operates on a copy
func (r Rect) Area() float64 {
return r.Width * r.Height
}
// Pointer receiver — can modify the original
func (r *Rect) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
r := Rect{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Area()) // 200
Closures
// A closure captures variables from its enclosing scope
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// Closures are often used with goroutines and callbacks
func processItems(items []string, fn func(string)) {
for _, item := range items {
fn(item)
}
}
processItems([]string{"a", "b"}, func(s string) {
fmt.Println(strings.ToUpper(s))
})
Interfaces & Implicit Implementation
Interfaces in Go are satisfied implicitly. If a type has all the methods an interface requires, it implements that interface. No implements keyword needed.
// Define an interface
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle implicitly implements Shape
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Use the interface
func printInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
printInfo(Circle{Radius: 5}) // works!
// The empty interface accepts any value (aliased as "any" since Go 1.18)
func describe(val any) {
fmt.Printf("(%v, %T)\n", val, val)
}
describe(42) // (42, int)
describe("hello") // (hello, string)
Type Assertions & Type Switches
// Type assertion — extract the concrete type
var i any = "hello"
s := i.(string) // panics if i is not string
fmt.Println(s) // "hello"
s, ok := i.(string) // safe form — ok is false if wrong type
if ok {
fmt.Println(s)
}
// Type switch — branch on dynamic type
func classify(val any) string {
switch v := val.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int: %d", v)
case string:
return fmt.Sprintf("string of len %d", len(v))
case bool:
return fmt.Sprintf("bool: %t", v)
case error:
return fmt.Sprintf("error: %s", v)
default:
return fmt.Sprintf("unknown: %T", v)
}
}
Common Standard Library Interfaces
| Interface | Method(s) | Purpose |
|---|---|---|
io.Reader |
Read(p []byte) (n int, err error) |
Reads bytes from a source (files, network, buffers) |
io.Writer |
Write(p []byte) (n int, err error) |
Writes bytes to a destination |
io.Closer |
Close() error |
Releases resources (files, connections) |
fmt.Stringer |
String() string |
Custom string representation for fmt.Print |
error |
Error() string |
The built-in error interface |
sort.Interface |
Len(), Less(i,j), Swap(i,j) |
Custom sorting for any collection |
http.Handler |
ServeHTTP(w, *r) |
Handle HTTP requests |
encoding.TextMarshaler |
MarshalText() ([]byte, error) |
Custom text serialization |
// Implementing fmt.Stringer
type Point struct{ X, Y float64 }
func (p Point) String() string {
return fmt.Sprintf("(%g, %g)", p.X, p.Y)
}
fmt.Println(Point{3, 4}) // prints: (3, 4)
// Implementing io.Reader
type InfiniteZeros struct{}
func (z InfiniteZeros) Read(p []byte) (int, error) {
for i := range p {
p[i] = 0
}
return len(p), nil
}
Generics Go 1.18+
Go 1.18 introduced type parameters, enabling functions and types to operate on multiple concrete types while preserving type safety.
// Generic function with type parameter
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
})
// doubled == [2, 4, 6]
// Type constraint — restrict what types are allowed
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
Min(3, 7) // 3
Min("alpha", "beta") // "alpha"
// Custom constraint interface
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// The comparable constraint — types that support == and !=
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Generic types
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
~int constraint matches int and any named type whose underlying type is int (e.g., type UserID int). Without the tilde, only the exact type matches.
Concurrency
Goroutines, channels, select, context, and sync primitives: Go's concurrency model built on CSP.
Goroutines & the go Keyword
A goroutine is a lightweight thread of execution managed by the Go runtime. Goroutines start with as little as 2 KB of stack space and are multiplexed onto OS threads.
// Launch a goroutine
go func() {
fmt.Println("running concurrently")
}()
// Named function as goroutine
go processRequest(req)
// Goroutines are cheap — launch thousands
for i := 0; i < 10_000; i++ {
go worker(i)
}
main() returns, all goroutines are terminated. Use sync.WaitGroup, channels, or select{} to keep main alive until work completes.
WaitGroup
import "sync"
func main() {
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://golang.org",
"https://go.dev",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
fmt.Printf("%s: %s\n", u, resp.Status)
}(url)
}
wg.Wait() // blocks until all goroutines call Done()
fmt.Println("All requests complete")
}
Channels
// ── Unbuffered channel (synchronous) ──
ch := make(chan string)
go func() {
ch <- "hello" // blocks until receiver is ready
}()
msg := <-ch // blocks until sender sends
fmt.Println(msg) // "hello"
// ── Buffered channel (asynchronous up to capacity) ──
jobs := make(chan int, 100) // buffer of 100
jobs <- 42 // doesn't block (buffer not full)
val := <-jobs // 42
// ── Directional channels (for function signatures) ──
func producer(out chan<- int) { // send-only
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) { // receive-only
for val := range in { // iterates until channel closed
fmt.Println(val)
}
}
ch := make(chan int)
go producer(ch)
consumer(ch)
// ── Closing channels ──
close(ch) // signal no more values
val, ok := <-ch // ok is false after close
// Range over channel (exits when channel is closed)
for item := range ch {
process(item)
}
Select Statement
Select lets a goroutine wait on multiple channel operations simultaneously. It blocks until one case is ready; if multiple are ready, one is chosen at random.
// ── Basic select ──
select {
case msg := <-msgCh:
fmt.Println("received:", msg)
case errCh <- err:
fmt.Println("sent error")
case <-done:
fmt.Println("shutting down")
return
}
// ── Select with default (non-blocking) ──
select {
case msg := <-ch:
handle(msg)
default:
// ch isn't ready, do something else
fmt.Println("no message available")
}
// ── Select with timeout ──
select {
case result := <-ch:
fmt.Println("got result:", result)
case <-time.After(3 * time.Second):
fmt.Println("timed out after 3s")
}
// ── Ticker loop with quit channel ──
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
fmt.Println("tick at", t)
case <-quit:
return
}
}
Context Package
The context package carries deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines.
import "context"
// ── WithCancel — manual cancellation ──
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
return
default:
doWork()
}
}
}(ctx)
cancel() // signal all goroutines using this context
// ── WithTimeout — automatic cancellation after duration ──
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := longOperation(ctx)
if err == context.DeadlineExceeded {
log.Println("operation timed out")
}
// ── WithDeadline — cancel at specific time ──
deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// ── WithValue — attach request-scoped data ──
type contextKey string
const userKey contextKey = "user"
ctx = context.WithValue(ctx, userKey, currentUser)
// Retrieve later
if user, ok := ctx.Value(userKey).(*User); ok {
fmt.Println("request from:", user.Name)
}
func DoThing(ctx context.Context, ...). Never store context in a struct. Always call the cancel function (use defer). Do not use context.WithValue for passing optional parameters; use it only for request-scoped data like trace IDs.
Sync Primitives
import "sync"
// ── Mutex — mutual exclusion ──
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
// ── RWMutex — multiple readers, single writer ──
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // multiple readers allowed
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, val string) {
c.mu.Lock() // exclusive write access
defer c.mu.Unlock()
c.data[key] = val
}
// ── Once — exactly one execution ──
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectToDatabase()
})
return instance
}
// ── Pool — reusable object pool ──
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
// use buf ...
bufPool.Put(buf) // return to pool
Atomic Operations
import "sync/atomic"
// ── Atomic counter (lock-free) ──
var counter atomic.Int64
counter.Add(1) // increment
counter.Add(-1) // decrement
val := counter.Load() // read
counter.Store(0) // write
swapped := counter.CompareAndSwap(5, 10) // CAS
// ── Atomic value (store any type) ──
var config atomic.Value
type Config struct {
Workers int
Debug bool
}
config.Store(Config{Workers: 4, Debug: false})
cfg := config.Load().(Config)
// ── Atomic bool ──
var ready atomic.Bool
ready.Store(true)
if ready.Load() {
proceed()
}
Common Concurrency 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)
}
}()
}
wg.Wait()
close(results)
}
Fixed pool of goroutines consuming from a shared job channel. Controls resource usage.
Fan-In (Merge)
func fanIn(channels ...<-chan string) <-chan string {
merged := make(chan string)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan string) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Merge multiple channels into one. Collects results from parallel producers.
Pipeline
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// Usage: compose stages
for v := range square(square(generate(2, 3))) {
fmt.Println(v) // 16, 81
}
Chain processing stages via channels. Each stage reads from the previous and writes to the next.
Semaphore
// Limit concurrent operations
sem := make(chan struct{}, 10) // max 10
for _, url := range urls {
sem <- struct{}{} // acquire
go func(u string) {
defer func() { <-sem }() // release
fetch(u)
}(url)
}
// Fill semaphore to wait for all
for i := 0; i < cap(sem); i++ {
sem <- struct{}{}
}
Buffered channel as a counting semaphore. Limits concurrency to N simultaneous operations.
Common Gotchas
for _, url := range urls {
go func() {
fetch(url) // all goroutines
// see last url!
}()
}
for _, url := range urls {
go func(u string) {
fetch(u) // each gets
// its own copy
}(url)
}
ch := make(chan int)
go func() {
result := compute()
ch <- result // blocks forever
// if nobody reads ch
}()
// forgot to <-ch
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
select {
case result := <-ch:
use(result)
case <-ctx.Done():
log.Println("timed out")
}
Error Handling
Go treats errors as values. Master the error interface, wrapping, sentinel errors, and best practices.
The error Interface
In Go, errors are ordinary values that implement the built-in error interface. There are no exceptions, no try/catch. Errors are returned and checked explicitly.
// The built-in error interface
type error interface {
Error() string
}
// Functions return errors as the last return value
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}
// Callers check errors immediately
data, err := readFile("config.yaml")
if err != nil {
log.Fatalf("cannot read config: %v", err)
}
Creating Errors: errors.New and fmt.Errorf
import (
"errors"
"fmt"
)
// Simple static error message
err := errors.New("connection refused")
// Formatted error with context
err := fmt.Errorf("user %q not found in database", username)
// Wrapping an underlying error with %w (Go 1.13+)
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("loading config %s: %w", path, err)
}
// The resulting error chain:
// "loading config app.yaml: open app.yaml: no such file or directory"
%w to wrap an error so callers can unwrap and inspect it with errors.Is or errors.As. Use %v when you want to include the error text but intentionally break the chain so callers cannot inspect the underlying error.
errors.Is, errors.As, errors.Unwrap
// ── errors.Is — check for a specific error value in the chain ──
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file does not exist")
}
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("operation timed out")
}
// errors.Is walks the entire wrap chain
wrapped := fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", os.ErrPermission))
errors.Is(wrapped, os.ErrPermission) // true
// ── errors.As — extract a specific error type from the chain ──
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("failed path:", pathErr.Path)
fmt.Println("operation:", pathErr.Op)
}
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Println("net op:", netErr.Op)
}
// ── errors.Unwrap — get the next error in the chain ──
inner := errors.Unwrap(err) // nil if not wrapped
Custom Error Types
// Custom error type with structured data
type ValidationError struct {
Field string
Message string
Value any
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s (got %v)",
e.Field, e.Message, e.Value)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "must be between 0 and 150",
Value: age,
}
}
return nil
}
// Callers can extract the structured error
err := validateAge(-5)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("bad field: %s, value: %v\n", ve.Field, ve.Value)
}
// Custom error with wrapping (implements Unwrap)
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
func (e *AppError) Unwrap() error {
return e.Err
}
Sentinel Errors vs Error Types
// Package-level error values
var (
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
ErrConflict = errors.New("conflict")
)
// Check with errors.Is
if errors.Is(err, ErrNotFound) {
w.WriteHeader(404)
}
// Structured error types
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %s not found",
e.Resource, e.ID)
}
// Check with errors.As
var nfe *NotFoundError
if errors.As(err, &nfe) {
log.Printf("missing: %s", nfe.ID)
}
| Approach | Use When | Check With |
|---|---|---|
| Sentinel errors | The condition matters, but no extra data is needed | errors.Is(err, ErrNotFound) |
| Error types | Callers need structured data from the error | errors.As(err, &target) |
| Opaque errors | Callers only need to know if it failed | err != nil |
Error Wrapping Chains
Wrap errors at each layer to build a trail of context. This creates a chain that errors.Is and errors.As can walk.
// Layer 1: Repository
func (r *UserRepo) FindByID(id string) (*User, error) {
row := r.db.QueryRow("SELECT ... WHERE id = $1", id)
if err := row.Scan(&user); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return &user, nil
}
// Layer 2: Service
func (s *UserService) GetProfile(id string) (*Profile, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("get profile: %w", err)
}
return buildProfile(user), nil
}
// Layer 3: Handler
func (h *Handler) HandleGetProfile(w http.ResponseWriter, r *http.Request) {
profile, err := h.svc.GetProfile(userID)
if err != nil {
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(profile)
}
// Full error message: "get profile: query user abc123: connection refused"
// errors.Is(err, ErrNotFound) works through the entire chain
Best Practices
- Always check errors. Never use
_to discard an error unless you can genuinely justify why failure is acceptable. - Add context when wrapping. Each layer should add what it was trying to do:
fmt.Errorf("parsing config: %w", err). - Do not log and return. Either handle the error (log + recover) or wrap and return it. Doing both creates duplicate log entries.
- Use sentinel errors for conditions. Export package-level
var ErrXxx = errors.New("...")for errors callers need to check. - Return early. Handle the error case first and return, keeping the happy path un-indented.
- Avoid panic in libraries. Reserve
panicfor truly unrecoverable states (programmer errors). Libraries should return errors. - Wrap at boundaries. When calling into another package, wrap with context. When returning from a public API, decide whether to expose the inner error chain.
if err != nil {
log.Println("failed:", err)
return err // caller logs again!
}
if err != nil {
return fmt.Errorf("connecting to db: %w", err)
}
// let the top-level handler decide to log
data, err := fetch()
if err == nil {
parsed, err := parse(data)
if err == nil {
// happy path buried deep
}
}
data, err := fetch()
if err != nil {
return err
}
parsed, err := parse(data)
if err != nil {
return err
}
// happy path at top level
Modules & Tooling
Go modules are the standard for dependency management. Master go.mod, workspaces, cross-compilation, and the essential toolchain.
go.mod Structure
Every Go project starts with a go.mod file at the module root. It declares the module path, Go version, and all dependencies.
// go.mod — the module manifest
module github.com/yourname/myproject
go 1.23
// Direct dependencies
require (
github.com/gin-gonic/gin v1.9.1
github.com/jackc/pgx/v5 v5.5.1
golang.org/x/sync v0.6.0
)
// Indirect dependencies (managed automatically by go mod tidy)
require (
github.com/bytedance/sonic v1.10.2 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
// Replace a dependency with a local copy or fork
replace github.com/broken/pkg => ../my-local-fix
// Replace with a specific fork
replace github.com/original/lib => github.com/myfork/lib v1.2.3
// Retract versions that should not be used
retract (
v1.0.0 // Published with critical bug
[v1.1.0, v1.2.0] // Accidental range with breaking changes
)
| Directive | Purpose |
|---|---|
module |
Declares the module's import path (usually a repository URL) |
go |
Minimum Go version required. Enables language features up to that version |
require |
Lists dependencies with their minimum version (MVS — Minimum Version Selection) |
replace |
Redirects a module to a fork, local path, or different version |
exclude |
Prevents a specific module version from being used |
retract |
Marks versions of your own module that should not be used |
Module Commands
| Command | Description |
|---|---|
go mod init <path> |
Initialize a new module, creating go.mod |
go mod tidy |
Add missing and remove unused dependencies. Run after changing imports |
go mod vendor |
Copy dependencies into a vendor/ directory for offline builds |
go mod download |
Download modules to local cache without building |
go mod graph |
Print the module dependency graph |
go mod why <pkg> |
Explain why a package is needed (shortest dependency chain) |
go mod edit -json |
Print go.mod as JSON. Also supports -require, -replace, -dropreplace |
go get <pkg>@v1.2.3 |
Add or upgrade a dependency to a specific version |
go get <pkg>@latest |
Upgrade to the latest version |
go get -u ./... |
Update all dependencies to their latest minor/patch versions |
# Common workflow: start a new project
mkdir myproject && cd myproject
go mod init github.com/yourname/myproject
# Add a dependency (automatically updates go.mod and go.sum)
go get github.com/gin-gonic/gin@latest
# Clean up after removing imports
go mod tidy
# Check why a dependency exists
go mod why golang.org/x/crypto
# Vendor dependencies for reproducible builds
go mod vendor
go build -mod=vendor ./...
Semantic Versioning
Go modules enforce semantic versioning (semver). The import compatibility rule is fundamental: "If an old package and a new package have the same import path, the new package must be backward-compatible with the old package."
| Version Range | Rules | Import Path |
|---|---|---|
| v0.x.x | Unstable / pre-release. No compatibility guarantees. Breaking changes allowed | github.com/user/pkg |
| v1.x.x | Stable. Patch and minor versions must be backward-compatible | github.com/user/pkg |
| v2+ | Major version bump. May contain breaking changes. Requires version suffix in path | github.com/user/pkg/v2 |
// Importing different major versions of the same module
import (
v1 "github.com/user/pkg" // v1.x.x
v2 "github.com/user/pkg/v2" // v2.x.x — different import path
)
// In go.mod, major version suffix is part of the module path
require github.com/jackc/pgx/v5 v5.5.1
module directive in go.mod and in all import paths. This allows v1 and v2 to coexist in the same build.
Workspaces (go.work)
Go workspaces (Go 1.18+) let you work on multiple modules simultaneously without replace directives. Ideal for multi-module repos or developing a library alongside its consumer.
// go.work — workspace manifest
go 1.23
use (
./api
./shared
./worker
)
# Initialize a workspace
go work init ./api ./shared ./worker
# Add another module to the workspace
go work use ./newmodule
# Sync workspace with module dependencies
go work sync
# Build all modules in the workspace
go build ./...
go.work file is typically for local development. Add it to .gitignore unless your entire repository is a structured multi-module workspace. CI/CD should use go.mod with proper versioned dependencies.
Build & Cross-Compilation
Go compiles to static binaries and natively supports cross-compilation. Set GOOS and GOARCH environment variables to target any supported platform.
| GOOS | GOARCH | Target |
|---|---|---|
linux |
amd64 |
Linux x86-64 (servers, containers) |
linux |
arm64 |
Linux ARM64 (AWS Graviton, RPi 4) |
darwin |
arm64 |
macOS Apple Silicon (M1/M2/M3/M4) |
darwin |
amd64 |
macOS Intel |
windows |
amd64 |
Windows x86-64 |
js |
wasm |
WebAssembly |
wasip1 |
wasm |
WASI Preview 1 |
# Cross-compile for Linux from any OS
GOOS=linux GOARCH=amd64 go build -o myapp-linux ./cmd/myapp
# Build for macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o myapp-macos ./cmd/myapp
# Build for Windows
GOOS=windows GOARCH=amd64 go build -o myapp.exe ./cmd/myapp
# List all supported platforms
go tool dist list
# Static binary with no CGO (important for containers)
CGO_ENABLED=0 GOOS=linux go build -o myapp ./cmd/myapp
Build tags conditionally include files in a build based on OS, architecture, or custom constraints.
//go:build linux && amd64
// +build linux,amd64 (old syntax, still works)
package mypackage
// This file is only compiled on linux/amd64
//go:build !windows
// This file is compiled on every OS except Windows
//go:build integration
// Custom tag — only built with: go test -tags=integration ./...
ldflags inject values at build time, commonly used for version information.
// main.go
package main
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
fmt.Printf("myapp %s (commit %s, built %s)\n", version, commit, date)
}
# Inject version info at build time
go build -ldflags "\
-X main.version=1.2.3 \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-s -w" \
-o myapp ./cmd/myapp
# -s strips symbol table
# -w strips DWARF debug info
# Both reduce binary size
Essential Tools
go fmt / gofmt
Canonical formatting. Tabs for indentation, spaces for alignment. Non-negotiable in Go.
# Format all files
go fmt ./...
# Format with simplifications
gofmt -s -w .
go vet
Static analysis that catches bugs the compiler misses: printf format mismatches, unreachable code, suspicious constructs.
# Vet all packages
go vet ./...
# Specific analyzer only
go vet -printf ./...
go doc
Browse documentation from the command line. Works for any installed package.
# Package overview
go doc fmt
# Specific function
go doc fmt.Fprintf
# Show source code
go doc -src fmt.Fprintf
go generate
Run code generation commands embedded in source files as //go:generate comments.
//go:generate stringer -type=Color
//go:generate mockgen -source=repo.go
# Run all generators
go generate ./...
golangci-lint is the standard meta-linter. It runs dozens of linters in parallel and is much faster than running them individually.
# Install
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Run with defaults (includes govet, errcheck, staticcheck, etc.)
golangci-lint run ./...
# Run specific linters only
golangci-lint run --enable=gosec,prealloc ./...
# .golangci.yml — project configuration
linters:
enable:
- govet
- errcheck
- staticcheck
- gosec
- gocritic
- prealloc
- exhaustive
disable:
- wsl
- godox
linters-settings:
govet:
enable-all: true
errcheck:
check-type-assertions: true
gocritic:
enabled-tags:
- diagnostic
- style
- performance
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- gosec
Race detector finds data races at runtime. Essential for concurrent code.
# Run tests with race detection (slower but catches races)
go test -race ./...
# Build with race detector embedded
go build -race -o myapp ./cmd/myapp
# Race detector works at runtime — exercise concurrent paths in tests
go fmt (check formatting), go vet (catch bugs), golangci-lint run (extended linting), go test -race -cover ./... (tests with race detection and coverage).
Testing
Go has a powerful built-in testing framework. No external test runner needed — just go test.
Test Basics
Test files live alongside the code they test, named *_test.go. Test functions start with Test and receive a *testing.T.
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, want %d", got, want)
}
}
func TestDivide(t *testing.T) {
got, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != 5.0 {
t.Errorf("Divide(10, 2) = %f, want 5.0", got)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("expected error for division by zero, got nil")
}
}
| Method | Behavior |
|---|---|
t.Error(args...) |
Log failure and continue running the test |
t.Errorf(format, args...) |
Formatted failure message, continue running |
t.Fatal(args...) |
Log failure and stop the test immediately |
t.Fatalf(format, args...) |
Formatted failure, stop immediately |
t.Log(args...) |
Log info (only shown with -v flag or on failure) |
t.Skip(args...) |
Skip this test (e.g., missing dependency) |
# Run all tests in current module
go test ./...
# Run tests in a specific package
go test ./internal/auth/
# Verbose output (shows t.Log, pass/fail per test)
go test -v ./...
# Run only tests matching a regex
go test -run TestDivide ./...
# Short mode (tests can check testing.Short() to skip long tests)
go test -short ./...
Table-Driven Tests
The idiomatic Go testing pattern. Define test cases as a slice of structs and iterate with subtests. This makes it easy to add cases and produces clear output.
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"mixed signs", -1, 5, 4},
{"zeros", 0, 0, 0},
{"large numbers", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}
// Table-driven test with error cases
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"normal division", 10, 2, 5, false},
{"integer result", 9, 3, 3, false},
{"fractional result", 1, 3, 0.3333333333333333, false},
{"divide by zero", 10, 0, 0, true},
{"zero numerator", 0, 5, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Fatalf("Divide(%v, %v) error = %v, wantErr %v",
tt.a, tt.b, err, tt.wantErr)
}
if !tt.wantErr && got != tt.want {
t.Errorf("Divide(%v, %v) = %v, want %v",
tt.a, tt.b, got, tt.want)
}
})
}
}
// Run a specific subtest:
// go test -run TestDivide/divide_by_zero ./...
Test Helpers
// t.Helper() marks a function as a test helper.
// When it fails, the error points to the caller, not the helper.
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestStuff(t *testing.T) {
assertEqual(t, Add(1, 2), 3) // failure points here, not inside assertEqual
}
// t.Cleanup() registers a function to run when the test finishes.
// Cleanups run in LIFO order, even if the test fails or panics.
func TestWithTempDir(t *testing.T) {
dir := t.TempDir() // automatically cleaned up
// ... use dir ...
}
func setupDatabase(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("opening db: %v", err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
The testdata/ directory is a special convention. Go tooling ignores it during builds, making it perfect for test fixtures.
// testdata/ directory — ignored by go build, perfect for fixtures
// mypackage/
// parser.go
// parser_test.go
// testdata/
// valid_input.json
// expected_output.json
// edge_case.json
func TestParser(t *testing.T) {
input, err := os.ReadFile("testdata/valid_input.json")
if err != nil {
t.Fatalf("reading test fixture: %v", err)
}
got, err := Parse(input)
if err != nil {
t.Fatalf("Parse() error: %v", err)
}
want, err := os.ReadFile("testdata/expected_output.json")
if err != nil {
t.Fatalf("reading expected output: %v", err)
}
if !bytes.Equal(got, want) {
t.Errorf("output mismatch:\ngot: %s\nwant: %s", got, want)
}
}
Parallel Tests
Call t.Parallel() to run a test concurrently with other parallel tests. This can significantly speed up I/O-bound or slow test suites.
func TestFetchUser(t *testing.T) {
t.Parallel() // this test runs in parallel with other parallel tests
user, err := FetchUser("abc123")
if err != nil {
t.Fatalf("FetchUser: %v", err)
}
if user.Name == "" {
t.Error("expected non-empty name")
}
}
// Table-driven + parallel: capture the loop variable correctly
func TestSlugify(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"simple", "Hello World", "hello-world"},
{"special chars", "Go is #1!", "go-is-1"},
{"unicode", "Gopher Cafe", "gopher-cafe"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // each subtest runs in parallel
got := Slugify(tt.input)
if got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q",
tt.input, got, tt.want)
}
})
}
}
tt variable. Shared setup should use sync.Once or be done before calling t.Parallel(). Set max parallelism with go test -parallel 4.
Benchmarks
Benchmark functions measure performance. They live in _test.go files and start with Benchmark.
func BenchmarkAdd(b *testing.B) {
for b.Loop() { // Go 1.24+: preferred over range b.N
Add(42, 58)
}
}
// Pre-1.24 style (still common)
func BenchmarkAddClassic(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(42, 58)
}
}
// Benchmark with setup that shouldn't be timed
func BenchmarkParse(b *testing.B) {
data, err := os.ReadFile("testdata/large.json")
if err != nil {
b.Fatalf("setup: %v", err)
}
b.ResetTimer() // exclude setup time
b.ReportAllocs() // report memory allocations
for b.Loop() {
Parse(data)
}
}
// Sub-benchmarks for comparing approaches
func BenchmarkConcat(b *testing.B) {
b.Run("plus", func(b *testing.B) {
for b.Loop() {
_ = "hello" + " " + "world"
}
})
b.Run("fmt", func(b *testing.B) {
for b.Loop() {
_ = fmt.Sprintf("%s %s", "hello", "world")
}
})
b.Run("builder", func(b *testing.B) {
for b.Loop() {
var buf strings.Builder
buf.WriteString("hello")
buf.WriteString(" ")
buf.WriteString("world")
_ = buf.String()
}
})
}
# Run benchmarks (tests run too unless you filter)
go test -bench=. ./...
# Run only benchmarks, skip tests
go test -bench=. -run=^$ ./...
# Run specific benchmark
go test -bench=BenchmarkConcat ./...
# With memory stats
go test -bench=. -benchmem ./...
# Longer benchmark runs for more stable results
go test -bench=. -benchtime=5s ./...
# Compare benchmarks with benchstat
go test -bench=. -count=10 ./... > old.txt
# ... make optimization ...
go test -bench=. -count=10 ./... > new.txt
benchstat old.txt new.txt
Examples & Coverage
Example functions serve as both documentation and verified tests. They appear in go doc output and are executed during go test.
// Example functions — verified documentation
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
func ExampleDivide() {
result, err := Divide(10, 3)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%.2f\n", result)
// Output: 3.33
}
// Example for a specific method
func ExampleUser_FullName() {
u := User{First: "Rob", Last: "Pike"}
fmt.Println(u.FullName())
// Output: Rob Pike
}
// Unordered output (for maps, goroutines, etc.)
func ExampleKeys() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
for _, k := range Keys(m) {
fmt.Println(k)
}
// Unordered output:
// a
// b
// c
}
# Run tests with coverage percentage
go test -cover ./...
# Generate a coverage profile
go test -coverprofile=coverage.out ./...
# View coverage in browser (opens HTML report)
go tool cover -html=coverage.out
# Show coverage per function
go tool cover -func=coverage.out
# Coverage for specific packages only
go test -coverpkg=./internal/... -coverprofile=coverage.out ./...
Testify
github.com/stretchr/testify is the most popular assertion library. It provides readable assertions, requirement checks, and mocking.
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
// assert — logs failure but continues the test
assert.Equal(t, 5, Add(2, 3))
assert.NotNil(t, user)
assert.Contains(t, "hello world", "world")
assert.Len(t, items, 3)
// require — stops the test on failure (like t.Fatal)
user, err := GetUser("abc")
require.NoError(t, err) // stop here if error
require.NotNil(t, user) // stop here if nil
assert.Equal(t, "Alice", user.Name) // continue even if this fails
}
| Assertion | Description |
|---|---|
assert.Equal(t, want, got) |
Deep equality check |
assert.NotEqual(t, a, b) |
Not equal |
assert.NoError(t, err) |
Error is nil |
assert.Error(t, err) |
Error is not nil |
assert.ErrorIs(t, err, target) |
Wraps errors.Is |
assert.Nil(t, obj) |
Value is nil |
assert.NotNil(t, obj) |
Value is not nil |
assert.True(t, cond) |
Boolean is true |
assert.Contains(t, s, sub) |
String/slice/map contains element |
assert.Len(t, obj, n) |
Collection has length n |
assert.Empty(t, obj) |
String/slice/map is empty |
assert.JSONEq(t, want, got) |
JSON strings are semantically equal |
// Table-driven tests with testify
func TestSlugify(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"basic", "Hello World", "hello-world"},
{"special", "Go & Rust!", "go-rust"},
{"spaces", " trim me ", "trim-me"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Slugify(tt.input)
assert.Equal(t, tt.want, got,
"Slugify(%q) should produce correct slug", tt.input)
})
}
}
// Test suites with testify/suite
import "github.com/stretchr/testify/suite"
type UserServiceSuite struct {
suite.Suite
db *sql.DB
svc *UserService
}
func (s *UserServiceSuite) SetupSuite() {
// Runs once before all tests in the suite
s.db = setupTestDB(s.T())
s.svc = NewUserService(s.db)
}
func (s *UserServiceSuite) TearDownSuite() {
s.db.Close()
}
func (s *UserServiceSuite) SetupTest() {
// Runs before each test — clean slate
s.db.Exec("DELETE FROM users")
}
func (s *UserServiceSuite) TestCreateUser() {
user, err := s.svc.Create("Alice", "alice@test.com")
s.Require().NoError(err)
s.Assert().Equal("Alice", user.Name)
s.Assert().NotZero(user.ID)
}
func (s *UserServiceSuite) TestGetUser() {
created, _ := s.svc.Create("Bob", "bob@test.com")
found, err := s.svc.GetByID(created.ID)
s.Require().NoError(err)
s.Assert().Equal("Bob", found.Name)
}
// Run the suite
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}
require for preconditions that must pass for the rest of the test to be meaningful (e.g., no error from setup). Use assert for the actual assertions you want to check — this lets you see all failures at once instead of stopping at the first one.
Standard Library
Go's standard library is famously comprehensive. These are the packages you'll use in nearly every project.
fmt
Formatted I/O. The Printf family is the workhorse for output, logging, and string building.
| Verb | Description | Example Output |
|---|---|---|
%v |
Default format | {Alice 30} |
%+v |
Default with field names | {Name:Alice Age:30} |
%#v |
Go syntax representation | main.User{Name:"Alice", Age:30} |
%T |
Type of the value | main.User |
%d |
Integer (base 10) | 42 |
%b |
Integer (base 2) | 101010 |
%x |
Integer (base 16, lowercase) | 2a |
%s |
String | hello |
%q |
Quoted string | "hello" |
%f |
Float (default precision) | 3.141593 |
%.2f |
Float (2 decimal places) | 3.14 |
%e |
Scientific notation | 3.141593e+00 |
%p |
Pointer address | 0xc0000b4000 |
%w |
Wrap error (Errorf only) | wraps for errors.Is/errors.As |
// Print family
fmt.Println("hello", "world") // hello world\n
fmt.Printf("name: %s, age: %d\n", name, age)
fmt.Print("no newline")
// Sprint family — return string instead of printing
s := fmt.Sprintf("user %s (#%d)", name, id)
msg := fmt.Sprintf("%.2f%%", 99.5) // "99.50%"
// Fprint family — write to any io.Writer
fmt.Fprintf(os.Stderr, "error: %v\n", err)
fmt.Fprintf(w, "HTTP response: %s", body)
// Errorf — create formatted errors (with optional wrapping)
err := fmt.Errorf("read config %s: %w", path, err)
strings & strconv
import "strings"
strings.Contains("seafood", "foo") // true
strings.HasPrefix("gopher", "go") // true
strings.HasSuffix("main.go", ".go") // true
strings.ToUpper("hello") // "HELLO"
strings.ToLower("HELLO") // "hello"
strings.TrimSpace(" hi ") // "hi"
strings.Trim("***hi***", "*") // "hi"
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.SplitN("a,b,c", ",", 2) // ["a", "b,c"]
strings.Join([]string{"a","b","c"}, "-") // "a-b-c"
strings.Replace("oink oink", "oink", "moo", 1) // "moo oink"
strings.ReplaceAll("oink oink", "oink", "moo") // "moo moo"
strings.Count("cheese", "e") // 3
strings.Index("chicken", "ken") // 4
strings.Repeat("na", 4) // "nananana"
// strings.Builder — efficient string concatenation
var b strings.Builder
for i := 0; i < 100; i++ {
fmt.Fprintf(&b, "item %d\n", i)
}
result := b.String()
import "strconv"
// String ↔ Integer
strconv.Itoa(42) // "42"
n, err := strconv.Atoi("42") // 42, nil
n, err := strconv.Atoi("nope") // 0, error
// String ↔ Float
f, err := strconv.ParseFloat("3.14", 64) // 3.14, nil
s := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
// String ↔ Bool
b, err := strconv.ParseBool("true") // true, nil
s := strconv.FormatBool(true) // "true"
// String ↔ Integer (with base and bit size)
n, err := strconv.ParseInt("ff", 16, 64) // 255, nil
s := strconv.FormatInt(255, 16) // "ff"
io & bufio
The io.Reader and io.Writer interfaces are the backbone of Go I/O. Nearly everything composes through them.
// The two fundamental interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Copy from any Reader to any Writer
n, err := io.Copy(dst, src) // e.g., file to HTTP response
// Read everything into memory
data, err := io.ReadAll(resp.Body)
// Compose readers
r := io.LimitReader(src, 1024*1024) // read at most 1MB
r := io.TeeReader(src, os.Stdout) // read and tee to stdout
// Pipe — synchronous in-memory pipe
pr, pw := io.Pipe()
go func() {
defer pw.Close()
json.NewEncoder(pw).Encode(data)
}()
// pr now streams the JSON as it's written
import "bufio"
// Scanner — read line by line (default) or by custom split
file, _ := os.Open("data.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// Scanner with custom split function
scanner.Split(bufio.ScanWords) // split by words instead of lines
// Buffered writer — reduces system calls
w := bufio.NewWriter(file)
fmt.Fprintf(w, "buffered write: %d\n", 42)
w.Flush() // don't forget to flush!
encoding/json
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if empty
Password string `json:"-"` // never marshal
CreatedAt time.Time `json:"created_at"`
}
// Marshal — struct → JSON bytes
user := User{ID: 1, Name: "Alice", Email: "alice@test.com"}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","email":"alice@test.com","created_at":"..."}
// Pretty print
data, err := json.MarshalIndent(user, "", " ")
// Unmarshal — JSON bytes → struct
var u User
err := json.Unmarshal(data, &u)
// Unmarshal into a map (dynamic JSON)
var m map[string]any
err := json.Unmarshal(data, &m)
name := m["name"].(string)
// Streaming with json.Decoder (efficient for HTTP bodies, files)
func decodeUsers(r io.Reader) ([]User, error) {
var users []User
dec := json.NewDecoder(r)
if err := dec.Decode(&users); err != nil {
return nil, err
}
return users, nil
}
// In an HTTP handler
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
// ... process user ...
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Custom marshaling
type Status int
const (
Active Status = iota
Inactive
)
func (s Status) MarshalJSON() ([]byte, error) {
names := map[Status]string{Active: "active", Inactive: "inactive"}
return json.Marshal(names[s])
}
func (s *Status) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
switch name {
case "active":
*s = Active
case "inactive":
*s = Inactive
default:
return fmt.Errorf("unknown status: %s", name)
}
return nil
}
net/http
Go's HTTP package is production-ready out of the box. Go 1.22+ added method-based routing to the default ServeMux.
// Basic HTTP server (Go 1.22+ enhanced routing)
mux := http.NewServeMux()
// Method + path patterns (Go 1.22+)
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser) // path parameter
mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Path parameter extraction
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+
// ... look up user by id ...
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Production server with timeouts
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Println("listening on :8080")
log.Fatal(server.ListenAndServe())
// Middleware pattern
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Apply middleware
handler := loggingMiddleware(mux)
// HTTP client with timeout (never use http.DefaultClient in production)
client := &http.Client{
Timeout: 10 * time.Second,
}
// Simple GET
resp, err := client.Get("https://api.example.com/data")
if err != nil {
return fmt.Errorf("GET request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var result Data
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
// POST with JSON body
payload, _ := json.Marshal(user)
resp, err := client.Post(
"https://api.example.com/users",
"application/json",
bytes.NewReader(payload),
)
// Custom request with headers
req, err := http.NewRequestWithContext(ctx, "PUT", url, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
http.Client and http.Server have no timeouts. In production, always configure ReadTimeout, WriteTimeout, and client Timeout to prevent resource leaks from slow or stalled connections.
slog (Go 1.21+)
Structured logging in the standard library. Replaces log for any production application.
import "log/slog"
// Default text logger
slog.Info("server started", "port", 8080)
// 2024-01-15 10:30:00 INFO server started port=8080
slog.Warn("slow query", "duration", time.Since(start), "query", q)
slog.Error("request failed", "err", err, "method", r.Method)
// JSON handler (for production / log aggregators)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("user created", "id", user.ID, "email", user.Email)
// {"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"user created","id":42,"email":"alice@test.com"}
// Log levels
slog.Debug("verbose detail") // hidden at default level
slog.Info("normal operation")
slog.Warn("something unusual")
slog.Error("something failed")
// Structured attributes and groups
slog.Info("request",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
slog.Duration("latency", duration),
slog.Group("user",
slog.Int("id", user.ID),
slog.String("role", user.Role),
),
)
// ... "user":{"id":42,"role":"admin"} ...
// Logger with persistent context (child loggers)
reqLogger := slog.With("request_id", requestID, "user_id", userID)
reqLogger.Info("processing order", "order_id", orderID)
reqLogger.Error("payment failed", "err", err)
// Dynamic log level
var level slog.LevelVar
level.Set(slog.LevelDebug) // change at runtime
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: &level,
})
time
Go uses a unique reference time for formatting: Mon Jan 2 15:04:05 MST 2006 (1/2 3:4:5 6 7). Memorize it by the mnemonic: 01/02 03:04:05 PM '06 -0700.
import "time"
// Current time
now := time.Now()
fmt.Println(now) // 2024-01-15 10:30:00.123456 -0800 PST
// Duration
elapsed := time.Since(start) // time.Duration since start
fmt.Printf("took %v\n", elapsed)
fmt.Printf("took %dms\n", elapsed.Milliseconds())
d := 5*time.Second + 500*time.Millisecond
time.Sleep(d)
// Formatting — use the reference time as a template
now := time.Now()
now.Format("2006-01-02") // "2024-01-15"
now.Format("2006-01-02 15:04:05") // "2024-01-15 10:30:00"
now.Format(time.RFC3339) // "2024-01-15T10:30:00-08:00"
now.Format("Mon, 02 Jan 2006") // "Mon, 15 Jan 2024"
now.Format("3:04 PM") // "10:30 AM"
// Parsing — same reference time approach
t, err := time.Parse("2006-01-02", "2024-01-15")
t, err := time.Parse(time.RFC3339, "2024-01-15T10:30:00Z")
t, err := time.ParseInLocation("2006-01-02", "2024-01-15", time.UTC)
// Timers and tickers
// Timer — fires once after duration
timer := time.NewTimer(5 * time.Second)
<-timer.C // blocks until timer fires
// AfterFunc — run function after delay
time.AfterFunc(2*time.Second, func() {
fmt.Println("delayed execution")
})
// Ticker — fires repeatedly at interval
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
fmt.Println("tick", time.Now())
// break when done
}
// Common operations
t1.Before(t2) // bool
t1.After(t2) // bool
t1.Equal(t2) // bool (handles monotonic clock)
t1.Add(24 * time.Hour) // time.Time (tomorrow)
t1.Sub(t2) // time.Duration (difference)
t1.Truncate(time.Hour) // round down to hour
t1.UTC() // convert to UTC
t1.In(time.FixedZone("EST", -5*3600)) // convert timezone
01/02 03:04:05 PM '06 -0700. Each component is a different digit: month=01, day=02, hour=03, minute=04, second=05, year=06, timezone=-0700. If you need 24-hour format, use 15 for the hour.
Ecosystem
Go's ecosystem favors focused, well-composed libraries over monolithic frameworks. These are the essential third-party packages.
Cobra — CLI Framework
github.com/spf13/cobra is the standard for building CLI applications. Used by kubectl, Hugo, GitHub CLI, and Docker.
// cmd/root.go — root command
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var verbose bool
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A brief description of your app",
Long: `A longer description with examples and usage.`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
// Persistent flags — available to this command and all subcommands
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
"enable verbose output")
// Add subcommands
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(migrateCmd)
}
// cmd/serve.go — subcommand
var port int
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the HTTP server",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Starting server on :%d\n", port)
return startServer(port)
},
}
func init() {
// Local flag — only for this command
serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "server port")
}
// cmd/migrate.go — subcommand with positional args
var migrateCmd = &cobra.Command{
Use: "migrate [up|down]",
Short: "Run database migrations",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
direction := args[0]
if direction != "up" && direction != "down" {
return fmt.Errorf("invalid direction: %s (use 'up' or 'down')", direction)
}
return runMigrations(direction)
},
}
// main.go
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}
// Usage:
// myapp serve --port 3000
// myapp migrate up --verbose
// myapp --help
Bubbletea — TUI Framework
github.com/charmbracelet/bubbletea builds terminal UIs using The Elm Architecture: Model, Update, View.
import (
tea "github.com/charmbracelet/bubbletea"
"fmt"
)
// Model holds all application state
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func initialModel() model {
return model{
choices: []string{"Buy groceries", "Wash dishes", "Do laundry"},
selected: make(map[int]struct{}),
}
}
// Init returns an initial command (or nil)
func (m model) Init() tea.Cmd {
return nil
}
// Update handles messages and returns updated model + optional command
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
if _, ok := m.selected[m.cursor]; ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
// View renders the UI as a string
func (m model) View() string {
s := "What should we buy?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
// Bubbletea commands — async operations
type statusMsg int
func checkServer() tea.Msg {
resp, err := http.Get("https://example.com")
if err != nil {
return errMsg{err}
}
return statusMsg(resp.StatusCode)
}
// In Update:
case tea.KeyMsg:
if msg.String() == "c" {
return m, checkServer // returns a tea.Cmd (function)
}
case statusMsg:
m.status = int(msg)
case errMsg:
m.err = msg.err
// Bubbles — prebuilt components
// github.com/charmbracelet/bubbles
// spinner, textinput, list, table, viewport, progress, paginator
tea.Cmd now accepts a context.Context.
Web Frameworks
Go 1.22+ made the standard library router much more capable. Third-party routers add middleware chains, route groups, and convenience.
// chi — lightweight, idiomatic, composable router
import "github.com/go-chi/chi/v5"
import "github.com/go-chi/chi/v5/middleware"
r := chi.NewRouter()
// Middleware stack
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Timeout(30 * time.Second))
// Routes
r.Get("/", homeHandler)
r.Post("/users", createUser)
// Route groups
r.Route("/api/v1", func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Route("/users/{userID}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
// Path parameter
func getUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
// ...
}
http.ListenAndServe(":3000", r)
| Feature | net/http (1.22+) | chi | Gin | Echo |
|---|---|---|---|---|
| Method routing | Yes (native) | Yes | Yes | Yes |
| Path parameters | {id} |
{id} |
:id |
:id |
| Middleware | Manual wrapping | Stack-based | Stack-based | Stack-based |
| Route groups | No | Yes | Yes | Yes |
| Handler signature | http.Handler |
http.Handler |
gin.HandlerFunc |
echo.HandlerFunc |
| stdlib compatible | Yes | Yes (100%) | Partial | Partial |
| Dependencies | Zero | Zero | Many | Some |
| Best for | Simple APIs | Most projects | Speed-focused APIs | Feature-rich APIs |
net/http for simple projects. Reach for chi when you need route groups and middleware chains — it's 100% compatible with net/http handlers, so you never lock into a proprietary API.
Database — GORM vs sqlx
// ── GORM — full ORM ──
import "gorm.io/gorm"
import "gorm.io/driver/postgres"
// Model definition
type User struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"size:100;not null"`
Email string `gorm:"uniqueIndex;not null"`
Posts []Post `gorm:"foreignKey:AuthorID"`
}
type Post struct {
gorm.Model
Title string
Body string
AuthorID uint
}
// Connect
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Auto-migrate (create/update tables)
db.AutoMigrate(&User{}, &Post{})
// CRUD
db.Create(&User{Name: "Alice", Email: "alice@test.com"})
var user User
db.First(&user, 1) // by primary key
db.Where("email = ?", email).First(&user) // by condition
db.Model(&user).Update("Name", "Bob")
db.Model(&user).Updates(User{Name: "Bob", Email: "bob@test.com"})
db.Delete(&user, 1)
// Queries
var users []User
db.Where("name LIKE ?", "%alice%").
Order("created_at DESC").
Limit(10).
Find(&users)
// Preload associations
db.Preload("Posts").Find(&users)
// ── sqlx — enhanced database/sql ──
import "github.com/jmoiron/sqlx"
import _ "github.com/lib/pq"
db, err := sqlx.Connect("postgres", dsn)
// Struct scanning (no manual Scan calls)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
// Get single row into struct
var user User
err := db.Get(&user, "SELECT * FROM users WHERE id = $1", 42)
// Select multiple rows into slice
var users []User
err := db.Select(&users, "SELECT * FROM users WHERE active = $1", true)
// Named queries — use struct field names
_, err := db.NamedExec(
`INSERT INTO users (name, email) VALUES (:name, :email)`,
user,
)
// Named query with map
_, err := db.NamedExec(
`UPDATE users SET name = :name WHERE id = :id`,
map[string]any{"name": "Alice", "id": 42},
)
// Transactions
tx, err := db.Beginx()
tx.NamedExec(`INSERT INTO users ...`, user)
tx.NamedExec(`INSERT INTO audit_log ...`, entry)
tx.Commit() // or tx.Rollback()
| Aspect | GORM | sqlx |
|---|---|---|
| Approach | Full ORM (Active Record) | Enhanced database/sql |
| SQL knowledge | Optional (query builder) | Required (you write SQL) |
| Migrations | Built-in AutoMigrate | Use external tool (goose, migrate) |
| Associations | Built-in (HasMany, BelongsTo) | Manual JOINs |
| Performance | Good (some overhead) | Near raw SQL |
| Best for | Rapid CRUD, complex relations | Full SQL control, performance-critical |
Viper — Configuration
github.com/spf13/viper handles configuration from files, environment variables, flags, and remote systems. Often paired with Cobra.
import "github.com/spf13/viper"
// Set defaults
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.timeout", "30s")
viper.SetDefault("database.max_conns", 10)
// Read config file
viper.SetConfigName("config") // config.yaml, config.json, etc.
viper.SetConfigType("yaml")
viper.AddConfigPath(".") // search current directory
viper.AddConfigPath("$HOME/.myapp")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found — use defaults
} else {
log.Fatalf("reading config: %v", err)
}
}
// Environment variables
viper.SetEnvPrefix("MYAPP") // MYAPP_SERVER_PORT
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Access values
port := viper.GetInt("server.port")
timeout := viper.GetDuration("server.timeout")
debug := viper.GetBool("debug")
// Unmarshal into a struct (the recommended approach)
type Config struct {
Server struct {
Port int `mapstructure:"port"`
Timeout time.Duration `mapstructure:"timeout"`
} `mapstructure:"server"`
Database struct {
DSN string `mapstructure:"dsn"`
MaxConns int `mapstructure:"max_conns"`
} `mapstructure:"database"`
Debug bool `mapstructure:"debug"`
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatalf("unmarshaling config: %v", err)
}
// config.yaml
// server:
// port: 3000
// timeout: 30s
// database:
// dsn: postgres://localhost/myapp
// max_conns: 25
// debug: false
Set() calls, flags, environment variables, config file, key/value store, defaults. This means environment variables always override config file values.
Wire — Dependency Injection
github.com/google/wire provides compile-time dependency injection. It generates code — no runtime reflection or service locators.
// providers.go — provider functions return dependencies
func NewDatabase(cfg *Config) (*sql.DB, error) {
return sql.Open("postgres", cfg.DatabaseDSN)
}
func NewUserRepo(db *sql.DB) *UserRepo {
return &UserRepo{db: db}
}
func NewUserService(repo *UserRepo, logger *slog.Logger) *UserService {
return &UserService{repo: repo, logger: logger}
}
func NewServer(svc *UserService, cfg *Config) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("GET /users", svc.ListUsers)
return &http.Server{Addr: cfg.Addr, Handler: mux}
}
// wire.go — injector declaration (input for wire tool)
//go:build wireinject
package main
import "github.com/google/wire"
func InitializeServer(cfg *Config) (*http.Server, error) {
wire.Build(
NewDatabase,
NewUserRepo,
NewUserService,
NewLogger,
NewServer,
)
return nil, nil // wire replaces this
}
// Provider sets — group related providers
var DatabaseSet = wire.NewSet(NewDatabase, NewUserRepo)
var ServiceSet = wire.NewSet(DatabaseSet, NewUserService, NewLogger)
func InitializeServer(cfg *Config) (*http.Server, error) {
wire.Build(ServiceSet, NewServer)
return nil, nil
}
# Generate wire_gen.go (the actual DI code)
go install github.com/google/wire/cmd/wire@latest
wire ./...
# The generated wire_gen.go contains a real function:
func InitializeServer(cfg *Config) (*http.Server, error) {
db, err := NewDatabase(cfg)
if err != nil { return nil, err }
repo := NewUserRepo(db)
logger := NewLogger()
svc := NewUserService(repo, logger)
server := NewServer(svc, cfg)
return server, nil
}
main() work fine. Wire's advantage is that it catches missing or circular dependencies at compile time, not runtime.
Go Idioms
Go has strong opinions. These proverbs, patterns, and conventions define idiomatic Go code.
Go Proverbs
From Rob Pike's Go Proverbs talk. These aren't just slogans — they shape how Go code is written and reviewed.
Instead of using mutexes to protect shared state, send data through channels. Let goroutines own their data and communicate via message passing.
var mu sync.Mutex
var balance int
func Deposit(amount int) {
mu.Lock()
balance += amount
mu.Unlock()
}
type Bank struct {
deposits chan int
balances chan int
}
func (b *Bank) Run() {
balance := 0
for {
select {
case amount := <-b.deposits:
balance += amount
case b.balances <- balance:
}
}
}
Functions should accept interfaces for flexibility and return concrete types for clarity. The caller decides when abstraction is needed, not the implementer.
// Good: accept io.Reader (any data source works)
func Process(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
// ...
return &Result{Data: data}, nil
}
// Works with files, HTTP bodies, buffers, strings...
Process(os.Stdin)
Process(resp.Body)
Process(strings.NewReader("hello"))
Process(&bytes.Buffer{})
Errors aren't special control flow. They're regular values you can store, compare, wrap, and pass around. This enables patterns like error accumulation and programmatic error handling.
// Errors as values enable creative patterns
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // skip if already failed
}
_, ew.err = ew.w.Write(buf)
}
// Clean sequential writes — check error once at the end
ew := &errWriter{w: fd}
ew.write(header)
ew.write(body)
ew.write(footer)
if ew.err != nil {
return ew.err
}
Design types so their zero value (all fields at default) is immediately usable without a constructor.
// sync.Mutex — zero value is an unlocked mutex
var mu sync.Mutex // ready to use, no initialization needed
// bytes.Buffer — zero value is an empty buffer
var buf bytes.Buffer
buf.WriteString("hello")
// Your own types should follow this pattern
type Server struct {
Addr string // "" defaults handled in ListenAndServe
Handler http.Handler // nil means DefaultServeMux
Timeout time.Duration // 0 means no timeout
}
// Works with zero value
s := Server{}
s.ListenAndServe() // sensible defaults for all fields
Don't import a large package for one small utility function. Copy a few lines instead of adding a dependency with its own transitive dependencies.
// Don't import a utility package just for this:
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Note: Go 1.21+ added built-in min/max, but the principle
// applies broadly to small helper functions
Write code that's obvious at first read. Avoid clever one-liners, tricky bit manipulation, and obscure optimizations unless they're genuinely needed and well-commented.
// What does this do?
v = v[:0+copy(v[0:], v[i:])]
// Remove element at index i
v = append(v[:i], v[i+1:]...)
Small interfaces are powerful. io.Reader (one method) is implemented by hundreds of types. An interface with 20 methods is rarely reusable.
// Strong abstraction — small interface
type Stringer interface {
String() string
}
// Weak abstraction — too many methods
type Repository interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, opts ListOpts) ([]*User, error)
Count(ctx context.Context) (int, error)
// ... this interface is too specific to be reusable
}
// Better: compose small interfaces or use concrete types
type UserReader interface {
GetByID(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
}
Reserve panic for truly unrecoverable states — programmer errors, impossible conditions, failed invariants during initialization. Libraries should never panic; always return errors.
// Acceptable: program cannot continue without this
func mustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(fmt.Sprintf("invalid regex %q: %v", pattern, err))
}
return re
}
// Only use must* patterns for package-level initialization
var emailRe = mustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$`)
// Never panic in normal code paths — return an error instead
Project Layout
There's no official enforced layout, but the community has converged on a standard structure for non-trivial projects.
myproject/
├── cmd/ # Application entry points
│ ├── server/
│ │ └── main.go # go run ./cmd/server
│ └── worker/
│ └── main.go # go run ./cmd/worker
│
├── internal/ # Private packages (enforced by Go toolchain)
│ ├── auth/ # Authentication logic
│ │ ├── auth.go
│ │ └── auth_test.go
│ ├── database/ # Database layer
│ │ ├── postgres.go
│ │ └── migrations/
│ └── handler/ # HTTP handlers
│ ├── user.go
│ └── user_test.go
│
├── pkg/ # Public packages (importable by other projects)
│ └── validator/ # Reusable validation utilities
│ ├── validator.go
│ └── validator_test.go
│
├── api/ # API definitions (OpenAPI, protobuf)
│ └── openapi.yaml
│
├── go.mod
├── go.sum
├── Makefile
└── README.md
internal/ can only be imported by code in the parent of internal/. This is enforced by the Go compiler. Use it to hide implementation details from external consumers. Packages in internal/ are your private API — refactor freely without breaking others.
| Directory | Purpose | Importable Externally? |
|---|---|---|
cmd/ |
Each subdirectory is a main package (one binary per dir) |
No (main packages) |
internal/ |
Private application code — enforced access restriction | No (compiler-enforced) |
pkg/ |
Public library code safe for external use | Yes |
Package naming conventions:
- Short, lowercase, no underscores:
http,json,auth,user - Singular nouns:
usernotusers,modelnotmodels - No stutter:
http.Servernothttp.HTTPServer - Avoid
util,common,helpers: these are meaningless — put code where it belongs - Package name is part of the API: callers write
auth.NewClient(), so name accordingly
Best Practices
init() functions run automatically when a package is imported. Use them sparingly.
// init() runs automatically — no explicit call needed
func init() {
// Acceptable uses:
// - Register database drivers
// - Set up package-level regex or lookup tables
// - Initialize default loggers
}
// Avoid init() for:
// - Complex logic that can fail (no error return)
// - Side effects that make testing hard
// - Anything that depends on runtime configuration
// Better alternative: explicit initialization
func Setup(cfg *Config) error {
// ... can return errors, accepts configuration
return nil
}
go:embed (Go 1.16+) embeds static files directly into the binary.
import "embed"
//go:embed templates/*.html
var templates embed.FS
//go:embed static/*
var staticFiles embed.FS
//go:embed version.txt
var version string // single file as string
//go:embed schema.sql
var schema []byte // single file as bytes
// Use with http.FileServer
http.Handle("/static/",
http.FileServer(http.FS(staticFiles)))
// Use with html/template
tmpl, err := template.ParseFS(templates, "templates/*.html")
Effective error messages follow a consistent style.
return errors.New("failed")
return errors.New("Error occurred")
return fmt.Errorf("ERROR: could not read the file!")
return fmt.Errorf("ReadConfig: %w", err)
return errors.New("connection refused")
return fmt.Errorf("read config %s: %w", path, err)
return fmt.Errorf("parse user age: %w", err)
return fmt.Errorf("dial %s:%d: %w", host, port, err)
- Lowercase, no trailing punctuation
- Include relevant context (filename, ID, operation)
- Build a chain:
"fetch user 42: query db: connection refused" - Don't start with "failed to" or "error" — it's already an error
Comment conventions (godoc format):
// Package auth provides authentication and authorization
// middleware for HTTP servers.
package auth
// Authenticator validates credentials and returns a user identity.
// It returns ErrUnauthorized if the credentials are invalid.
type Authenticator interface {
Authenticate(ctx context.Context, token string) (*Identity, error)
}
// NewJWTAuth creates an Authenticator that validates JWT tokens
// using the provided signing key. The key must be at least 256 bits.
//
// Example:
//
// auth := NewJWTAuth([]byte(os.Getenv("JWT_SECRET")))
// identity, err := auth.Authenticate(ctx, token)
func NewJWTAuth(key []byte) *JWTAuth {
// ...
}
// ErrUnauthorized is returned when credentials are invalid or expired.
var ErrUnauthorized = errors.New("unauthorized")
// Deprecated: Use NewJWTAuth instead.
func NewAuth(key string) *Auth { ... }