Open-source libraries used

1 libraries are bundled into this tool's code.

Go Cheatsheet — Quick Reference

A Go 1.22 reference for syntax, types, goroutines, and the most-used standard library packages — about 80% of day-to-day needs.

Go

Go Go 1.22

Go (gc toolchain) · Compiled · Concurrent · Imperative · Static · Strongly typed · Structural (interfaces)

Recommended Learning Path

Start with go run / go build and go.mod → learn variables, types, and control flow → go deeper on functions, pointers, and structs → organize data with slices and maps → understand interfaces and error handling → write concurrency with goroutines and channels → then learn http, context, time, and build/testing as needed. The FAQ section is a great place to come back when you hit footguns.

1.Hello World & Build Environment

Run Go programs, manage go.mod modules, and use the toolchain.

Minimal program

Every program starts at package main with a func main entry point. Use fmt for output.

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
// Run:
// go run hello.go
// package main produces an executable
// Other packages are libraries
// main function returns no value

Run and build

go run compiles and runs; go build produces an executable; go install puts it on $GOBIN.

1
2
3
4
5
6
7
8
9
10
11
12
// Run directly:
// go run main.go
// Run the whole package:
// go run .
// Build an executable:
// go build -o hello .
// Install to $GOBIN:
// go install .
// Static-check the code:
// go vet ./...
// The output binary name defaults to the module name
// Windows produces .exe

go.mod modules

go.mod declares the module path and dependencies. go mod init bootstraps it; go mod tidy cleans up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Initialize a module:
// go mod init example.com/hello
// Contents of go.mod:
// module example.com/hello
// go 1.22
// Add a dependency:
// go get github.com/gin-gonic/gin
// Clean up dependencies:
// go mod tidy
// Update dependencies:
// go get -u ./...
// Dependency cache:
// All modules live in GOPATH/pkg/mod
// Verify: go mod verify

Packages and imports

import brings in standard library and third-party packages. Files in the same directory share a package. Unused imports fail to compile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"os"
"strings"
)
// Import a third-party package:
// import "github.com/gin-gonic/gin"
// Alias:
// import f "fmt"
// Blank import (triggers init):
// import _ "net/http/pprof"
// An unused import fails to compile:
// Use _ to silence
// Package name matches the last segment of the import path

Formatted output

fmt.Println/Printf/Print are the workhorses. Printf uses verbs like %s %d %v. Sprintf returns the formatted string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
fmt.Println("Hello") // automatic newline
fmt.Printf("Age: %d\n", 30)
fmt.Printf("%s is %d\n", "Nick", 30)
// Common verbs:
// %v default %#v struct %T type
// %d integer %f float %.2f two decimals
// %s string %q quoted
// %t bool %x hex
// Return a string:
s := fmt.Sprintf("sum: %d", 1+1)
}

Arguments and input

os.Args holds command-line arguments, the flag package parses them, and fmt.Scan reads interactive input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"os"
)
func main() {
// os.Args[0] is the program path
args := os.Args[1:]
fmt.Println("args:", args)
// Read input:
var name string
fmt.Scanln(&name) // read one line
// With prompt:
fmt.Print("enter: ")
var n int
fmt.Scan(&n)
}

Multi-file programs

Multiple .go files in the same directory belong to the same package and can call each other directly; they share package scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// utils.go
package main
func helper() string {
return "hi"
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(helper()) // call directly
}
// Run:
// go run .
// Identifiers in the same package see each other
// Function names start lowercase: package-private
// Start uppercase: exported

Toolchain

go version shows the version, go env lists the environment, go doc shows documentation, and go fmt formats the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Version:
// go version
// Environment variables:
// go env GOPATH GOROOT
// go env GOPROXY # proxy
// Documentation:
// go doc fmt.Println
// go doc strings.Builder
// Format:
// gofmt -w .
// Module dependency graph:
// go list -m all
// Update the toolchain:
// go get golang.org/dl/go1.22.0
// Help: go help <command>

2.Variables & Constants

Variable declarations, short declarations, constants, zero values, and type conversions.

Variable declarations

Use var to declare variables; the type comes after the name. You must initialize the value or accept the zero value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name string
var age int
var ok bool
// With initialization:
var count int = 5
var title = "Go" // type inferred
// The type comes after the name:
var score float64
// Batch declaration:
var (
a int
b string
)
// Uninitialized variables have zero values
// Declared-but-unused variables fail to compile

Short variable declarations

:= declares and initializes with type inference. It works only inside functions, and at least one variable on the left must be new.

1
2
3
4
5
6
7
8
9
10
11
name := "Go" // inferred string
count := 42
flag := true
// Existing + new variable:
name, age := "Nick", 30
age = 31 // already exists: use =
// Redeclaration:
// := requires at least one new variable on the left
// := is not allowed outside functions
// Redeclaring in the same scope is an error
// x, y := 1, 2

Constants

const declares values fixed at compile time. iota produces auto-incremented enumerations. Constants have arbitrary precision.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Pi = 3.14
const (
StatusOK = 200
StatusNotFound = 404
)
// iota auto-increment:
const (
A = iota // 0
B // 1
C // 2
)
// Typed constant:
const MaxSize int = 100
// Untyped constants can be any numeric type:
const N = 10
var f float64 = N // OK
// Constants cannot be modified

Zero values

An uninitialized variable has a zero value: 0 for numbers, "" for strings, false for booleans, nil for pointers. Zero values are usable out of the box.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var n int // 0
var s string // ""
var b bool // false
var p *int // nil
var arr [3]int // [0 0 0]
var m map[string]int // nil map
// Zero values are ready to use:
var buf bytes.Buffer
t := buf.String() // ""
// nil slices can be appended to:
var nums []int
nums = append(nums, 1)
// nil maps cannot be written to:
// m["a"] = 1 // panic
// Struct zero value: var r bytes.Reader

Multiple assignment

Assign multiple variables on one line, swap values, and unpack multi-return function calls.

1
2
3
4
5
6
7
8
9
10
11
a, b := 1, 2
a, b = b, a // swap
// Ignore a value:
x, _ := getPair()
// Unpack multiple return values:
value, err := risky()
// Partial update:
map1["a"], n := "x", 1
// And initialize at the same time:
// But := requires at least one new variable
// The unpack count must match the function signature

Scope

Block scope: variables are visible only inside their block. Watch out for shadowing. Package-level variables exist once per package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
var global = 10 // package-level
func f() {
x := 1
if y := x + 1; y > 0 {
z := y * 2 // inside the if block
_ = z
}
// y and z are not visible here
}
// Shadowing:
func g() {
global := 99 // shadows the package-level global
_ = global
}
// Short declarations can shadow outer variables
// Loop variables are scoped to the loop block

Type conversion

T(x) performs an explicit conversion. Go never auto-converts numeric types; conversions can overflow or truncate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var i int = 42
f := float64(i) // int -> float64
// Float-to-int conversion truncates:
n := int(3.99) // 3
// String to/from number:
// use strconv:
s := strconv.Itoa(42) // "42"
n2, _ := strconv.Atoi("42")
// Bytes to string:
s2 := string([]byte{'h', 'i'})
// Note: string(65) is "A", not "65"
// Rune conversion:
r := rune(20013)
// Overflow truncation:
// var b byte = 300 compile error

Naming conventions

Identifiers starting with an uppercase letter are exported across packages; lowercase ones stay package-private. Use camelCase and short names for locals.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package shapes
// Exported (uppercase first letter):
export const Area = 100
func Calculate() float64 // exported
// Unexported (lowercase):
func helper() int // package-internal
var internalCount = 0
// camelCase:
// maxRetries / getUserById
// Initialisms keep their case:
// theURL / HTTPClient
// Recommendations:
// Short names for locals (i, n)
// Package names are lowercase and singular

3.Data Types

Built-in types, structs, arrays, interfaces, and generics.

Basic types

bool, string, integers, floats, complex numbers, and the byte/rune aliases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var b bool
var s string
// Integers:
var i int // platform-dependent
var i8 int8; var u uint
// Floats:
var f32 float32; var f64 float64
// Complex:
var c complex128
// Aliases:
var by byte = 'a' // uint8
var r rune = '中' // int32
// Unsigned:
uint8 uint16 uint32 uint64
// Platform: int and uint are 64-bit
// Sizes:
// byte is 1 byte, rune is 4 bytes

Numeric operations

Arithmetic, bitwise operators, and the math package. Integer division truncates. Watch for overflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a, b := 7, 2
a / b // 3 (integer division truncates)
a % b // 1
// Floats:
// math.Sqrt(16) math.Pow(2, 3)
// math.Max/Min math.Abs
// Bitwise ops:
// & | ^ << >>
// Overflow:
// var x int8 = 127; x++ // -128
// Overflow detection (1.18+):
// a, ok := bits.Add(a, b, 0)
// Random numbers: math/rand
// Constant arithmetic has arbitrary precision

struct

A struct composes fields. Initialize by name or positionally; use struct tags for metadata.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type User struct {
Name string
Age int
Tags []string
}
// Initialization:
u := User{Name: "Nick", Age: 30}
u2 := User{"Anna", 25, nil} // positional (not recommended)
// Access:
u.Name = "New"
// Zero value:
var u3 User // all-zero
// Field tags:
// type S struct {
// ID int `json:"id"`
// }
// Pointers auto-dereference:
p := &u
p.Name = "X" // (*p).Name

Arrays

Arrays have a fixed length and behave as value types (they are copied). You rarely use them directly — most code uses slices.

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr [3]int // [0 0 0]
arr[0] = 10
// Initialization:
arr2 := [3]int{1, 2, 3}
arr3 := [...]int{1, 2, 3} // inferred length
// Length: len(arr)
// Value type:
b := arr2
b[0] = 99 // arr2 is unchanged
// Comparison:
// [3]int{1,2,3} == [3]int{1,2,3}
// Arrays are copied when passed as arguments
// Pass by reference via slices or pointers

type definitions

type defines a new type or an alias. A defined type is distinct from its underlying type and needs explicit conversion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Celsius float64
type ID string
// New type:
var c Celsius = 25.0
// Distinct method set, conversion required:
var f float64 = float64(c)
// Methods:
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", c)
}
// Type alias (1.9+):
type MyInt = int // fully identical
// Named types can have methods
// Define over a slice or map:
type Stack []int

Generics

Generic functions use [T any] type parameters. Constraints (often interfaces) restrict the set of allowed types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
// Generic function:
func Min[T int | float64](a, b T) T {
if a < b { return a }
return b
}
// Constraints:
// any means any type
// comparable means comparable
// Generic type:
type Pair[K comparable, V any] struct {
Key K
Value V
}
// Usage:
p := Pair[string, int]{Key: "a", Value: 1}
fmt.Println(Min(1, 2), Min(1.5, 2.5))
// Constraint interface:
// type Number interface { ~int | ~float64 }

interface types

An interface is a method set. The empty interface interface{} (any) holds any value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Shape interface {
Area() float64
}
// Implementation:
type Circle struct{ R float64 }
func (c Circle) Area() float64 {
return 3.14 * c.R * c.R
}
// Usage:
func PrintArea(s Shape) {
fmt.Println(s.Area())
}
// Empty interface (1.18+ use any):
func PrintAny(v any) {
fmt.Println(v)
}
// Assertion:
if c, ok := s.(Circle); ok {
fmt.Println(c.R)
}
// Interface value = dynamic type + value

comparable constraints

comparable is a built-in constraint for types that support ==. Used for map keys and generic comparisons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// comparable constraint:
func Contains[T comparable](items []T, target T) bool {
for _, v := range items {
if v == target {
return true
}
}
return false
}
// Comparable types:
// numbers, strings, bool, pointers,
// structs (all fields comparable), arrays
// Not comparable:
// slices, maps, funcs, structs containing them
// Usage:
// Contains([]string{"a"}, "a")
// Map keys must be comparable
// Use comparable as a generic constraint instead of getting an == error

4.Pointers

Pointers and address operations: take address, dereference, new/make, stack vs heap.

Address-of &

& takes the address of a variable and returns a pointer that refers to that memory location.

1
2
3
4
5
6
7
8
9
10
11
12
13
n := 42
p := &n // p is a pointer to n
fmt.Println(p) // address
fmt.Println(*p) // 42
// Pointer type: *int
// The pointed-to value can be modified:
*p = 99
fmt.Println(n) // 99
// & on fields or array elements:
// &arr[0]
// You cannot take the address of a literal:
// &42 is an error
// Address of a struct literal: &User{...}

Dereference *

*p accesses the value pointed to by p. Struct fields through a pointer auto-dereference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Point struct{ X, Y int }
pt := Point{1, 2}
p := &pt
// Auto-dereference:
p.X = 10 // (*p).X
(*p).Y = 20
// When you need explicit dereference:
q := new(int)
*q = 7
// Chaining:
// (*ptr).field.method()
// Dereferencing a nil pointer panics:
// var p *int; *p = 1 // crash
// Multi-level pointers: * *int

nil pointers

A nil pointer is the zero value. Dereferencing one panics — check for nil before use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var p *int // nil
// Check:
if p != nil {
fmt.Println(*p)
} else {
fmt.Println("p is nil")
}
// Dereferencing nil crashes:
// *p = 1 // panic: nil pointer
// Common idiom:
func safeDeref(p *int) int {
if p == nil {
return 0
}
return *p
}
// Returning nil signals "no value":
// Functions return (T, bool) or *T

Pointer parameters

A pointer parameter lets a function mutate the caller's variable. Value parameters work on a copy. Large structs should be passed by pointer to avoid copying.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Value parameter (copy):
func setZeroValue(n int) {
n = 0 // only mutates the copy
}
// Pointer parameter (mutates the caller):
func setZeroPtr(n *int) {
*n = 0
}
// Usage:
x := 100
setZeroPtr(&x)
fmt.Println(x) // 0
// Pointer to a struct:
func Scale(p *Point, f int) {
p.X *= f
p.Y *= f
}
// Pass large structs by pointer to avoid copying
// Mutate via pointer when the change needs to persist

new and make

new allocates a value type and returns a pointer to it. make initializes slices, maps, and channels and returns a ready-to-use value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// new: returns a pointer to a zero value
p := new(int)
// *p == 0
pp := new(User) // pointer to a zero-valued struct
// make: initialize reference types
nums := make([]int, 0, 10) // slice
m := make(map[string]int) // map
ch := make(chan int) // channel
// Differences:
// new(T) returns *T, zero value
// make only for slice/map/channel
// make returns an initialized, non-nil value
// Maps must be made before writing
// Slices can be declared nil and appended to

Pointer idioms

Pointers share mutable state. Return a pointer or value depending on the semantics; pick method receivers consistently.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Return a pointer:
func NewUser(name string) *User {
return &User{Name: name}
}
// Pointer-receiver method:
func (u *User) SetName(name string) {
u.Name = name // mutate the original
}
// Value receiver:
func (u User) FullName() string {
return u.Name
}
// When to use a pointer:
// 1. To mutate the receiver
// 2. To avoid copying a large struct
// 3. When semantics call for sharing
// When to use a value:
// Small structs or immutable semantics
// Pointer consistency: use one kind across a type's methods

Stack and heap

The compiler decides whether a variable lives on the stack or the heap. Taking the address may cause escape to the heap.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Local variables live on the stack by default:
func local() int {
x := 42
return x // the return value lets x stay on the stack
}
// Taking the address makes it escape to the heap:
func escape() *int {
x := 42
return &x // x escapes to the heap
}
// Inspect escapes with:
// go build -gcflags='-m' .
// Why values escape:
// 1. Address of a local is returned
// 2. The address is stored in a global or heap object
// 3. Interface boxing or closure capture
// Escapes add GC pressure — but don't over-optimize

Pointer safety

Go pointers have no arithmetic (no C-style p+1). The GC manages lifetimes, so there are no dangling pointers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// No pointer arithmetic:
// p = p + 1 is an error
// The operation does not exist
// Safe access (compiler-guaranteed):
// no out-of-bounds, no dangling pointers
// Pointer to a slice element:
nums := []int{1, 2, 3}
p := &nums[0]
// The pointer refers to the array element
// The unsafe package bypasses checks (use with care):
// import "unsafe"
// Conversions must be verified manually
// Nil checks: check for nil before dereference
// Memory safety is enforced by the GC + compiler

5.Control Flow

if, for, switch, defer, and loop control.

if / else

if conditions don't use parentheses. You can write a short init statement before the condition. Conditions must be boolean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// Init statement:
if err := doSomething(); err != nil {
fmt.Println(err)
}
// err is only visible inside the if/else blocks
// Conditions must be boolean (no truthy checks)
// Assignment isn't a comparison:
// if ok = true is an error — use if ok

for loops

Go has only the for loop. Use the C-style three-clause form, a while-style condition, or an infinite loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Full three-clause form:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while-style:
n := 0
for n < 5 {
n++
}
// Infinite loop:
for {
break // or return
}
// Multiple variables:
for i, j := 0, 10; i < j; i, j = i+1, j-1 {
}
// Omit the init or condition:
// for ; n < 3; n++

range iteration

range iterates over arrays, slices, maps, strings, and channels. Since Go 1.22, loop variables are scoped per iteration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nums := []int{1, 2, 3}
for i, v := range nums {
fmt.Println(i, v)
}
// Values only:
for _, v := range nums
// Indices only:
for i := range nums
// Iterate over a map:
for k, v := range m { }
// Iterate over a string (by rune):
for i, r := range "你好" {
fmt.Println(i, r) // one rune per iteration
}
// Iterate over a channel:
for v := range ch { }
// 1.22+ loop variables are independent per iteration
// Older versions: watch for goroutine closures

switch

switch does not fall through by default. Cases can list multiple values, omit the expression (if-else chain), or do a type switch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
day := "mon"
switch day {
case "mon", "tue":
fmt.Println("weekday")
case "sat", "sun":
fmt.Println("weekend")
default:
fmt.Println("other")
}
// No expression (boolean conditions):
score := 75
switch {
case score >= 90:
fmt.Println("A")
default:
fmt.Println("other")
}
// Type switch:
var v any = "str"
switch t := v.(type) {
case string:
fmt.Println("string", t)
case int:
fmt.Println("int", t)
}

defer

defer schedules a call to run when the surrounding function returns. Defers run in LIFO order — perfect for cleanup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readFile() error {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // close when the function returns
// process...
return nil
}
// Multiple defers run LIFO:
func example() {
defer fmt.Println("first")
defer fmt.Println("second") // printed first
}
// Deferred arguments are evaluated when defer runs:
func count() {
i := 0
defer fmt.Println(i) // 0
i = 10
}
// Common uses:
// closing files / connections, unlocking, recovering from panic

break and continue

break exits the loop, continue skips to the next iteration. Both accept labels to control nested loops.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
if i > 7 {
break
}
fmt.Println(i) // 1 3 5 7
}
// Labeled control of the outer loop:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // skip to the next outer iteration
}
fmt.Println(i, j)
}
}
// break outer exits the outer loop
// The label sits on the same line as the loop, or the line above

switch with init

switch can take an init statement before the expression. Useful for the error-handling chain idiom.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch n := rand.IntN(3); n {
case 0:
fmt.Println("zero")
case 1:
fmt.Println("one")
default:
fmt.Println("other")
}
// The init variable is only visible inside the switch
// Equivalent without the init clause:
// n := rand.IntN(3)
// switch n { ... }
// Typical use: error-handling chain
// switch err := f(); err {
// case nil: ...
// default: log(err)
// }
// Unmatched cases fall through to default

goto and labels

goto jumps to a label. Go allows it but uses rarely; deep error-handling code is the one common case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func process(items []int) error {
for i, v := range items {
if v < 0 {
goto failure
}
_ = i
}
return nil
failure:
return errors.New("found a negative number")
}
// Caveats:
// Labels must be declared after the goto that uses them
// Cannot jump into a scope or over variable declarations
// goto cannot jump into another block
// In most cases defer / returning errors is clearer
// Use sparingly — readability first

6.Functions

Function definitions, multiple return values, closures, methods, function values.

Function definitions

func defines a function. Parameters take their types; the return type comes last.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
// Group parameters of the same type:
func sub(a, b int) int {
return a - b
}
// No return value:
func log(msg string) {
fmt.Println(msg)
}
// Call site:
func main() {
sum := add(1, 2)
fmt.Println(sum)
}
// Function arguments are passed by value (copied)
// To share state, pass a pointer / slice / map

Multiple return values

Go functions can return multiple values. The idiom is (value, error).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Use:
result, err := div(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(result)
}
// Ignore a return value:
q, _ := div(10, 2)
// Multiple useful values:
func minMax(nums []int) (int, int) {
return nums[0], nums[len(nums)-1]
}
// Convention:
// The last return value is error
// Other returns are zero values on error

Named return values

Return values can be named. Assign to them in the body and use a bare return to send them out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // bare return sends x and y
}
// When named returns help:
func stats(nums []int) (sum, avg float64) {
sum = 0
for _, v := range nums {
sum += float64(v)
}
avg = sum / float64(len(nums))
return
}
// Notes:
// Bare returns are controversial for readability
// A defer can mutate named return values
// Named return values start at their zero value
// Complex returns read better when named

Variadic parameters

...T gathers any number of arguments into a slice. Use slice... to spread one on a call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Calls:
sum(1, 2, 3) // 6
sum() // 0
// Spread a slice:
nums := []int{1, 2}
sum(nums...) // spread into individual arguments
// Mixed with regular params:
// func greet(prefix string, names ...string)
// Variadic parameter must be the last one
// Inside the function the type is []T (same as a slice)

Closures

Closures capture surrounding variables. A returned function keeps its own state. Since 1.22, loop variables are captured per iteration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func makeCounter() func() int {
count := 0
return func() int {
count++ // captures count
return count
}
}
// Usage:
counter := makeCounter()
counter() // 1
counter() // 2
// Closures capture the variable itself:
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
// Each closure has its own state:
c1, c2 := makeCounter(), makeCounter()
// 1.22+ loop variables are independent per iteration

Methods

Methods attach to a receiver, which appears between func and the method name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Rectangle struct {
Width, Height float64
}
// Value receiver:
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Pointer receiver:
func (r *Rectangle) Scale(f float64) {
r.Width *= f
r.Height *= f
}
// Usage:
rect := Rectangle{2, 3}
fmt.Println(rect.Area())
rect.Scale(2)
// Methods vs functions:
// Methods bind to a type
// Methods cannot be defined on types from other packages
// Pointer receivers can mutate the original

Function values

Functions are first-class values — assign them, pass them as arguments, return them. A function type is its signature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Function type:
type Operation func(a, b int) int
func add(a, b int) int { return a + b }
func apply(op Operation, x, y int) int {
return op(x, y)
}
// Usage:
result := apply(add, 3, 4) // 7
// Anonymous function:
f := func(a, b int) int { return a - b }
// Passed as a callback:
func filter(nums []int, pred func(int) bool) []int {
out := make([]int, 0)
for _, n := range nums {
if pred(n) {
out = append(out, n)
}
}
return out
}
// Common: sort.Slice takes a comparator function

init functions

init functions run automatically when a package is loaded. Each file may contain multiple init functions for setup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
var config map[string]string
func init() {
config = make(map[string]string)
config["env"] = "dev"
fmt.Println("init complete")
}
// Characteristics:
// Runs before main
// Each file may have multiple init functions
// Multiple inits run in file / source order
// Dependency package inits run first
// Use cases:
// register drivers, initialize configuration
// when a value can't be set up with var
// Avoid relying on init ordering

7.Strings

Immutable strings, rune/byte, the strings package, and formatting.

String basics

A string is an immutable byte sequence. Use double-quoted literals for interpreted strings and backticks for raw strings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s := "hello"
// Backtick raw string literal:
raw := `multi-line
text`
// Length (in bytes):
len("hello") // 5
len("你好") // 6 (UTF-8: 3 bytes per CJK char)
// Indexing is by byte:
s[0] // 'h' (byte)
// Concatenation:
s2 := "a" + "b"
// Immutable:
// s[0] = 'x' is an error
// Copy:
// assigning a string copies the reference (safe because strings are immutable)
// Strings are comparable: == < >

rune and characters

A rune is a Unicode code point (int32). range iterates by rune; the unicode/utf8 package decodes bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好, Go"
// range iterates by rune:
for _, r := range s {
fmt.Printf("%c ", r)
}
// 你好 , space G o
// Convert to a slice of runes:
rs := []rune(s)
fmt.Println(len(rs)) // number of runes
// Slice of bytes:
bs := []byte(s)
// Rune literal:
ch := '中' // rune
// Decoding helpers:
import "unicode/utf8"
// utf8.RuneCountInString(s)
// utf8.DecodeRuneInString
// Indices are byte positions — don't slice CJK strings by len

strings.Builder

Builder efficiently accumulates strings. Avoid the + chain in loops — Builder reduces allocations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "strings"
var b strings.Builder
b.WriteString("hello")
b.WriteString(", ")
b.WriteString("world")
result := b.String() // "hello, world"
// Pre-allocate:
b.Grow(100) // pre-allocate capacity
// Write byte/char:
b.WriteByte('!')
b.WriteRune('中')
// Return length:
b.Len()
// Use cases:
// Concatenate in loops, generate HTML/CSV
// More efficient than + (avoids multiple allocations)
// More semantically fitting for strings than bytes.Buffer

The strings package

Contains, Split, Join, Trim, Replace, and many more string operations in the strings package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "strings"
s := " Hello World "
strings.TrimSpace(s)
strings.ToUpper(s)
strings.ToLower(s)
strings.Contains(s, "World") // true
strings.HasPrefix(s, " ")
strings.HasSuffix(s, " ")
strings.Replace(s, "World", "Go", -1)
strings.ReplaceAll(s, " ", "-")
// Split:
parts := strings.Split("a,b,c", ",")
strings.Join([]string{"a", "b"}, "-")
// Substring:
strings.Index(s, "World")
strings.LastIndex(s, "o")
// Count:
strings.Count(s, "o")
// Compare:
strings.Compare("a", "b")
strings.EqualFold("Go", "go") // case-insensitive

fmt formatting

Printf/Sprintf use verbs, with width, precision, and padding. Fprintf writes to any io.Writer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name, age := "Nick", 30
fmt.Printf("%s is %d years old\n", name, age)
// Verbs:
// %v default %+v with field names %#v Go syntax
// %d decimal %x hex
// %f float %.2f 2 decimal places
// %s string %q quoted
// %t bool %T type
// %5d width %-5d left-aligned
// %05d zero-padded
// %10.2f width 10 with 2 decimals
// Return string:
s := fmt.Sprintf("%s-%d", name, age)
// Write to io.Writer:
fmt.Fprintf(buf, "%s", name)

strconv conversion

Convert strings to/from numbers and booleans. Handle quote and unquote too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "strconv"
// Number to string:
s := strconv.Itoa(42) // "42"
// String to number:
n, err := strconv.Atoi("42")
// Parse float:
f, err := strconv.ParseFloat("3.14", 64)
// Specify base:
n64, _ := strconv.ParseInt("101", 2, 64)
// Format:
strconv.FormatInt(255, 16) // "ff"
strconv.FormatFloat(3.14, 'f', 2, 64)
// Bool:
b, _ := strconv.ParseBool("true")
strconv.FormatBool(true)
// Quote:
strconv.Quote("hi") // "\"hi\""
// Base formatting:
fmt.Sprintf("%x", 255) // "ff"

Encoding/decoding

Base64, hex, and URL encoding. Convert between []byte and string as needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"encoding/base64"
"encoding/hex"
"net/url"
)
// base64:
b64 := base64.StdEncoding.EncodeToString([]byte("hi"))
decoded, _ := base64.StdEncoding.DecodeString(b64)
// hex:
h := hex.EncodeToString([]byte("\x00\xff"))
// URL encoding:
url.QueryEscape("a b&c") // "a+b%26c"
// Byte and string:
s := string([]byte{104, 105})
bs := []byte(s)
// Hex output:
fmt.Printf("%x\n", "hi")
// Handle err on encode/decode

Character iteration

Iterate strings by rune to handle non-ASCII text. Be aware that string indices are byte offsets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好 Go"
// range iterates by rune:
for _, r := range s {
fmt.Printf("%c", r)
}
// With index:
for i, r := range s {
fmt.Println(i, r) // byte offset + code point
}
// Byte index (ASCII only):
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
// Convert to []rune:
rs := []rune(s)
// Get the n-th character:
fmt.Println(string(rs[2]))
// Always process Chinese as rune

8.Collections

Slices, maps, arrays, and sorting.

Slices

A slice is a dynamic view over a backing array. A nil slice is length 0 and usable with append.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums := []int{1, 2, 3} // literal
var s []int // nil slice
// Append:
nums = append(nums, 4)
// Empty slice:
empty := []int{}
// Length and capacity:
len(nums); cap(nums)
// Slice:
sub := nums[1:3] // view shares underlying array
// References the underlying array:
// Modifying sub affects nums
// append past capacity allocates a new array
// make with capacity:
make([]int, 5, 10)

Slice operations

Use append to grow, copy to duplicate, and idioms on top of them to insert or remove elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nums := []int{1, 2, 3, 4, 5}
// Append multiple:
nums = append(nums, 6, 7)
// Concatenate slices:
nums = append(nums, []int{8, 9}...)
// Delete at index 2:
nums = append(nums[:2], nums[3:]...)
// Copy:
dst := make([]int, len(nums))
copy(dst, nums)
// Prepend:
nums = append([]int{0}, nums...)
// Clear:
nums = nums[:0]
// Reverse:
for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 {
nums[i], nums[j] = nums[j], nums[i]
}
// Note that copies may share the underlying array

make and capacity

make creates a slice and pre-allocates capacity. Pre-allocating avoids repeated reallocations during append.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Length 0, capacity 5:
nums := make([]int, 0, 5)
// Pre-allocate to avoid repeated copies:
nums = append(nums, 1, 2, 3)
// Capacity growth:
// append past capacity doubles the allocation
// Pre-allocate for large slices:
// make([]int, 0, 100000)
// Length and capacity:
s := make([]int, 5) // length 5, zero values
cap(s) // 5
// Clear while keeping capacity:
s = s[:0]
// Check empty:
if len(s) == 0
// Use cap to pre-allocate and optimize performance

map

Maps store key/value pairs. Create with make, read, write, delete with delete, iterate with range. A nil map is read-only.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
// Read:
v := m["a"] // returns zero value if absent
v, ok := m["a"] // ok indicates presence
if val, ok := m["x"]; ok {
fmt.Println(val)
}
// Delete:
delete(m, "a")
// Iterate (unordered):
for k, v := range m {
fmt.Println(k, v)
}
// Length: len(m)
// Literal:
mm := map[string]int{"a": 1}
// nil map:
// var mm map[string]int
// mm["a"] = 1 // panic

map idioms

Use maps as sets, counters, and caches. Great for collecting keyed values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Set:
seen := make(map[string]bool)
for _, v := range list {
if !seen[v] {
seen[v] = true
fmt.Println("new:", v)
}
}
// Count:
counts := make(map[string]int)
for _, w := range words {
counts[w]++
}
// Cache:
var cache = make(map[string][]byte)
func get(key string) ([]byte, bool) {
data, ok := cache[key]
return data, ok
}
// Struct values:
m := map[string]Point{}
// Update nested:
// Value copy pitfall: p := m["a"]; p.X=1; m["a"]=p

Sorting

The sort package sorts slices. Pass a custom comparator; use sort.Search for binary search.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "sort"
// Basic sort:
nums := []int{3, 1, 2}
sort.Ints(nums)
strs := []string{"b", "a"}
sort.Strings(strs)
// Custom comparison:
type Person struct{ Name string; Age int }
people := []Person{{Age: 30}, {Age: 20}}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// Reverse:
sort.Slice(people, func(i, j int) bool {
return people[i].Age > people[j].Age
})
// Stable sort:
sort.SliceStable(people, ...)
// Binary search:
// sort.SearchInts(nums, 2)
// Check sorted: sort.IntsAreSorted(nums)

The container package

container/heap for heaps and container/list for doubly-linked lists. Use them when you need these data structures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// container/list (linked list):
import "container/list"
l := list.New()
l.PushBack(1)
l.PushFront(0)
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// Heap:
import "container/heap"
// Implement heap.Interface:
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h; n := len(old); x := old[n-1]
*h = old[:n-1]; return x
}
// Usage:
h := &IntHeap{3, 1, 2}
heap.Init(h)
heap.Pop(h) // smallest 1

Iteration tricks

range gives you indices and/or values. Sort map keys when a deterministic order matters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Iterate with index:
for i := range nums {
nums[i] *= 2
}
// Index + value:
for i, v := range nums {
fmt.Println(i, v)
}
// Value only:
for _, v := range nums
// String rune:
for i, r := range s
// Sorted map key iteration:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// Reverse slice iteration:
for i := len(nums) - 1; i >= 0; i--

9.Memory & Performance

Garbage collection, escape analysis, allocation optimization, and memory profiling.

Garbage collection

Go has an automatic, concurrent, low-pause GC. You never call free manually — the runtime manages memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Go uses concurrent mark-and-sweep GC
// No manual malloc/free needed
// Allocation:
// Heap objects are reclaimed by GC
// Stack objects are released when the function returns
// GC parameter:
// GOGC=100 default threshold
// debug.SetGCPercent()
// Trigger:
// Memory doubles / explicit runtime.GC()
// Impact:
// Reducing heap allocations lowers GC pressure
// Use sync.Pool for many small objects
// Watch the lifecycle of large objects

Escape analysis

The compiler decides where a variable lives. Taking its address, closing over it, or boxing it into an interface may escape it to the heap.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Stack allocation:
func local() int {
x := 42 // does not escape, on stack
return x
}
// Heap allocation (escape):
func leak() *int {
x := 42
return &x // address escapes via return
}
// Inspect escape:
// go build -gcflags='-m' ./...
// Escape triggers:
// 1. Returning the address of a local variable
// 2. Closure capturing a variable
// 3. Storing a value in an interface (boxing)
// 4. Assigning to a global variable
// Optimization:
// Avoid unnecessary escapes to reduce GC pressure
// But don't over-optimize

Allocation optimization

Pre-allocate capacity, reuse buffers, and reduce small allocations on hot paths.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Pre-allocate slice:
nums := make([]int, 0, n) // avoid repeated growth
// Reuse buffer:
var buf bytes.Buffer
buf.Grow(1024) // pre-allocate
// Avoid temporary objects:
// Avoid fmt.Sprintf inside hot loops
// Use builder to concatenate:
var sb strings.Builder
// Object pool:
// Reuse short-lived objects
// Interface call overhead:
// Avoid interface dispatch on hot paths
// Measure before optimizing:
// Use pprof to find hotspots
// Don't pre-optimize ordinary code

Finalizers

runtime.SetFinalizer runs a hook before an object is reclaimed — useful as a safety net for external resources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "runtime"
type Resource struct {
name string
handle *os.File
}
func NewResource(name string) *Resource {
r := &Resource{name: name}
runtime.SetFinalizer(r, func(r *Resource) {
if r.handle != nil {
r.handle.Close() // fallback cleanup
}
})
return r
}
// Caveats:
// Finalizers are not guaranteed to run promptly
// Pending finalizers are not guaranteed to run on program exit
// Best practice:
// Prefer explicit Close
// Use finalizers only as a safety net
// Reference cycles can prevent finalizers from ever running

Memory model

In concurrent code, writes are only guaranteed visible to other goroutines after a synchronization event: channel, mutex, or atomic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Go memory model:
// A write in one goroutine is
// visible to a read in another goroutine
// only when synchronized via primitives:
// 1. channel:
ch := make(chan int)
gr() { ch <- 1 }() // write
<-ch // read
// 2. mutex:
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()
// 3. atomic:
var x atomic.Int64
x.Store(1)
v := x.Load()
// Unsynchronized shared reads/writes are data races
// Use go run -race to detect them

Memory profiling

pprof profiles memory and CPU. Heap snapshots help you find leaks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
_ "net/http/pprof"
"net/http"
)
// Start pprof:
// go run -race main.go
// In your code:
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ...
}
// Analyze:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/profile
// CLI interaction:
// top / list / web
// Flame graph:
// go tool pprof -http=:8080 profile.out
// Locate: allocation hotspots, leaking objects

sync.Pool

Pool reuses short-lived objects to ease GC pressure — great for buffers and decoders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// Usage:
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // clear before reuse
// write to and use it...
bufPool.Put(b) // return to pool
// Suitable for:
// Many short-lived objects
// Temporary buffers, serialization buffers
// Notes:
// GC empties the Pool (no retention guarantee)
// Not suitable for: long-held objects
// Get may return nil (if New is unset)
// Don't put stateful objects in a Pool

Stack and recursion

goroutine stacks grow dynamically. Deep recursion is limited; rewrite very deep recursion as a loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// goroutine stack:
// Initial 2KB, grows dynamically
// Stack ceiling ~1GB (on 64-bit)
// Recursion:
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
// Deep recursion risk:
// Stack overflow crash: fatal error: stack overflow
// Convert deep recursion to iteration:
func factorialIter(n int) int {
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}
// Recursion depth log:
// Watch stack growth when nesting tens of thousands of frames

10.Structs & Methods

Go's OOP: structs, interfaces, embedding, and composition (no class inheritance).

structs and methods

Structs hold data; methods bind behavior to a type. Use constructor functions for clarity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Account struct {
ID string
Balance float64
}
// Value receiver:
func (a Account) Balance() float64 {
return a.Balance
}
// Pointer receiver:
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
// Constructor:
func NewAccount(id string) *Account {
return &Account{ID: id}
}
// Usage:
acc := NewAccount("a1")
acc.Deposit(100)
// Value/pointer auto-conversion:
// pointer acc can call value methods

Choosing a receiver

Value receivers don't mutate; pointer receivers do. Keep all methods of a type using the same receiver kind.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Counter struct {
N int
}
// Value receiver (copy):
func (c Counter) Value() int {
return c.N
}
// Pointer receiver (mutates):
func (c *Counter) Inc() {
c.N++
}
// Usage:
c := Counter{}
c.Inc() // auto address-of
// Rules:
// Mutating receiver -> pointer
// Large struct -> pointer (saves copies)
// Keep all method receivers consistent:
// Mixing causes some calls to auto address
// Interface satisfaction also requires matching receivers

Interfaces

An interface is a behavioral contract. A type satisfies it implicitly if its method set matches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Greeter interface {
Greet() string
}
// Type implementation:
type Dog struct{ Name string }
func (d Dog) Greet() string {
return "Woof " + d.Name
}
type Cat struct{ Name string }
func (c Cat) Greet() string {
return "Meow " + c.Name
}
// Usage:
func GreetAll(gs []Greeter) {
for _, g := range gs {
fmt.Println(g.Greet())
}
}
// Implementation is implicit:
// Satisfied as soon as the method set matches
// No `implements` declaration needed
// An interface value carries the concrete type
// Interfaces decouple parameters

Interfaces and nil

A nil interface is different from an interface holding a typed nil pointer. Be careful when checking for nil.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Error interface {
Error() string
}
// Pitfall:
func returnsNil() *MyError {
return nil
}
var err error = returnsNil()
// err != nil but it wraps a nil pointer
if err != nil {
fmt.Println("error") // misjudged
}
// Reason: an interface = type + value
// a nil pointer still has type info
// Fixes:
// 1. Return error rather than *T
// 2. Use reflection to check nil:
// reflect.ValueOf(err).IsNil()
// 3. Never store a nil pointer in an interface
// Ensure that when you return error, nil is the error's nil

Embedding

An embedded struct (or interface) promotes its fields and methods. Prefer composition over inheritance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Base struct {
ID int
}
func (b Base) GetID() int { return b.ID }
type User struct {
Base // embedded
Name string
}
// Promotion:
u := User{Base: Base{ID: 1}, Name: "Nick"}
u.GetID() // promoted method
u.ID // promoted field
// Override:
type Admin struct {
Base
Permissions []string
}
func (a Admin) GetID() int {
return a.Base.ID * 100 // custom logic
}
// Embedding an interface works too:
// type Reader interface { io.Reader }
// Composition over inheritance:
// fields + method promotion

Composition

Go favors composition over inheritance. Compose small interfaces to build larger ones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Interface composition:
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type ReadWriter interface {
Reader
Writer
}
// Struct composition:
type File struct {
name string
}
type GzipFile struct {
File // reuse file logic
level int
}
// Composition provides reuse without the fragility of inheritance
// Small-interface principle:
// The smaller the interface, the easier to satisfy
// Compose only when you need to
// Embed structs to reuse functionality

Type assertions

Assert an interface back to a concrete type. Use the ok form to stay safe; type switch handles multiple cases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var v any = "hello"
// Safe assertion:
s, ok := v.(string)
if ok {
fmt.Println(s)
}
// Unsafe assertion (panics on failure):
// s := v.(string)
// type switch:
switch val := v.(type) {
case string:
fmt.Println("string", val)
case int:
fmt.Println("int", val)
default:
fmt.Println("unknown", val)
}
// Failed assertion returns the zero value + false
// Asserting an interface to a more specific interface:
// if r, ok := w.(io.Reader); ok

any / empty interface

any (alias for interface{}) holds any value. Recover the concrete type with an assertion or a type switch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// `any` is an alias for interface{}:
var v any
v = 42
v = "str"
// Extraction requires an assertion:
s := v.(string)
// Generics alternative:
// Use generics when you need type parameters:
func Print[T any](v T) {
fmt.Println(v)
}
// Empty interface as a container:
var store = make(map[string]any)
store["age"] = 30
store["name"] = "Nick"
// Type-assert on extraction:
age, ok := store["age"].(int)
// Use with care:
// Loses type safety
// Prefer concrete types or generics
// `any` is common when (de)serializing JSON

11.Error Handling

The error interface, error wrapping, errors.Is/As, panic/recover.

The error interface

error is a built-in interface with a single method, Error() string. Functions return errors — there are no exceptions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type error interface {
Error() string
}
// Return an error:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
// Check:
result, err := divide(10, 0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
// Custom error type:
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return "field " + e.Field + " is invalid"
}
// A nil error means success

Returning errors

Errors bubble up the call stack. Check err != nil at each step and handle or wrap it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func openConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // pass through
}
if len(data) == 0 {
return nil, errors.New("config is empty")
}
return data, nil
}
// Call chain:
func load() error {
data, err := openConfig("app.json")
if err != nil {
return err // bubble up
}
_ = data
return nil
}
// Conventions:
// On error return zero value + err
// Other returns are valid only when err is nil
// Don't swallow errors:
// At least log them or return them

Error messages

fmt.Errorf builds a formatted error; errors.New constructs a plain one. Use %w to wrap an underlying error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"
// Simple error:
// errors.New("failed")
// With parameters:
func open(name string) error {
return fmt.Errorf("cannot open %s", name)
}
// Multi-layer wrapping:
// %w wraps the underlying error
func load() error {
err := readFile()
if err != nil {
return fmt.Errorf("load failed: %w", err)
}
return nil
}
// %v without wrapping:
// fmt.Errorf("failed: %v", err)
// New error instance:
// errors.New("message")
// Custom:
// fmt.Errorf("status code %d", 404)

errors.Is

errors.Is walks the error chain looking for a match. Prefer it over err == sentinel checks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "errors"
var ErrNotFound = errors.New("not found")
func find(id int) error {
return fmt.Errorf("find %d: %w", id, ErrNotFound)
}
// Check (works through wrapping):
err := find(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("not found")
}
// Multiple targets:
// errors.Is(err, ErrNotFound) || errors.Is(err, ErrTimeout)
// Single target:
// Define a sentinel error: var ErrX = errors.New(...)
// == cannot see through %w wrapping
// Standard library usage:
// errors.Is(err, os.ErrNotExist)

errors.As

errors.As walks the chain and assigns the first error matching the target type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
type ConfigError struct {
Path string
Msg string
}
func (e *ConfigError) Error() string {
return e.Path + ": " + e.Msg
}
// Wrap and raise:
func loadConfig() error {
return fmt.Errorf("loading: %w",
&ConfigError{Path: "app.json", Msg: "syntax error"})
}
// Extract:
err := loadConfig()
var configErr *ConfigError
if errors.As(err, &configErr) {
fmt.Println(configErr.Path, configErr.Msg)
}
// Difference from Is:
// Is compares error values (sentinels)
// As extracts an error type (with fields)
// Custom errors use pointer receivers
// Common for third-party library errors

panic and recover

panic aborts the goroutine; recover (only inside a deferred call) catches it. Reserve for unrecoverable situations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// panic:
func divide(a, b int) int {
if b == 0 {
panic("divide by zero") // crash
}
return a / b
}
// recover to catch:
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
f()
return nil
}
// Usage:
// err := safeCall(func() { divide(1, 0) })
// Rules:
// Only recover panic inside a defer
// Use only for errors the program cannot continue from
// Don't use panic for ordinary error handling
// Library code should avoid panic whenever possible

Error handling in defer

Use a named return value so a deferred function can assign a close error back to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Check error when closing a file:
func read() (err error) {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = closeErr // override return value
}
}()
// process...
return nil
}
// Close a connection:
// defer conn.Close()
// Unlock:
// mu.Lock()
// defer mu.Unlock()
// Clean up a temp file:
// defer os.Remove(tmp)
// Note: defer arguments are evaluated immediately

Joining errors

errors.Join combines multiple errors into one. Useful for validating many fields or steps at once.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
// Combine multiple errors:
err1 := errors.New("database failed")
err2 := errors.New("cache failed")
joined := errors.Join(err1, err2)
fmt.Println(joined)
// Each error on its own line
// Check any one error:
// errors.Is(joined, err1) // true
// All-nil returns nil:
// errors.Join(nil, nil) // nil
// Collect task errors:
func runAll() error {
var errs []error
if err := step1(); err != nil {
errs = append(errs, err)
}
if err := step2(); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}
// Go 1.20+
// An error can be Is-checked against multiple targets

12.Input / Output

File I/O, bufio, JSON, io.Reader/Writer.

Reading files

Use os.ReadFile for small files. For large files, os.Open plus a Reader (such as bufio.Scanner) streams line by line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// Whole file:
data, err := os.ReadFile("data.txt")
if err != nil {
log.Fatal(err)
}
// Read line by line:
f, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// Line-by-line doesn't hold the whole file in memory
// Chunked reading:
// use io.Reader + Read with a fixed buffer

Writing files

Use os.WriteFile to overwrite atomically, os.OpenFile to append, and bufio.Writer to batch writes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "os"
// Write all at once:
err := os.WriteFile("out.txt",
[]byte("hello"), 0644)
// Append:
f, _ := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
f.WriteString("more content\n")
// Buffered write:
w := bufio.NewWriter(f)
w.WriteString("text")
w.Flush() // flush to file
// Permissions:
// 0644 owner read/write, others read
// Create directory: os.MkdirAll
// Don't ignore errors

JSON marshaling

json.Marshal encodes structs to JSON. Use struct tags to control field names and options.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
// Omit when empty:
Email string `json:"email,omitempty"`
// Skip the field entirely:
Secret string `json:"-"`
}
// Marshal:
u := User{Name: "Nick", Age: 30}
data, err := json.Marshal(u)
// Pretty:
pretty, _ := json.MarshalIndent(u, "", " ")
// From a map:
m := map[string]any{"k": "v"}
b, _ := json.Marshal(m)
// Default field name:
// Exported field names are not lowercased; the original name is used
// Use struct tags to control the output field name

JSON unmarshaling

json.Unmarshal parses JSON into a struct or map. json.NewDecoder streams large payloads.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
// Parse a string:
var u User
err := json.Unmarshal([]byte(`{"name":"Nick","age":30}`), &u)
// Parse into a map:
var m map[string]any
json.Unmarshal(data, &m)
// Numbers in a map are float64:
age := m["age"].(float64)
// Streaming parse (large JSON):
dec := json.NewDecoder(bytes.NewReader(data))
dec.Decode(&u)
// Unknown fields:
// Decoder.DisallowUnknownFields() does the opposite
// Error handling:
// A syntax error returns *json.SyntaxError

io.Reader / Writer

io.Reader and io.Writer abstract streams. io.ReadAll and io.Copy are the convenience helpers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"io"
"strings"
)
// Read all from a Reader:
r := strings.NewReader("hello")
data, _ := io.ReadAll(r)
// Copy a stream:
// io.Copy(dst, src)
// Streaming pipe:
// io.Pipe()
// Composition:
// bufio.NewReader / bufio.NewWriter
// Bounded read:
// io.LimitReader(r, 100)
// Multi-reader:
// io.MultiReader(a, b)
// Multi-writer:
// io.MultiWriter(a, b)
// Discard sink:
// io.Discard discards writes

bufio buffering

bufio wraps a Reader or Writer with a buffer to reduce syscalls. Scanner makes line iteration easy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"os"
)
// Read:
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
// Read a fixed number of bytes:
buf := make([]byte, 1024)
reader.Read(buf)
// Scanner, line by line:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// Write:
w := bufio.NewWriter(os.Stdout)
w.WriteString("hello\n")
w.Flush()
// Buffer size:
// bufio.NewReaderSize(r, 64*1024)
// Scanner default line cap is 64K:
// sc.Buffer(buf, max)
// Performance: fewer syscalls

Standard streams

os.Stdin/Stdout/Stderr are the standard streams. fmt.Scan, fmt.Scanf, and bufio.Scanner cover most input needs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"fmt"
"os"
)
// Print:
fmt.Println("hello")
// To stderr:
fmt.Fprintln(os.Stderr, "error")
// Read input:
var name string
fmt.Scanln(&name)
// Formatted scan:
var age int
fmt.Scanf("%d", &age)
// Process stdin line by line:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// Pipe usage:
// cat data | go run app.go
// Redirect output:
// go run app.go > out.txt

Path operations

filepath handles paths portably. Join, Dir, Base, and Ext are the everyday helpers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"path/filepath"
"fmt"
)
// Join paths:
path := filepath.Join("dir", "sub", "a.txt")
// Dir / file / extension:
filepath.Dir(path) // dir/sub
filepath.Base(path) // a.txt
filepath.Ext(path) // .txt
// Clean redundant separators:
filepath.Clean("dir//./a.txt")
// Absolute path:
abs, _ := filepath.Abs(path)
// Relative path:
rel, _ := filepath.Rel("/a", "/a/b/c")
// Separator:
// Windows \ Unix /
// filepath.Separator
// Match / glob:
// filepath.Glob("data/*.json")
// Recursive walk: filepath.WalkDir
// Don't hardcode the separator

13.Common Pitfalls

The most common Go footguns and the correct idioms.

nil vs empty slice

A nil slice and an empty slice are different — JSON marshals nil as null and empty as []. Be explicit with make.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD: a nil slice marshals to null
var s []string
json.Marshal(s) // "null"
// GOOD: an empty slice marshals to []
s := make([]string, 0)
json.Marshal(s) // "[]"
// You can append to a nil slice:
var n []int
n = append(n, 1) // works
// Check:
// len(s) == 0 is true for both
// Convention for returning collections:
// Explicitly return an empty slice:
func list() []string {
return make([]string, 0)
}

Loop variable capture

Before Go 1.22, goroutines closing over a loop variable shared it. Since 1.22, each iteration gets its own variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD (pre-1.22): all goroutines print the last value
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // may all print 3
}()
}
// GOOD: pass the value explicitly
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
// 1.22+ each iteration gets a fresh loop variable:
// for i := range 3 { go ... } is safe
// Backwards-compatible workaround:
// take a local copy: x := i
// Passing the value as a parameter is the clearest

Maps iterate in random order

Map iteration order is randomized. Sort the keys if you need a deterministic order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD: depending on map iteration order
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v) // random order
}
// GOOD: iterate over sorted keys
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// Why: maps are hash-based, order is unspecified
// Output / JSON / tests need a stable order
// Slices preserve insertion order

Mutating strings

Strings are immutable. Convert to []byte (for ASCII) or []rune (for text), edit, and convert back.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD: cannot mutate a string directly
s := "hello"
// s[0] = 'H' error: cannot assign
// GOOD: convert to a mutable type
bs := []byte(s)
bs[0] = 'H'
s = string(bs) // "Hello"
// For Chinese text use []rune:
rs := []rune("你好")
rs[0] = '再'
s = string(rs) // "再好"
// Concatenation:
// + or strings.Builder
// Conversions to/from string copy the bytes
// Watch memory for large strings

Slice copying and sharing

Assigning a slice shares its backing array. Use copy (or a full-slice append) to make an independent one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD: thinking assignment copies the data
original := []int{1, 2, 3}
copySlice := original
copySlice[0] = 99
fmt.Println(original) // [99 2 3] !
// GOOD: use copy
// copy destination must have enough capacity
dst := make([]int, len(original))
copy(dst, original)
// or use the append pattern:
copySlice := append([]int{}, original...)
// append may still share the backing array:
// a new array is allocated only on growth
// Slices returned from functions may share storage
// Confirm ownership before mutating

Ignoring errors

Ignoring err returns silently breaks. Handle it, log it, or assign to _ to make the intent explicit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD: ignoring the error
f, _ := os.Open("data.txt") // f is nil on failure
f.Read(...) // nil pointer panic
// BAD: bare ignore
json.Unmarshal(data, &u) // not checked
// GOOD: check and handle
f, err := os.Open("data.txt")
if err != nil {
log.Fatalf("open failed: %v", err)
}
defer f.Close()
// Explicitly ignore when intentional:
_, _ = fmt.Println("x") // rare
// Convention:
// Always handle err != nil
// Add context before returning

Inconsistent receivers

Mixing value and pointer receivers on the same type leads to subtle interface-satisfaction issues. Pick one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD: mixed receivers
func (c Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ } // inconsistent
// Some methods only have pointer implementations:
// the value type does not satisfy the interface
// GOOD: use pointer receivers uniformly
func (c *Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ }
// Interface implementation:
var _ CounterIface = &Counter{} // pointer
// Compile-time assertion:
// var _ Shape = (*Circle)(nil)
// Use pointer receivers for mutable types
// Value receivers for small immutable types

Variable shadowing

:= in an inner scope creates a new variable, shadowing the outer one. Be explicit with = when you want reuse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD: err is shadowed
func f() error {
x, err := compute()
if err != nil {
return err
}
if y, err := other(); err != nil {
// err is a new inner-scope variable
return err
}
_ = x
return nil
}
// Shadowing leaves the outer err unassigned
// Detection:
// go vet reports shadowing
// or -gcflags
// GOOD: reuse err:
// y, err := other() already-declared vars can be reused
// Rule:
// Use = when re-assigning an outer variable
// Use := for a brand new variable

defer in loops

A defer in a loop only fires when the surrounding function returns. Wrap each iteration in a helper to release resources promptly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BAD: defers piling up inside a loop
for _, f := range files {
fh, err := os.Open(f)
if err != nil {
continue
}
defer fh.Close() // all run when the function returns
}
// many files opened but never closed
// GOOD: wrap in a function
for _, f := range files {
err := processFile(f)
if err != nil {
log.Println(err)
}
}
func processFile(name string) error {
fh, err := os.Open(name)
if err != nil {
return err
}
defer fh.Close() // closes on each call's return
return nil
}
// or close immediately

14.Concurrency

goroutines, channels, select, and concurrency safety.

goroutine

The go keyword starts a concurrent function call. Goroutines are cheap with growable stacks; when main exits the program ends.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// start a goroutine:
go fmt.Println("并发执行")
// anonymous function:
go func() {
fmt.Println("后台任务")
}()
// main does not wait for goroutines:
// when the program exits, all goroutines terminate
// use WaitGroup or channel to wait for completion
// example:
go func(n int) {
fmt.Println(n * 2)
}(10)
// note:
// do not rely on goroutine execution order
// handle errors inside the goroutine

channel

A channel is a typed conduit for goroutine communication. An unbuffered channel blocks send until a receive is ready and vice versa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// create a channel:
ch := make(chan int)
// send:
ch <- 42
// receive:
value := <-ch
// unbuffered synchronously:
// both sender and receiver block until paired
// close a channel:
close(ch)
// detect close on receive:
v, ok := <-ch
if !ok {
fmt.Println("已关闭")
}
// read-only / write-only:
// func f(ch <-chan int) // read-only
// func g(ch chan<- int) // write-only
// directional channels limit the usage surface

Buffered channels

make(chan T, n) creates a buffered channel. Sends block only when full, receives only when empty.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// buffer of 2:
ch := make(chan int, 2)
ch <- 1 // does not block
ch <- 2 // does not block
// ch <- 3 // buffer full, blocks
// take one out to free space:
<-ch
// use cases:
// task queue:
work := make(chan Task, 10)
// semaphore for throttling:
// make(chan struct{}, 5)
// producer-consumer decoupling
// caveats:
// the buffer only affects when sends block
// sending on a closed channel panics
// receive all buffered values before seeing the close

select

select waits on multiple channel operations. If several are ready, one is chosen at random; default runs immediately when none are.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
select {
case v := <-ch1:
fmt.Println("ch1", v)
case v := <-ch2:
fmt.Println("ch2", v)
case ch3 <- 1:
fmt.Println("发送成功")
default:
fmt.Println("无就绪,非阻塞")
}
// when multiple cases are ready, one is chosen at random
// an empty select blocks forever:
// select {}
// timeout pattern:
select {
case v := <-data:
fmt.Println(v)
case <-time.After(2 * time.Second):
fmt.Println("超时")
}
// loop over select to multiplex many streams

WaitGroup

sync.WaitGroup waits for a group of goroutines. Call Add before starting, Done (via defer) when finished, Wait to block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var wg sync.WaitGroup
// launch tasks:
for i := 0; i < 5; i++ {
wg.Add(1) // counter +1
go func(n int) {
defer wg.Done() // counter -1 when done
fmt.Println(n)
}(i)
}
// wait for all to complete:
wg.Wait()
// rules:
// Add must run before the goroutine
// use defer to ensure Done runs
// the counter cannot be reused after Wait
// for many tasks, call Add(n) outside the loop
// do not copy a WaitGroup after Add

Mutex

sync.Mutex protects shared state with Lock/Unlock. Use sync.RWMutex when reads dominate writes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock() // always unlock
balance += amount
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// read-write lock:
// var rw sync.RWMutex
// rw.RLock() read lock
// rw.RUnlock()
// reads can run concurrently, writes are exclusive
// avoid deadlocks:
// always lock in the same order
// do not call slow operations while holding a lock

sync.Once

sync.Once guarantees a function runs at most once, even under concurrent calls. Great for lazy singletons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import "sync"
var (
once sync.Once
instance *Config
)
// singleton pattern:
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
Env: os.Getenv("APP_ENV"),
}
// runs only once
})
return instance
}
// features:
// safe under concurrent goroutines
// executes exactly once (success or failure)
// used for:
// database connections, cache loading
// registry initialization
// difference from init:
// init always runs when the package loads
// Once is on-demand and can be reset in tests

Data race detector

go run -race (or go test -race) detects unsynchronized concurrent accesses to the same memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// detection:
// go run -race main.go
// go test -race ./...
// example race:
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // no lock, races
}()
}
// output:
// WARNING: DATA RACE
// fix:
// use a lock, channel, or atomic:
var c atomic.Int64
c.Add(1)
// best practices:
// keep -race on in tests
// integrate race checks in CI
// do not share values; pass messages

15.Networking

HTTP requests, servers, context, and TCP.

HTTP GET

http.Get issues a GET. Always close resp.Body (typically with defer) and check the status code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "net/http"
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // must close
// check the status code:
if resp.StatusCode != http.StatusOK {
log.Fatalf("状态码 %d", resp.StatusCode)
}
// read the response body:
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// response headers:
contentType := resp.Header.Get("Content-Type")
// note:
// http.Get has no timeout; use http.Client
// cap the read size for large responses

HTTP server

http.HandleFunc registers a handler; ListenAndServe starts the server. Use a custom *http.Server for graceful shutdown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ok": true}`)
})
// listen on :8080:
log.Fatal(http.ListenAndServe(":8080", nil))
}
// read a form field: r.FormValue("name")
// return JSON: json.NewEncoder(w).Encode(v)
// graceful shutdown: http.Server + Shutdown

HTTP client

Use http.Client to set timeouts and connection-pool options. http.NewRequest lets you customize method, headers, and body.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net/http"
"time"
)
// client with a timeout:
client := &http.Client{
Timeout: 10 * time.Second,
}
// custom request:
req, _ := http.NewRequest("POST",
"https://api.example.com",
strings.NewReader(`{"name":"go"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token")
// send:
resp, err := client.Do(req)
// reuse the connection pool:
// share one Client globally
// Transport options:
// MaxIdleConnsPerHost
// timeout types:
// Timeout covers the whole request including reading the response

JSON API

Decode JSON responses into typed structs. Use tags to map API field names to Go fields.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func fetchUsers() ([]User, error) {
resp, err := http.Get("https://api.example.com/users")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("状态码 %d", resp.StatusCode)
}
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return nil, err
}
return users, nil
}
// use Decoder to stream large responses
// unknown fields are ignored by default
// server field names match the struct tags

context

context carries cancellation and deadlines across calls. WithTimeout/WithCancel derive child contexts; thread it through I/O.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "context"
// with timeout:
ctx, cancel := context.WithTimeout(
context.Background(), 2*time.Second)
defer cancel() // release resources
// make a context-aware request:
req, _ := http.NewRequestWithContext(
ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
// cancellable:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel() // cancel all child tasks
}()
// passing it around:
// context goes as the first argument
// do not store it in struct fields
// derive: WithValue for request-scoped metadata

URL parsing

url.Parse splits a URL into scheme, host, path, query, and fragment. Build URLs with url.Values.Encode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "net/url"
u, err := url.Parse(
"https://user:[email protected]/path?q=go#sec")
// parts:
u.Scheme // "https"
u.Host // "example.com"
u.Path // "/path"
u.Fragment // "sec"
// query params:
q := u.Query()
q.Get("q") // "go"
// build a URL:
base, _ := url.Parse("https://api.com")
base.Path = "/v1/users"
params := url.Values{}
params.Set("page", "2")
base.RawQuery = params.Encode()
// encoding:
// url.QueryEscape / PathEscape
// combine via ResolveReference

TCP socket

net.Dial connects a client; net.Listen accepts on the server side. The connection is an io.ReadWriteCloser over raw bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net"
"bufio"
)
// client:
conn, err := net.Dial("tcp", "example.com:80")
defer conn.Close()
// send an HTTP request:
fmt.Fprintf(conn,
"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
// read the response:
resp, _ := bufio.NewReader(conn)
.ReadString('\n')
fmt.Println(resp)
// server:
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
go handleConn(conn) // one goroutine per connection
}
// write: conn.Write([]byte)
// set timeout: conn.SetDeadline

Routing enhancements

Since Go 1.22, http.ServeMux supports method matching (GET /path), path parameters ({id}), and wildcards.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "net/http"
// Go 1.22+ routing:
mux := http.NewServeMux()
// method + path:
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
// path parameters:
mux.HandleFunc("GET /users/{id}", getUser)
// wildcards:
mux.HandleFunc("/files/{path...}", serveFile)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // get the parameter
fmt.Fprintf(w, "用户 %s", id)
}
// old style:
// http.HandleFunc("/users/", handler)
// parse r.URL.Path manually
// mismatched method returns 405

16.Time & Date

The time package, Duration, formatting, and time zones.

Getting time

time.Now returns the current Time. Read its components; convert to UTC or Local with .UTC() / .In(loc).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
now := time.Now() // local time
// fields:
now.Year() // 2026
now.Month() // August
now.Day() // 2
now.Hour()
now.Minute()
now.Second()
now.Weekday() // Sunday
// UTC:
t := time.Now().UTC()
// Unix timestamps:
unix := time.Now().Unix() // seconds
milli := time.Now().UnixMilli() // milliseconds
// specific time:
// time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
// compare: t.Before(u) / t.After(u)

Duration

time.Duration is an int64 count of nanoseconds. Use named unit constants (time.Second, etc.) and add/subtract freely.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// constants:
// time.Nanosecond
// time.Microsecond
// time.Millisecond
// time.Second
// time.Minute
// time.Hour
// define a duration:
d := 2 * time.Second
// convert:
ms := d.Milliseconds()
ns := d.Nanoseconds()
// format: d.String() // "2s"
// parse:
d, _ = time.ParseDuration("1h30m")
// arithmetic:
// t.Add(d) / t.Sub(u)
// t.AddDate(0, 1, 0) adds one month
// sleep:
// time.Sleep(d)
// compare:
// d > time.Minute
// type-safe: cannot do plain int math on a Duration

Formatting

Time.Format uses a reference layout — Mon Jan 2 15:04:05 MST 2006 — to describe the desired format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
t := time.Now()
// standard formats:
t.Format("2006-01-02") // 2026-08-02
t.Format("15:04:05") // 14:30:22
t.Format("2006-01-02 15:04") // full
t.Format("2006-01-02 15:04:05 MST") // with timezone
// reference time is fixed:
// 01 month 02 day 03 hour 04 minute 05 second
// 06 year 07 month MST
// 2006 is the magic marker
// predefined:
// time.RFC3339: "2006-01-02T15:04:05Z07:00"
// time.RFC1123 / time.Kitchen
// an invalid layout returns an empty string

Parsing

time.Parse parses a string into Time using the same reference layout as Format. Use time.ParseInLocation for a specific zone.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
// parse with a layout:
t, err := time.Parse("2006-01-02", "2026-08-02")
// with timezone:
// time.Parse(time.RFC3339, "2026-08-02T10:00:00Z")
// parse in the local timezone:
// time.ParseInLocation(layout, value, time.Local)
// common errors:
// a wrong layout returns the zero value
// month/day must use the reference values
// example:
// time.Parse("2006/01/02 15:04", "2026/08/02 10:30")
// timezone handling:
// values without a zone parse as UTC
// use ParseInLocation when local time is required

Ticker

time.Ticker fires on a fixed interval. Stop it (with defer) when done to release resources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// fire every second:
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // release
// run periodically:
for range ticker.C {
fmt.Println("tick", time.Now())
}
// manual control:
// consume once only:
// <-ticker.C
// stop on demand:
for {
select {
case t := <-ticker.C:
fmt.Println("执行", t)
case <-stop:
return // exit
}
}
// note:
// forgetting Stop leaks the timer
// an unbuffered C drops ticks that arrive late
// shortcut: time.Tick does not release, use with care

Sleep and waiting

time.Sleep blocks the calling goroutine for the given duration. Use Timer/After for cancellable delays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "time"
// sleep for 2 seconds:
time.Sleep(2 * time.Second)
// dynamic duration:
wait := time.Duration(n) * time.Millisecond
time.Sleep(wait)
// milliseconds literal:
time.Sleep(500 * time.Millisecond)
// note:
// blocks the current goroutine
// not for precise scheduling
// speed up in tests:
// extract the duration into a variable for easy replacement
// difference from Timer:
// Sleep always blocks for the full duration
// Timer can be cancelled
// do not use Sleep as a timeout in select

Timer

time.Timer fires once after a delay. Stop or Reset it; combine with select for cancellation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "time"
// fire after 3 seconds:
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
// wait for the trigger:
<-timer.C
fmt.Println("定时到")
// cancel early:
// timer.Stop()
// detect cancellation:
// use select:
select {
case <-timer.C:
fmt.Println("执行")
case <-ctx.Done():
fmt.Println("已取消")
}
// reset:
// timer.Reset(5 * time.Second)
// one-shot vs Ticker repeating
// used for: timeouts, delayed tasks
// After is a convenience wrapper around Timer

Time zones

Load IANA zones with time.LoadLocation, convert a Time with .In(loc), or build a fixed-offset zone with time.FixedZone.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
// load a timezone:
loc, err := time.LoadLocation("Asia/Shanghai")
// convert to that timezone:
t := time.Now().In(loc)
// common timezones:
// time.UTC, time.Local
// "America/New_York"
// "Europe/London"
// "Asia/Tokyo"
// fixed offset:
fixed := time.FixedZone("CST", 8*3600)
// construct a time in a specific zone:
// time.Date(2026, 8, 2, 10, 0, 0, 0, loc)
// note:
// no IANA database returns an error
// on Windows you must import tzdata:
// import _ "time/tzdata"
// store times as UTC

17.Process & Environment

Command-line arguments, environment variables, subprocesses, and exit codes.

Command-line arguments

os.Args is the slice of command-line arguments. os.Args[0] is the program path; the rest follow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"fmt"
"os"
)
// all arguments:
// go run app.go a b c
// os.Args = ["app.go", "a", "b", "c"]
for i, arg := range os.Args {
fmt.Printf("args[%d] = %s\n", i, arg)
}
// only business arguments:
args := os.Args[1:]
// simple parsing:
if len(args) < 1 {
fmt.Println("用法: app <name>")
os.Exit(1)
}
name := args[0]
// note:
// arguments are space-separated; quoted values can contain spaces
// use the flag package for complex arguments
// arguments are strings

flag parsing

The flag package parses command-line flags. Define defaults, parse, then read the values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "flag"
// define flags:
name := flag.String("name", "Go", "用户名")
count := flag.Int("n", 1, "次数")
verbose := flag.Bool("v", false, "详细输出")
// parse:
flag.Parse()
// use:
for i := 0; i < *count; i++ {
fmt.Printf("%s (%v)\n", *name, *verbose)
}
// run:
// go run app.go -name Nick -n 3 -v
// positional arguments:
rest := flag.Args()
// custom usage message:
flag.Usage = func() { fmt.Println("用法...") }
// parsing errors:
// flag.Parse exits on error
// variable-bound form:
// flag.StringVar(&s, "x", "", "说明")

Environment variables

os.Getenv reads, os.Setenv sets, and os.LookupEnv distinguishes "unset" from "empty". os.Environ lists them all.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"fmt"
"os"
)
// read:
path := os.Getenv("PATH")
// with default:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// check existence:
val, ok := os.LookupEnv("APP_ENV")
if !ok {
fmt.Println("未设置 APP_ENV")
}
// set:
os.Setenv("APP_ENV", "production")
// unset:
os.Unsetenv("APP_ENV")
// list all:
// os.Environ() // ["KEY=value", ...]
// expand: os.ExpandEnv("$HOME/x")
// prefer environment variables for configuration

Exit codes

os.Exit terminates immediately with the given status. 0 means success, non-zero means failure. Defers don't run.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "os"
// successful exit:
// a normal return exits with code 0
// exit on failure:
if err != nil {
fmt.Fprintln(os.Stderr, "失败:", err)
os.Exit(1)
}
// note:
// os.Exit does not run deferred functions
// it does not flush buffered output
// conventions:
// 0 success
// 1 general error
// 2 usage error
// 64-113 reserved / signals
// do not call os.Exit from library code
// only use it in main
// log.Fatal also exits:
// log.Fatalf("错误: %v", err) // exit code 1

Running subprocesses

exec.Command runs an external program. Capture stdout/stderr, pipe stdin, or set environment variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os/exec"
"strings"
)
// run and capture output:
out, err := exec.Command("ls", "-l").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// combined output (includes stderr):
// cmd.CombinedOutput()
// capture stderr:
var stderr strings.Builder
cmd := exec.Command("go", "version")
cmd.Stderr = &stderr
// set environment:
cmd.Env = append(os.Environ(), "FOO=bar")
// provide input:
// cmd.Stdin = strings.NewReader("data")
// stream output:
// cmd.Stdout = os.Stdout
// run without capturing: cmd.Run()

Signal handling

signal.Notify delivers OS signals to a channel. Handle SIGINT/SIGTERM for graceful shutdown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"os"
"os/signal"
"syscall"
)
// listen for signals:
quit := make(chan os.Signal, 1)
signal.Notify(quit,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM) // kill
// wait for a signal:
<-quit
fmt.Println("收到退出信号,清理资源")
// save state, close connections, stop tasks
os.Exit(0)
// ignore a signal:
// signal.Ignore(syscall.SIGINT)
// stop listening:
// signal.Stop(quit)
// common uses:
// graceful server shutdown, interrupting CLI tools

File system

Create directories with MkdirAll, remove trees with RemoveAll, rename with Rename, and inspect with Stat.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// create directory (including parents):
os.MkdirAll("data/sub", 0755)
// remove a directory tree:
os.RemoveAll("data")
// remove a file:
os.Remove("tmp.txt")
// rename / move:
os.Rename("a.txt", "b.txt")
// file info:
info, err := os.Stat("data.txt")
if err == nil {
fmt.Println(info.Size())
fmt.Println(info.IsDir())
fmt.Println(info.ModTime())
}
// existence check:
if _, err := os.Stat("f"); os.IsNotExist(err) {
fmt.Println("不存在")
}
// temporary file:
// os.CreateTemp("", "prefix-*")
// list a directory: os.ReadDir("dir")

Working directory

Read the working directory with os.Getwd, change it with os.Chdir, and turn relatives into absolutes with filepath.Abs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os"
"path/filepath"
)
// current directory:
cwd, err := os.Getwd()
fmt.Println(cwd)
// change directory:
// os.Chdir("data")
// join paths:
path := filepath.Join(cwd, "config", "app.yaml")
// clean / resolve:
// filepath.Clean(path)
// filepath.Abs(path)
// common uses:
// config files with relative paths
// cache directory:
// os.UserCacheDir()
// config directory:
// os.UserConfigDir()
// executable path:
// os.Executable()
// note: the program's run directory differs from the source directory

18.Regular Expressions

The regexp package: compile, match, find, replace, and capture groups.

Compile

Use regexp.Compile to handle the error, or regexp.MustCompile when you want a panic on failure (typical at package scope).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "regexp"
// compile (can return an error):
re, err := regexp.Compile(`^[a-z]+$`)
if err != nil {
log.Fatal(err)
}
// fixed pattern: just use MustCompile:
re = regexp.MustCompile(`\d+`)
// compiling is expensive; compile outside loops:
// global variable:
var emailRe = regexp.MustCompile(
`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// shortcuts:
// regexp.MatchString recompiles every call
// precompile for hot paths
// a compiled regex is goroutine-safe and reusable

Match test

MatchString tests whether a pattern matches (or matches fully with ^...$). Match works on []byte.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var digitRe = regexp.MustCompile(`\d+`)
// match a string:
digitRe.MatchString("abc123") // true
// match bytes:
digitRe.Match([]byte("abc123")) // true
// exact match:
// wrap the pattern with ^...$
re := regexp.MustCompile(`^\d{3}$`)
re.MatchString("123") // true
re.MatchString("1234") // false
// case sensitivity:
// inline flag (?i)
// regexp.MustCompile(`(?i)^go$`)
// notes:
// greedy by default
// an empty match still counts as a match
// use cases: form validation, log filtering

Find

FindString returns the first match. FindStringIndex returns the start/end byte offsets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "regexp"
var re = regexp.MustCompile(`\bgo\b`)
// first match:
re.FindString("I love go and Go") // "go"
// bytes: re.Find([]byte(s))
// match position:
idx := re.FindStringIndex("go and go")
// idx = [0 2]
// all positions:
all := re.FindAllStringIndex("go and go", -1)
// [[0 2] [8 10]]
// limit the count:
// re.FindAllString(s, 2) at most 2
// submatch strings:
// re.FindStringSubmatch(s)
// presence check:
// re.FindString(s) != ""

Find all

FindAllString returns all matches; pass -1 (or omit) to get all, or an integer to cap the count.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
var re = regexp.MustCompile(`\d+`)
// all matches:
all := re.FindAllString("a1 b22 c333", -1)
// ["1" "22" "333"]
// limit to the first 2:
re.FindAllString(s, 2)
// count:
// len(all)
// bytes variant:
// re.FindAll(s, -1)
// positions:
// re.FindAllStringIndex(s, -1)
// capture groups per match:
// re.FindAllStringSubmatch(s, -1)
// example: extract every email
// note:
// no match returns a nil slice
// -1 means unlimited

Replace

ReplaceAllString substitutes every match. Use $1, $2 in the replacement to refer to capture groups.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var re = regexp.MustCompile(`\d+`)
// replace every digit:
re.ReplaceAllString("a1b2", "#")
// "a#b#"
// backreferences:
var nameRe = regexp.MustCompile(
`(\w+),\s*(\w+)`)
nameRe.ReplaceAllString("Doe, John", "$2 $1")
// "John Doe"
// limit count:
// re.ReplaceAllString(s, "x") takes no count
// use ReplaceAllLiteralString to avoid expansion:
// re.ReplaceAllLiteralString(s, "$1")
// callback form:
// re.ReplaceAllStringFunc(s, fn)
// example: increment every number
// no match returns the original string

Split

re.Split slices a string around matches of the regex. -1 means no limit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// split on commas:
var re = regexp.MustCompile(`,`)
parts := re.Split("a,b,c", -1)
// ["a" "b" "c"]
// split on whitespace:
spaces := regexp.MustCompile(`\s+`)
spaces.Split("a b\tc", -1)
// ["a" "b" "c"]
// limit the number of parts:
re.Split(s, 2) // at most 2 parts
// note:
// -1 means no limit
// empty matches produce special split behaviour
// alternatives:
// strings.Fields splits on whitespace
// strings.Split splits on a literal
// use a regex only when the pattern is complex
// remove empty entries yourself if needed

Capture groups

Parentheses capture sub-matches. FindStringSubmatch returns them; named groups (P<name>) look them up by name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
// capture group:
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
sub := re.FindStringSubmatch("2026-08-02")
// sub[0] = "2026-08-02" whole match
// sub[1] = "2026"
// sub[2] = "08"
// sub[3] = "02"
// named group:
nameRe := regexp.MustCompile(`(?P<year>\d{4})`)
sub = nameRe.FindStringSubmatch("2026")
idx := nameRe.SubexpIndex("year") // 1
sub[idx] // "2026"
// all groups:
// re.FindAllStringSubmatch(s, -1)
// non-capturing group:
// (?:...) does not consume an index
// count groups: re.NumSubexp()

Common patterns

Common patterns (email, URL, IP, phone) plus the core syntax: classes \d\w\s, quantifiers +*?, and anchors ^$.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// email:
regexp.MustCompile(`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// URL:
regexp.MustCompile(`^https?://[\w.-]+`)
// mobile phone (China):
regexp.MustCompile(`^1[3-9]\d{9}$`)
// ID number:
regexp.MustCompile(`^\d{17}[\dXx]$`)
// IP address:
regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`)
// syntax cheat sheet:
// \d digit \w word \s whitespace
// + one or more * zero or more
// {2,4} repetition range
// ^ start of line $ end of line
// greedy to lazy: add ? e.g. \d+?
// negated classes: \D \W \S
// character classes: [a-zA-Z] [^0-9]

19.Build & Test

Building, unit testing, static analysis, and cross-platform compilation.

go build

go build compiles a package. Use -o for the output path, -tags for build tags, and -ldflags for size/info tweaks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// build the current directory:
// go build
// specify output:
// go build -o bin/app .
// multiple packages:
// go build ./...
// compile without output (check only):
// go build ./...
// build tags:
// go build -tags "jsoniter"
// shrink the binary:
// go build -ldflags "-s -w"
// embed version info:
// go build -ldflags "-X main.version=1.2.3"
// cross-compile see next section
// caching:
// incremental builds use the build cache
// verify:
// go build && go vet

Unit testing

go test runs *_test.go files. Use t.Error/t.Errorf for non-fatal checks and t.Fatal/t.Fataliff to stop a test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// test file: calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
got := add(1, 2)
if got != 3 {
t.Errorf("add(1,2) = %d; want 3", got)
}
}
// table-driven test:
func TestAddTable(t *testing.T) {
cases := []struct{ a, b, want int }{
{1, 2, 3}, {0, 0, 0}, {-1, 1, 0},
}
for _, c := range cases {
if got := add(c.a, c.b); got != c.want {
t.Errorf("add(%d,%d)=%d; want %d",
c.a, c.b, got, c.want)
}
}
}
// run:
// go test // current package
// go test ./... // all packages
// go test -v -run TestAdd
// coverage: go test -cover

Benchmarking

go test -bench runs Benchmark functions. The framework loops b.N times to measure per-op cost.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "testing"
func BenchmarkConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
concat("a", "b") // function under test
}
}
// run:
// go test -bench=. -benchmem
// output:
// BenchmarkConcat-8 42984412
// 27.36 ns/op 16 B/op 2 allocs/op
// options:
// -bench=Concat match by name
// -benchmem show allocations
// -count=3 run multiple times
// -benchtime=5s fixed duration
// compare before/after optimization:
// run once before and once after to compare
// note:
// the compiler may inline / constant-fold calls
// the lack of warm-up is usually negligible

Static analysis

go vet catches common mistakes (printf mismatches, bad locks, etc.). Run it in CI alongside the build.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// check the current package:
// go vet
// check everything:
// go vet ./...
// common findings:
// 1. format string mismatch:
// fmt.Printf("%d", "x") type mismatch
// 2. lock misuse:
// copying sync.Mutex
// 3. useless assignments
// 4. string escape problems
// 5. out-of-range offsets / indices
// related tools:
// golangci-lint aggregates many more checks
// static analysis:
// go build -gcflags=-m shows inlining
// best practices:
// run before every commit
// integrate go vet ./... in CI

Formatting

gofmt is the canonical formatter. goimports adds automatic import management on top of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// format a single file:
// gofmt -w file.go
// format everything:
// gofmt -w .
// check without modifying:
// gofmt -l .
// recursive directory:
// gofmt -l ./...
// import ordering:
// goimports -w file.go
// editor integration:
// format on save
// team workflow:
// unified format, no style debates
// note:
// gofmt only changes formatting, never logic
// CI check:
// test -z "$(gofmt -l .)"
// mixed tabs and spaces are corrected automatically

Cross-compilation

Set GOOS and GOARCH before go build to cross-compile. CGO must be off or you need a cross C toolchain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// compile for Windows:
// GOOS=windows GOARCH=amd64 go build .
// compile for Linux:
// GOOS=linux GOARCH=amd64 go build .
// compile for macOS:
// GOOS=darwin GOARCH=arm64 go build .
// 32-bit:
// GOOS=linux GOARCH=386
// ARM:
// GOOS=linux GOARCH=arm64
// output name with a suffix:
// go build -o app.exe .
// available platform combos:
// go tool dist list
// note:
// CGO is disabled by default (only pure Go can cross-compile)
// cross-compile without the target environment
// typical release matrix covers common combos

Race detector

go build -race (or go test -race) instruments the binary so the runtime reports data races as they happen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// run with detection:
// go run -race main.go
// test with detection:
// go test -race ./...
// build with detection:
// go build -race
// what it detects:
// concurrent read/write across goroutines
// the same memory address
// without synchronization
// sample output:
// WARNING: DATA RACE
// Read at 0x... by goroutine 5
// fixes:
// add a mutex / use channels
// use atomic operations
// note:
// only detects at runtime
// coverage depends on tests
// do not ship -race binaries to production
// enable -race regularly in CI

Module versions

go.mod pins versions, go get updates them, replace directs a module to a fork/local path, and go.sum hashes them for integrity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// add a dependency:
// go get github.com/user/[email protected]
// update to latest:
// go get -u ./...
// upgrade a major version:
// go get module/v2@latest
// replace for local development:
// replace github.com/foo/bar => ../bar
// or point at a fork:
// replace foo => github.com/user/foo v1.0.0
// remove unused:
// go mod tidy
// verify checksums:
// go mod verify
// pinned versions:
// see the require block in go.mod
// go.sum ensures consistency:
// it stores hashes of every dependency

CI integration

A typical CI run checks format, runs go vet, executes go test -race -cover, then builds and publishes artifacts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// typical CI steps:
// 1. checkout the code
// 2. install Go:
// actions/setup-go
// 3. cache modules:
// $HOME/go/pkg/mod
// 4. check formatting:
// gofmt -l .
// 5. static analysis:
// go vet ./...
// 6. tests:
// go test -race -cover ./...
// 7. build:
// go build ./...
// 8. publish artifacts:
// cross-compile binaries for each target platform
// matrix:
// Go versions 1.21 / 1.22
// OS linux/windows/darwin
// gate:
// any failing test fails the build
// coverage threshold is optional

Official Links

Direct links to the official docs and resources.

About this Cheatsheet

This page is a self-contained Go 1.22 cheatsheet that covers the language core and the most-used parts of the standard library — roughly 80% of what real projects need. It favours modern idioms: short variable declarations, structured error handling with errors.Is / errors.As, generics, and (since 1.22) per-iteration loop variables. For authoritative reference see the official Go tutorial and Effective Go. The 19 sections each focus on one topic — from your first program to goroutines, interfaces, and common pitfalls. Each section splits into 8–14 short sub-topics (5–20 lines of code each) for about 150 topics in total. Code samples are intentionally short and self-explanatory. Everything happens in your browser — no uploads, no tracking. This page is part of GuruToolkit's free developer tool collection; the snippets here are free to use, with no warranty of any kind.

Version 2.1.0