Comprehensive Go language cheatsheet, written to be something you can actually keep open while coding.
It includes core syntax, language rules, standard patterns, and essential "Go-isms".
- Program Structure
- Building & Running
- Variables & Constants
- Primitive Types
- Type Conversions & Assertions
- Operators
- Strings
- Control Flow
- Functions
- Closures
- Pointers
- Arrays
- Slices
- Maps
- Structs
- Embedding
- Methods & Receivers
- Interfaces
- Generics
- Error Handling
defer,panic,recoverinitFunctions- Concurrency
- Channels
- Context
- Packages & Modules
- Visibility & Naming
- Memory & Zero Values
- Testing
- Common Standard Library
- Example Applications
- Common Gotchas
- Idiomatic Go Rules
package main // every executable must be in package main
import (
"fmt" // grouped imports (preferred over multiple import statements)
"os"
)
func main() { // entry point — takes no args, returns nothing
fmt.Println("Hello, Go")
os.Exit(0) // explicit exit code (rarely needed; 0 is default)
}- Every executable has
package mainandfunc main() - Imports are explicit — unused imports are a compile error
- Use
_to import for side effects only:import _ "net/http/pprof"
go run main.go # compile + run in one step (no binary produced)
go run . # run the package in the current directorygo build # builds binary named after module (in current dir)
go build -o myapp # specify output binary name
go build -o bin/myapp ./cmd/server # build a specific sub-packagego install # builds and places binary in $GOPATH/bin (or $GOBIN)
go install example.com/tool@latest # install a remote tool# Go cross-compiles with just two env vars — no extra toolchain needed
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=darwin GOARCH=arm64 go build -o myapp-mac
GOOS=windows GOARCH=amd64 go build -o myapp.exeCommon GOOS/GOARCH combos:
GOOS |
GOARCH |
Target |
|---|---|---|
linux |
amd64 |
Linux x86-64 |
linux |
arm64 |
Linux ARM (e.g., AWS Graviton) |
darwin |
amd64 |
macOS Intel |
darwin |
arm64 |
macOS Apple Silicon |
windows |
amd64 |
Windows x86-64 |
//go:build linux
// +build linux // old syntax (pre-1.17), still works
package mypackagego build -tags "integration" # include files with //go:build integration// main.go
var version = "dev" // default valuego build -ldflags "-X main.version=1.2.3" -o myapp
# sets the version variable at compile time| Flag | Purpose |
|---|---|
-o name |
output binary name |
-v |
verbose (print packages being compiled) |
-race |
enable race detector |
-ldflags "-s -w" |
strip debug info (smaller binary) |
-ldflags "-X pkg.Var=val" |
inject string variable at build time |
-tags "tag1 tag2" |
enable build tags |
-trimpath |
remove local file paths from binary |
- A single static binary — no runtime dependencies, no VM, no interpreter
- Includes the Go runtime and garbage collector
- Typical binary size: 5-15 MB (use
-ldflags "-s -w"to shrink) - Binary is ready to deploy — just copy it to the target machine
x := 10 // type inferred as int
name := "Jon" // type inferred as string
a, b := 1, "hello" // multiple assignmentvar x int = 10 // explicit type + value
var y = 20 // type inferred
var z int // zero-initialized (z == 0)
var a, b int // multiple vars, both zerovar Version = "1.0.0" // exported (uppercase)
var debug = false // unexported (lowercase)const Pi = 3.14159 // untyped constant — higher precision until assigned
const Timeout = 5 * time.Second
const (
StatusOK = 200
StatusNotOK = 400
)const (
Read = 1 << iota // 1
Write // 2
Execute // 4
)
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)- Constants must be compile-time evaluable
- No
constfor slices, maps, or structs - Untyped constants adapt to context:
const x = 5works as int, float64, etc.
int, int8, int16, int32, int64 // signed integers
uint, uint8, uint16, uint32, uint64 // unsigned integers
float32, float64 // IEEE 754 floats
complex64, complex128 // complex numbers
uintptr // pointer-sized unsigned int (unsafe)
intanduintare platform-dependent: 32-bit on 32-bit systems, 64-bit on 64-bit.
byte // alias for uint8 — used for raw data
rune // alias for int32 — represents a Unicode code pointbool // true or false (zero value: false)
string // immutable UTF-8 byte sequence (zero value: "")| Type | Size | Range |
|---|---|---|
int8 |
1 byte | -128 to 127 |
int16 |
2 bytes | -32,768 to 32,767 |
int32 |
4 bytes | -2.1B to 2.1B |
int64 |
8 bytes | ±9.2 quintillion |
float32 |
4 bytes | ~7 decimal digits precision |
float64 |
8 bytes | ~15 decimal digits |
i := 42
f := float64(i) // int → float64
u := uint(f) // float64 → uint
s := string(rune(65)) // int → rune → string ("A")Go has no implicit conversions. Even
int32→int64requires explicit cast.
import "strconv"
s := strconv.Itoa(42) // int → string: "42"
i, err := strconv.Atoi("42") // string → int: 42
f, err := strconv.ParseFloat("3.14", 64) // string → float64var x any = "hello"
s := x.(string) // panics if x is not a string
s, ok := x.(string) // safe — ok is false if wrong type
if s, ok := x.(string); ok {
fmt.Println(s) // use s safely
}switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown: %T\n", v)
}+ - * / % // add, subtract, multiply, divide, modulo
++ -- // increment/decrement (statement only, not expression)
i++is a statement. You cannot writex = i++.
== != < <= > >=&& || ! // AND, OR, NOT (short-circuit evaluation)& // AND
| // OR
^ // XOR (also unary NOT)
<< // left shift
>> // right shift
&^ // AND NOT (bit clear)= := += -= *= /= %= &= |= ^= <<= >>=- ❌ No ternary operator (
? :) — useif/else - ❌ No operator overloading
s := "hello, world" // double quotes for interpreted strings
r := `raw \n string` // backticks for raw strings (no escape processing)len(s) // byte length (not rune count!)
utf8.RuneCountInString(s) // actual character count
s[0] // byte at index (not rune!)
s[1:4] // substring (byte indices)
s + " suffix" // concatenation (creates new string)for i, r := range s { // r is a rune, i is byte index
fmt.Printf("%d: %c\n", i, r)
}
// to iterate bytes:
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}import "strings"
strings.Contains(s, "llo") // true
strings.HasPrefix(s, "hel") // true
strings.HasSuffix(s, "rld") // true
strings.Index(s, "lo") // 3
strings.ToUpper(s) // "HELLO, WORLD"
strings.ToLower(s) // "hello, world"
strings.TrimSpace(" hi ") // "hi"
strings.Split("a,b,c", ",") // []string{"a", "b", "c"}
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.ReplaceAll(s, "l", "L") // "heLLo, worLd"
strings.Repeat("ha", 3) // "hahaha"var b strings.Builder // use Builder for many concatenations
b.WriteString("hello")
b.WriteString(" world")
s := b.String() // "hello world"Strings are immutable. Every concatenation allocates a new string. Use
strings.Builderorbytes.Bufferin loops.
if x > 0 {
// positive
} else if x == 0 {
// zero
} else {
// negative
}With initializer (scoped to if/else block):
if v, err := strconv.Atoi(s); err == nil {
fmt.Println(v)
} else {
fmt.Println("error:", err)
}
// v and err are NOT accessible here// classic C-style
for i := 0; i < 10; i++ {}
// while-style
for condition {}
// infinite loop
for {
break // exit loop
continue // skip to next iteration
}for i, v := range slice {} // index + value
for _, v := range slice {} // value only
for i := range slice {} // index only
for k, v := range aMap {} // key + value
for k := range aMap {} // key only
for i, r := range "hello" {} // i = byte index, r = rune
for range 5 {} // Go 1.22+: repeat 5 timesouter:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // skip to next outer iteration
}
if i == 2 {
break outer // exit both loops
}
}
}switch x {
case 1:
fmt.Println("one") // no fallthrough by default (unlike C)
case 2, 3:
fmt.Println("two or three") // multiple values
fallthrough // explicit fallthrough to next case
case 4:
fmt.Println("falls here from 2 or 3")
default:
fmt.Println("other")
}Expression-less switch (cleaner than if/else chains):
switch {
case x > 100:
fmt.Println("big")
case x > 10:
fmt.Println("medium")
default:
fmt.Println("small")
}func add(a int, b int) int { // basic function
return a + b
}
func add(a, b int) int { // shorthand when types match
return a + b
}func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // "naked return" — returns named values
}
result = a / b
return
}func sum(nums ...int) int { // nums is []int
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3)
sum(nums...) // spread a slicevar fn func(int) int // function variable
fn = func(x int) int { return x * 2 }
fmt.Println(fn(5)) // 10
// as parameter
func apply(f func(int) int, x int) int {
return f(x)
}func counter() func() int {
n := 0 // captured by the closure
return func() int {
n++ // mutates the captured variable
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3// WRONG — all goroutines share the same `i`
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }() // likely prints "5" five times
}
// CORRECT (Go <1.22) — pass as argument
for i := 0; i < 5; i++ {
go func(i int) { fmt.Println(i) }(i)
}
// Go 1.22+ — loop vars are per-iteration by default (fixed!)x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 — dereference to read
*p = 100 // modify x through the pointer
fmt.Println(x) // 100// 1. Mutate the caller's data
func double(n *int) {
*n *= 2
}
x := 5
double(&x)
fmt.Println(x) // 10
// 2. Avoid copying large structs
func process(u *User) { /* works on original, no copy */ }
// 3. Signal "optional" values (nil means absent)
func find(id int) *User {
if id == 0 {
return nil
}
return &User{ID: id}
}- ❌ No pointer arithmetic (use
unsafepackage if you must — you shouldn't) new(T)allocates zeroedTand returns*T:p := new(int) // *int, value 0- Pointers to local variables are fine — Go's escape analysis handles it
var a [3]int // [0, 0, 0] — zero-valued
b := [3]int{1, 2, 3} // [1, 2, 3]
c := [...]int{1, 2, 3} // compiler counts: [3]int- Fixed size — size is part of the type:
[3]int ≠ [4]int - Value types — assignment and function args copy the entire array
- Rarely used directly; use slices instead
A slice is a reference to a contiguous segment of an array: (pointer, length, capacity).
s := []int{1, 2, 3} // slice literal (no size = slice, not array)
s := make([]int, 5) // len=5, cap=5, all zeros
s := make([]int, 0, 10) // len=0, cap=10 (pre-allocate)s = append(s, 4) // may allocate a new backing array
s = append(s, 5, 6, 7) // append multiple
s = append(s, other...) // append another slicea := []int{0, 1, 2, 3, 4}
b := a[1:3] // [1, 2] — shares backing array with a
c := a[:3] // [0, 1, 2]
d := a[2:] // [2, 3, 4]
e := a[:] // full slice (shallow copy of header, same backing array)b := a[1:3:3] // [1, 2] — cap=2, prevents append from overwriting a's datadst := make([]int, len(src))
n := copy(dst, src) // returns number of elements copied (min of both lens)s = append(s[:i], s[i+1:]...) // remove element at index i
s = slices.Delete(s, i, i+1) // Go 1.21+ (preferred)var s []int // nil — len=0, cap=0, s == nil is true
s := []int{} // empty — len=0, cap=0, s == nil is false
// both work with len(), cap(), append(), range
// json.Marshal: nil → "null", empty → "[]"m := map[string]int{ // map literal
"a": 1,
"b": 2,
}
m := make(map[string]int) // empty map
m := make(map[string]int, 100) // empty map with capacity hintm["key"] = 42 // set
v := m["key"] // get (returns zero value if missing)
v, ok := m["key"] // comma-ok idiom — ok is false if missing
delete(m, "key") // delete (no-op if key absent)
len(m) // number of entriesfor k, v := range m {
fmt.Println(k, v) // order is randomized each run!
}var m map[string]int // nil map
_ = m["key"] // safe — returns zero value
m["key"] = 1 // PANIC — can't write to nil map- Reference type — passing to a function shares the underlying data
- Not thread-safe — concurrent reads/writes cause a fatal crash. Use
sync.Mapor async.RWMutex - Keys must be comparable (
==supported): no slices, maps, or functions as keys
type User struct {
ID int
Name string
Email string `json:"email"` // struct tag for JSON field name
CreatedAt time.Time `json:"created_at,omitempty"` // omit if zero
}u := User{ID: 1, Name: "Jon"} // named fields (preferred)
u := User{1, "Jon", "", time.Time{}} // positional (fragile, avoid)
p := &User{ID: 1} // pointer to struct
u := new(User) // *User, zero-valuedu.Name // direct field access
p.Name // auto-dereferenced (same as (*p).Name)point := struct {
X, Y int
}{X: 1, Y: 2}// Structs are comparable if all fields are comparable
a := User{ID: 1, Name: "Jon"}
b := User{ID: 1, Name: "Jon"}
fmt.Println(a == b) // trueGo uses composition instead of inheritance.
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " speaks"
}
type Dog struct {
Animal // embedded — Dog "inherits" Animal's fields and methods
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" — promoted field
fmt.Println(d.Speak()) // "Rex speaks" — promoted method- Embedding is not inheritance — there is no polymorphism between Dog and Animal
- Outer type can override promoted methods by defining its own
- You can embed interfaces too (useful for partial implementation / decoration)
// Value receiver — operates on a copy
func (u User) FullName() string {
return u.Name
}
// Pointer receiver — can mutate the original
func (u *User) SetName(n string) {
u.Name = n
}| Use pointer receiver when... | Use value receiver when... |
|---|---|
| Method modifies the receiver | Method only reads the receiver |
| Struct is large (avoids copy) | Struct is small (e.g., Point{X,Y}) |
| Consistency — if any method uses pointer, all should | Type is immutable by design |
// Go auto-dereferences — you can call pointer methods on values and vice versa
u := User{Name: "Jon"}
u.SetName("Jane") // compiler takes &u automatically- Methods can only be defined on types in the same package
- You cannot define methods on built-in types directly (use a named type)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Interface composition
type ReadWriter interface {
Reader
Writer
}No implements keyword — if a type has the required methods, it satisfies the interface.
type MyReader struct{}
func (MyReader) Read(p []byte) (int, error) {
return 0, io.EOF
}
var r Reader = MyReader{} // works — MyReader satisfies Readervar x any // any == interface{} (alias since Go 1.18)
x = 42
x = "hello"
x = []int{1, 2} // anything can be assigned- Keep interfaces small (1-2 methods)
- Define interfaces where they're consumed, not where they're implemented
- "Accept interfaces, return structs"
io.Reader,io.Writer,fmt.Stringer,error— learn these well
var p *User = nil
var i any = p
fmt.Println(i == nil) // false! interface holds (*User, nil), not nil itselffunc Map[T any, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// usage
doubled := Map([]int{1, 2, 3}, func(x int) int { return x * 2 })type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
// ~ means underlying type (allows named types like `type Age int`)
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}any // no constraint (== interface{})
comparable // supports == and != (usable as map keys)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
}result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err) // wrap with context
}// Simple
err := errors.New("something went wrong")
// Formatted
err := fmt.Errorf("user %d not found", id)
// Wrapped (preserves chain for inspection)
err := fmt.Errorf("query failed: %w", originalErr)type NotFoundError struct {
ID int
Type string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d not found", e.Type, e.ID)
}
// Return it
return nil, &NotFoundError{ID: id, Type: "user"}// Is — checks if any error in the chain matches a target value
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not found")
}
// As — checks if any error in the chain matches a target type
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Println("missing:", nfe.Type, nfe.ID)
}var ErrNotFound = errors.New("not found") // package-level, exported
var errInternal = errors.New("internal") // package-level, unexportedNever compare errors with
==unless they're sentinel values. Useerrors.Is().
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // guaranteed to run when function returns
// Multiple defers execute in LIFO order (stack)
defer fmt.Println("first")
defer fmt.Println("second") // prints "second" then "first"Deferred calls' arguments are evaluated immediately, but the call happens later:
x := 1 defer fmt.Println(x) // prints 1, not 2 x = 2
panic("something went catastrophically wrong")
panic(fmt.Sprintf("unexpected type: %T", v))- Unwinds the call stack, running deferred functions
- Crashes the program if not recovered
- Don't use for normal error handling — only for truly unrecoverable situations
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("oh no")
}recover()only works inside a deferred function- Returns
nilif no panic is in progress - Common in libraries and HTTP handlers to prevent one bad request from crashing the server
package mypackage
func init() {
// runs automatically when package is imported
// before main() executes
// used for: registering drivers, setting defaults, validation
}- Each file can have multiple
init()functions - Execution order: package-level vars →
init()→main() - Avoid complex logic in
init()— it makes testing and debugging harder
go doWork() // fire and forget
go func() { // anonymous goroutine
fmt.Println("running concurrently")
}()- Goroutines are multiplexed onto OS threads by Go's runtime scheduler
- They start with a ~8KB stack that grows dynamically
- Spawning thousands (even millions) is normal
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // increment before launching
go func(id int) {
defer wg.Done() // decrement when done
fmt.Println("worker", id)
}(i)
}
wg.Wait() // block until counter is zerovar mu sync.Mutex
var count int
func increment() {
mu.Lock()
defer mu.Unlock()
count++
}var mu sync.RWMutex
func read() int {
mu.RLock()
defer mu.RUnlock()
return count
}var once sync.Once
var instance *DB
func GetDB() *DB {
once.Do(func() {
instance = connectDB()
})
return instance
}- Don't communicate by sharing memory; share memory by communicating (channels)
- If you share memory, protect it with a mutex
- No automatic cancellation or cleanup for goroutines — you must handle it
Channels are typed conduits for passing data between goroutines.
ch := make(chan int) // unbuffered — send blocks until receiver is ready
ch := make(chan int, 10) // buffered — send blocks only when buffer is fullch <- 42 // send (blocks if unbuffered and no receiver)
v := <-ch // receive (blocks until a value is sent)
v, ok := <-ch // ok is false if channel is closed and emptyclose(ch) // signal that no more values will be sent
// receiving from closed channel returns zero value immediately
// sending to closed channel panics!for v := range ch { // loops until channel is closed
fmt.Println(v)
}func produce(ch chan<- int) { ch <- 1 } // send-only
func consume(ch <-chan int) { <-ch } // receive-onlyselect {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready") // non-blocking if default present
}// Done channel (signaling completion)
done := make(chan struct{})
go func() {
defer close(done)
doWork()
}()
<-done // wait for completion
// Fan-out: launch multiple goroutines reading from one channel
// Fan-in: merge multiple channels into one
// Pipeline: chain of stages connected by channelsUsed for cancellation, deadlines, and request-scoped values across API boundaries.
import "context"
// Create contexts
ctx := context.Background() // root context (top-level)
ctx := context.TODO() // placeholder when unsure
ctx, cancel := context.WithCancel(parent) // manual cancellation
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // auto-cancel after timeout
ctx, cancel := context.WithDeadline(parent, deadline) // auto-cancel at deadline
defer cancel() // ALWAYS call cancel to release resourcesselect {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err()) // context.Canceled or DeadlineExceeded
return
case result := <-ch:
fmt.Println(result)
}// Always pass as the first parameter, named ctx
func FetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
// ...
}Never store context in a struct. Pass it explicitly.
go mod init example.com/myapp # creates go.mod
go mod tidy # add missing / remove unused dependencies
go get example.com/pkg@v1.2.3 # add a dependency
go get -u ./... # update all dependenciesimport "fmt" // standard library
import "example.com/myapp/util" // local package
import m "math" // aliased import (m.Sqrt)
import . "math" // dot import — use Sqrt directly (avoid)
import _ "image/png" // side-effect import (registers PNG decoder)myapp/
├── go.mod
├── go.sum # checksums (not a lockfile — auto-generated)
├── main.go # package main
├── internal/ # private packages (not importable outside module)
│ └── db/
├── pkg/ # public library packages (optional convention)
│ └── util/
└── cmd/ # multiple binaries
├── server/
└── cli/
- One module per
go.mod= many packages - Package name = directory name (by convention)
internal/enforced by the compiler — cannot be imported from outside
func PublicFunc() // exported — accessible from other packages
func privateFunc() // unexported — package-private
type PublicType struct {
ExportedField int // accessible externally
unexportedField int // only within this package
}| Thing | Convention | Example |
|---|---|---|
| Package | short, lowercase, no underscores | http, strconv |
| Interface (1 method) | method name + "er" | Reader, Stringer |
| Acronyms | all caps | URL, HTTP, ID |
| Getters | no Get prefix |
Name() not GetName() |
| Local vars | short | i, n, err, ctx |
Every type has a zero value — allocated memory is always initialized.
| Type | Zero Value |
|---|---|
int |
0 |
float64 |
0.0 |
bool |
false |
string |
"" |
pointer |
nil |
slice |
nil |
map |
nil |
channel |
nil |
interface |
nil |
struct |
all fields zeroed |
func |
nil |
p := new(User) // allocates zeroed User, returns *User
s := make([]int, 10) // make is for slices, maps, channels only
m := make(map[string]int)
ch := make(chan int, 5)Stack vs heap: Go's escape analysis decides. If a value outlives the function (e.g., returned pointer), it's heap-allocated. You rarely need to think about this.
var s []int
len(s) // 0 (safe)
append(s, 1) // works (safe)
for range s {} // works (safe)
s[0] // PANIC — index out of range
var m map[string]int
m["key"] // returns 0 (safe read)
m["key"] = 1 // PANIC — assignment to nil map// file: math_test.go (must end in _test.go)
package math
import "testing"
func TestAdd(t *testing.T) { // must start with Test
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d, want %d", got, want)
}
}func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("got %d, want %d", got, tt.expected)
}
})
}
}func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// run: go test -bench=.go test ./... # all packages
go test -v ./... # verbose
go test -run TestAdd # specific test
go test -cover # coverage
go test -race # race detector (essential for concurrent code)// Formatting
fmt.Println("hello") // print with newline
fmt.Printf("name: %s, age: %d\n", n, a) // formatted
fmt.Sprintf("id-%d", 42) // returns string
fmt.Fprintf(w, "hello") // write to io.Writer
// I/O
io.Copy(dst, src) // copy reader to writer
io.ReadAll(r) // read all bytes (Go 1.16+)
os.ReadFile("path") // read file to []byte (Go 1.16+)
os.WriteFile("path", data, 0644) // write []byte to file
// Time
time.Now() // current time
time.Since(start) // duration since start
time.Sleep(100 * time.Millisecond)
t.Format("2006-01-02 15:04:05") // Go uses this specific reference time!
time.Parse("2006-01-02", "2025-02-01")
// JSON
json.Marshal(v) // struct → []byte
json.Unmarshal(data, &v) // []byte → struct
json.NewEncoder(w).Encode(v) // stream to writer
json.NewDecoder(r).Decode(&v) // stream from reader
// HTTP
http.Get("https://example.com")
http.ListenAndServe(":8080", handler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
// Sorting
sort.Ints(s) // sort []int in place
sort.Strings(s) // sort []string in place
slices.Sort(s) // Go 1.21+ generic sort
slices.SortFunc(s, func(a, b int) int { return a - b })
// Logging
log.Println("info message")
log.Fatalf("fatal: %v", err) // logs and calls os.Exit(1)
slog.Info("msg", "key", "value") // structured logging (Go 1.21+)| Verb | Description | Example Output |
|---|---|---|
%v |
default format | {1 Jon} |
%+v |
with field names | {ID:1 Name:Jon} |
%#v |
Go syntax | User{ID:1} |
%T |
type | main.User |
%d |
decimal integer | 42 |
%x |
hex | 2a |
%f |
float | 3.140000 |
%.2f |
float (2 decimal) | 3.14 |
%s |
string | hello |
%q |
quoted string | "hello" |
%p |
pointer | 0xc0000b4008 |
%w |
wrap error (Errorf only) | — |
A simple CLI that counts words, lines, and characters from stdin or files.
// main.go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func count(scanner *bufio.Scanner) (lines, words, chars int) {
for scanner.Scan() {
line := scanner.Text()
lines++
words += len(strings.Fields(line))
chars += len(line) + 1 // +1 for newline
}
return
}
func main() {
var scanner *bufio.Scanner
if len(os.Args) > 1 {
// read from file
f, err := os.Open(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer f.Close()
scanner = bufio.NewScanner(f)
} else {
// read from stdin
scanner = bufio.NewScanner(os.Stdin)
}
lines, words, chars := count(scanner)
fmt.Printf("%8d %8d %8d\n", lines, words, chars)
}# build and run
go build -o wc .
echo "hello world" | ./wc # 1 2 12
./wc main.go # counts lines/words/chars in main.goA minimal REST API with health check, JSON responses, and graceful shutdown.
// main.go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"time"
)
type StatusResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
type Message struct {
ID int `json:"id"`
Text string `json:"text"`
}
// respondJSON writes a JSON response with the given status code
func respondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func main() {
mux := http.NewServeMux()
// health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, StatusResponse{
Status: "ok",
Timestamp: time.Now().UTC().Format(time.RFC3339),
})
})
// list messages
messages := []Message{
{ID: 1, Text: "Hello, Go!"},
{ID: 2, Text: "Concurrency is not parallelism."},
}
mux.HandleFunc("GET /messages", func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, messages)
})
// create message
mux.HandleFunc("POST /messages", func(w http.ResponseWriter, r *http.Request) {
var msg Message
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid JSON",
})
return
}
msg.ID = len(messages) + 1
messages = append(messages, msg)
respondJSON(w, http.StatusCreated, msg)
})
// configure server
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// start server in a goroutine
go func() {
log.Printf("listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// graceful shutdown: wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit // block until SIGINT
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server stopped")
}# build and run
go build -o api-server .
./api-server
# test endpoints (in another terminal)
curl http://localhost:8080/health
curl http://localhost:8080/messages
curl -X POST http://localhost:8080/messages \
-H "Content-Type: application/json" \
-d '{"text": "A new message"}'Note: The
"GET /messages"pattern syntax requires Go 1.22+. For older versions, usemux.HandleFunc("/messages", handler)and checkr.Methodmanually.
Demonstrates goroutines, channels, WaitGroup, and the pipeline pattern.
// main.go
package main
import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"sync"
)
// FileHash holds a file path and its SHA-256 checksum
type FileHash struct {
Path string
Hash string
Err error
}
// walk sends file paths on a channel (producer)
func walk(root string) <-chan string {
paths := make(chan string)
go func() {
defer close(paths) // close signals no more files
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
paths <- path
return nil
})
}()
return paths
}
// hash reads files and computes checksums (worker)
func hash(paths <-chan string, results chan<- FileHash) {
for path := range paths {
data, err := os.ReadFile(path)
if err != nil {
results <- FileHash{Path: path, Err: err}
continue
}
sum := sha256.Sum256(data)
results <- FileHash{Path: path, Hash: fmt.Sprintf("%x", sum)}
}
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
// stage 1: walk directory tree
paths := walk(root)
// stage 2: fan-out to N workers
results := make(chan FileHash)
var wg sync.WaitGroup
numWorkers := 4
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
hash(paths, results)
}()
}
// close results channel when all workers are done
go func() {
wg.Wait()
close(results)
}()
// stage 3: collect results
for r := range results {
if r.Err != nil {
fmt.Fprintf(os.Stderr, "error: %s: %v\n", r.Path, r.Err)
continue
}
fmt.Printf("%s %s\n", r.Hash[:16], r.Path)
}
}go build -o hasher .
./hasher . # hash all files in current directory
./hasher /path/to/dir # hash all files in a specific directory| Gotcha | Explanation |
|---|---|
nil interface ≠ nil value |
An interface holding a nil pointer is not nil itself |
Shadowing with := |
Inner scope := creates a new variable, doesn't reassign outer |
| Map iteration order | Randomized intentionally — don't depend on it |
| Slice sharing | Sub-slices share the backing array; use copy or full slice expression |
| Nil map write panics | Must make() a map before writing |
| JSON tags are case-sensitive | json:"name" must match exactly |
| Loop var capture (pre-1.22) | Goroutines in loops capture the variable, not the value |
| Goroutine leaks | Goroutines run until they return — forgotten ones leak memory |
defer in loops |
Defers don't run until the function exits, not the loop iteration |
| String indexing returns bytes | "hello"[0] is a byte, not a rune |
- Prefer clarity over cleverness — readable code wins
- Handle errors immediately — don't defer error checking
- Small interfaces — 1-2 methods; accept interfaces, return structs
- Composition over inheritance — embed, don't extend
- One obvious way to do things — resist abstraction for its own sake
- Don't stutter —
http.Servernothttp.HTTPServer - Use
gofmt— non-negotiable; format your code - Document exported symbols — godoc comments start with the name
- Make the zero value useful —
sync.Mutex{}is ready to use - Don't over-channel — a mutex is fine when shared state is simpler
Go is:
- Simple — small language spec, few keywords
- Explicit — no hidden magic, no implicit conversions
- Fast — compiled, statically typed, low-overhead concurrency
- Predictable — one way to format, one way to handle errors
It rewards discipline and punishes cleverness.
- author: Jon LaBelle
- date: February 27, 2026
- source: https://jonlabelle.com/snippets/view/markdown/go-golang-language-cheatsheet