Getting Started
Hello World
Every Go program starts in package main's main() function. Use 'go run' to execute, 'go build' to compile. gofmt automatically formats code (tabs, spacing). The import statement pulls in packages — fmt handles formatted I/O.
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}
// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.goGo Modules (go.mod)
Go modules (go.mod) manage dependencies since Go 1.11. 'go mod init' creates the module file. 'go get' adds dependencies. 'go mod tidy' removes unused imports and adds missing ones. The module path is the import path for your package.
// Initialize a new module
// $ go mod init github.com/user/project
// go.mod file:
module github.com/user/project
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
golang.org/x/sync v0.3.0
)
// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy // clean up unused depsPackage Structure
Go organizes code in packages — one package per directory. Capitalized names (Println, Add) are exported (public); lowercase names (private) are package-private. The 'internal' directory restricts imports to the parent module. The package name should match the directory name.
// project structure:
// myproject/
// go.mod
// main.go // package main
// utils/
// helper.go // package utils
// math.go
// internal/
// api/ // private (only importable within parent)
// main.go
package main
import (
"fmt"
"github.com/user/project/utils"
)
func main() {
fmt.Println(utils.Add(1, 2))
}
// Exported names start with uppercase letter
// unexported names start with lowercase letterVariables & Constants
Use 'var name type = value' for explicit declarations, 'name := value' for short declarations (functions only). Go has zero values: 0 for numbers, '' for strings, false for booleans, nil for pointers/slices/maps. Constants (const) are compile-time and can't use :=.
package main
import "fmt"
// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159
func main() {
// var with type
var name string = "Alice"
var age int = 30
// Short declaration (only inside functions)
city := "NYC"
// Multiple assignment
a, b, c := 1, 2, 3
// Zero values (default)
var x int // 0
var s string // ""
var ok bool // false
var p *int // nil
fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}Basic Types & Conversion
Go requires explicit type conversion — there's no implicit conversion like in C. rune is an alias for int32 (represents a Unicode code point). byte is an alias for uint8. Converting between numeric types may lose precision (float to int truncates). string(65) converts a code point to its character.
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias
// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n) // int -> float64
var s string = string(65) // 65 -> "A" (rune to string)
var i2 int = int(f) // float64 -> int (truncates)
// Type inference
var x = 42 // int
var y = 3.14 // float64
var z = len("hi") // intStrings & Formatting
String Basics
Go strings are immutable byte sequences. len() returns byte count, not character count — use utf8.RuneCountInString() for Unicode. Strings are compared lexicographically with ==, <, >. Range over a string iterates by rune (Unicode code point), handling multi-byte characters correctly.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, World"
// Length (bytes, not runes!)
fmt.Println(len(s)) // 12
// Concatenation
s1 := "Hello" + " " + "World"
s2 := fmt.Sprintf("%s = %d", "age", 30)
// Comparison
fmt.Println("abc" == "abc") // true
fmt.Println("a" < "b") // true (lexicographic)
// Iteration (by rune)
for i, r := range "Go语" {
fmt.Printf("%d: %c\n", i, r)
}
}strings Package
The strings package provides common string operations. Contains/HasPrefix/HasSuffix check for substrings. Split breaks by delimiter; Join combines. Replace takes a count (-1 for all). TrimSpace removes leading/trailing whitespace. All functions return new strings (strings are immutable).
import (
"fmt"
"strings"
)
s := "Hello, World"
// Case
strings.ToUpper(s) // "HELLO, WORLD"
strings.ToLower(s) // "hello, world"
strings.Title("hello world") // "Hello World"
// Search
strings.Contains(s, "World") // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World") // 7 (-1 if not found)
strings.Count(s, "l") // 3
// Split & Join
parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
joined := strings.Join(parts, "-") // "a-b-c"
// Replace
strings.Replace(s, "l", "L", 1) // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L") // "HeLLo, WorLd"
strings.TrimSpace(" hi ") // "hi"strconv & Formatting
strconv converts between strings and numbers — always check the error return. fmt.Printf formats output: %d (int), %f (float), %s (string), %t (bool), %T (type), %x (hex), %q (quoted). Use %.2f for 2 decimal places, %05d for zero-padding. Sprintf returns the string instead of printing.
import (
"fmt"
"strconv"
)
// String to number
n, err := strconv.Atoi("42") // string -> int
f, err := strconv.ParseFloat("3.14", 64)
// Number to string
s1 := strconv.Itoa(42) // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42) // int
fmt.Printf("Hex: %x\n", 255) // ff
fmt.Printf("Pad: %05d\n", 42) // 00042
fmt.Printf("Quote: %q\n", "hi") // "hi"
// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)Raw Strings & Multiline
Raw strings (backticks) preserve everything literally — no escape sequences, can span multiple lines. Use for regex, SQL, HTML templates. Interpreted strings (double quotes) process \n, \t, etc. For efficient string concatenation in loops, use strings.Builder (avoids O(n²) allocation).
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`
// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"
// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`
// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("line\n")
}
result := sb.String()Unicode & Runes
Go strings are UTF-8 encoded byte sequences. len() gives bytes; utf8.RuneCountInString() gives characters. Range over a string decodes UTF-8 automatically. To modify a string, convert to []rune, change, and convert back. This is essential for internationalized text processing.
import (
"fmt"
"unicode/utf8"
)
s := "Hello, 世界"
// Byte length vs rune length
fmt.Println(len(s)) // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)
// Decode runes manually
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%c ", r)
i += size
}
// Range automatically decodes runes
for i, r := range s {
fmt.Printf("%d:%c ", i, r)
}
// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"Control Flow
If / Else
Go's if doesn't need parentheses around conditions, but braces are mandatory (even for single-line bodies). The init statement (if x := f(); x > 0) is scoped to the if/else block — common for error checking. This pattern keeps variable scope tight.
// Basic if/else
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// If with initialization statement
if n := computeValue(); n > 100 {
fmt.Println("big:", n)
} else {
fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)
// No parentheses needed, but braces are requiredFor Loops
Go has only one loop keyword: 'for'. It handles C-style, while-style (for cond), infinite (for), and iteration (for range). Range works with slices, maps, strings, and channels. Use _ to skip the index or value. Map iteration order is random by design.
// C-style for
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style (condition only)
n := 10
for n > 0 {
n--
}
// Infinite loop
for {
break
}
// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("%d: %d\n", index, value)
}
// Skip index or value with _
for _, value := range nums {
fmt.Println(value)
}
// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
fmt.Println(key, val)
}Switch
Go's switch doesn't fall through by default (unlike C/Java) — each case is a separate branch. Use fallthrough to force it. Multiple values per case use commas. Switch with no expression acts as a cleaner if/else chain. Switch with init statement scopes the variable to the switch.
// Basic switch
day := 3
switch day {
case 1:
fmt.Println("Mon")
case 2, 3, 4, 5:
fmt.Println("Weekday")
case 6, 7:
fmt.Println("Weekend")
default:
fmt.Println("Invalid")
}
// Switch with no expression (like if/else chain)
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
default:
grade = "C"
}
// Switch with init
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Linux")
case "darwin":
fmt.Println("macOS")
}
// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two") // executes even though value is 1
}Defer
defer schedules a function call to run when the surrounding function returns — last-in-first-out (LIFO). Use it for cleanup (closing files, releasing locks, closing connections). Deferred calls' arguments are evaluated immediately, but the call executes at return. Defers run even if the function panics.
// Defer runs when function returns (LIFO order)
func main() {
defer fmt.Println("third")
defer fmt.Println("second")
fmt.Println("first")
}
// Output: first, second, third
// Common: resource cleanup
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns
// ... use f ...
return nil
}
// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2Goto, Break, Continue
break exits the innermost loop; continue skips to the next iteration. Labels (break outer) exit nested loops. goto exists but is rarely used — prefer structured control flow. Labels can also be used with continue to skip to the next iteration of an outer loop.
// Break and continue
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // skip even numbers
}
if i > 7 {
break // exit loop
}
fmt.Println(i)
}
// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // exits both loops
}
}
}
// Goto (rarely used, avoid)
i := 0
loop:
if i < 5 {
fmt.Println(i)
i++
goto loop
}Select (Channel Operations)
select is like switch for channels — it waits on multiple channel operations and picks the first one ready. The default case makes it non-blocking. Use select in a loop for event-driven patterns. time.After() creates a timeout channel. select picks randomly if multiple cases are ready, preventing starvation.
// Select waits on multiple channel operations
select {
case msg := <-messages:
fmt.Println("received:", msg)
case <-timeout:
fmt.Println("timed out")
default:
fmt.Println("no activity") // non-blocking
}
// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
}
// Select in a loop (event loop pattern)
for {
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(5 * time.Second):
return // timeout after 5s of inactivity
}
}Functions
Define & Multiple Returns
Go functions can return multiple values — the canonical pattern is (result, error). Named returns act as declared variables initialized to zero values; a 'naked' return uses them. Named returns improve readability for complex functions but can be confusing if overused. Always check errors immediately.
// Function with multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil
}
// Named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // "naked" return (uses named values)
}
// Usage
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
x, y := split(100)
fmt.Println(x, y)Variadic & Closures
Variadic functions (...T) accept any number of arguments, received as a slice. Spread a slice with .... Closures capture variables by reference from their enclosing scope — the counter() function returns a closure that remembers 'count'. Closures are useful for callbacks, iterators, and stateful functions.
// Variadic function (variable arguments)
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6
// Closure (function that captures variables)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3Functions as Values
Functions in Go are first-class values — they can be assigned to variables, passed as arguments, and stored in data structures. Define function types with 'type Name func(params) returns'. Maps of functions are useful for dispatch tables, command handlers, and strategy patterns.
// Functions are first-class values
func apply(f func(int) int, x int) int {
return f(x)
}
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25
// Function type
type MathFunc func(int) int
var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11
// Map of functions
ops := map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7init() & Anonymous Functions
init() functions run automatically before main(), in the order they're declared. Use for setup (config loading, validation, registration). Multiple init() functions per file are allowed. Anonymous functions can be immediately invoked (IIFE) or assigned to variables. They're also used for goroutines.
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main
import "fmt"
var config string
func init() {
// Setup code: config, connections, validation
config = "production"
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2, config:", config)
}
func main() {
fmt.Println("main")
}
// Output: init 1, init 2, main
// Anonymous function (IIFE)
result := func(x int) int {
return x * 2
}(5) // immediately invoked, result = 10
// Goroutine with anonymous function
go func(msg string) {
fmt.Println(msg)
}("async")Generics (Go 1.18+)
Go 1.18+ supports generics with type parameters [T any]. Type constraints (interfaces) restrict allowed types — use 'any' for no constraint, 'comparable' for == / != operators. Union constraints (int | float64) allow specific types. Generics enable type-safe reusable data structures and algorithms without code duplication.
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]
strs := Map(nums, func(n int) string {
return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]
// Type constraints
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}Methods
Methods are functions with a receiver argument. Value receivers (r Rectangle) work on a copy — can't modify the original. Pointer receivers (r *Rectangle) can modify and avoid copying large structs. Consistency matters: if one method uses a pointer receiver, all should. Methods can be defined on any type in the same package.
type Rectangle struct {
Width, Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// Method on non-struct types
type MyString string
func (s MyString) Shout() string {
return strings.ToUpper(string(s)) + "!"
}
// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width) // 20
s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!Data Structures
Arrays & Slices
Arrays have fixed length; slices are dynamic (backed by arrays). append() adds elements, growing capacity as needed. make([]T, len, cap) pre-allocates for efficiency. Slicing creates a view (shares underlying array) — use copy() for independent data. Always check len/cap when optimizing.
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred
// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4) // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10
// Slicing
sub := nums[1:4] // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:] // [4, 5, 6, 7]
// Copy
dst := make([]int, len(nums))
copy(dst, nums)
// Length and capacity
fmt.Println(len(nums), cap(nums))Maps
Maps are Go's hash tables — unordered key/value pairs. The comma-ok pattern (val, ok := m[key]) checks if a key exists. delete() removes a key. Map iteration order is random by design. Maps are reference types — passing them to functions shares the underlying data. nil maps can't be written to (use make()).
// Create a map
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Using make
ages := make(map[string]int)
ages["Charlie"] = 35
// Access
fmt.Println(m["Alice"]) // 30
// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
fmt.Println("not found")
}
// Delete
delete(m, "Bob")
// Iterate (random order)
for name, age := range m {
fmt.Printf("%s: %d\n", name, age)
}
// Length
fmt.Println(len(m))
// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}Structs
Structs group related fields. Use named initialization (Person{Name: ...}) for clarity. new() returns a pointer with zero values. Anonymous structs are useful for one-off data shapes. Struct embedding (no field name) promotes the embedded struct's fields and methods — Go's alternative to inheritance.
// Define a struct
type Person struct {
Name string
Age int
Address string // field names are exported (capitalized)
}
// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person) // returns *Person, zero values
p3.Name = "Charlie"
// Anonymous struct (one-off)
config := struct {
Port int
Host string
}{
Port: 8080,
Host: "localhost",
}
// Struct embedding (composition)
type Employee struct {
Person // embedded (promotes fields)
Salary float64
}
emp := Employee{
Person: Person{Name: "Dave", Age: 40},
Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)Pointers
Go pointers (*T) hold memory addresses. & takes address, * dereferences. Unlike C, Go has no pointer arithmetic (safer). Struct pointers allow shorthand (u.Name instead of (*u).Name). nil pointers cause panics on dereference. Go has garbage collection — no manual free() needed.
// Pointer basics
x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 (dereference)
*p = 100 // modify x through pointer
fmt.Println(x) // 100
// new() allocates and returns pointer
p2 := new(int) // *int, value 0
*p2 = 42
// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob" // (*u).Name shorthand
fmt.Println(u.Name) // Bob
// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference
// No pointer arithmetic (unlike C)
// p++ is illegalSlice Operations
Go doesn't have built-in filter/map/reduce — write them or use slices package (Go 1.21+). Insert/remove require shifting elements with append+copy. Be careful with slice aliasing: s[:i] and s[i+1:] share the underlying array. For 2D slices, allocate each row separately.
// Filter
func filter(nums []int, pred func(int) bool) []int {
result := []int{}
for _, n := range nums {
if pred(n) {
result = append(result, n)
}
}
return result
}
evens := filter([]int{1,2,3,4,5}, func(n int) bool {
return n%2 == 0
}) // [2, 4]
// Insert at index
func insert(s []int, i, v int) []int {
s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = v
return s
}
// Remove at index
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, 4)
}Sorting & Searching
sort.Ints/Strings/Float64s sort in place. sort.Slice with a comparator handles custom types. sort.Search* does binary search on sorted slices. Go 1.21+ adds the slices package with generic sort functions. All sorting is in-place — make a copy first if you need the original order.
import "sort"
// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)
// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]
// Sort with custom comparator
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // by age ascending
})
// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4
// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp funcMethods & Interfaces
Defining Interfaces
Interfaces define method signatures. Go uses structural typing — a type implements an interface automatically if it has all the required methods (no explicit 'implements' declaration). This enables decoupled design: define interfaces where you use them, not where you implement them.
// Interface: a set of method signatures
type Shape interface {
Area() float64
Perimeter() float64
}
// Implement implicitly (no 'implements' keyword)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Use the interface
func describe(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
s.Area(), s.Perimeter())
}
c := Circle{Radius: 5}
describe(c) // works! Circle implements ShapeEmpty Interface & Type Assertions
interface{} (or 'any' in Go 1.18+) holds any value. Type assertion (v.(T)) extracts the concrete type — panics if wrong type, so use the comma-ok pattern. Type switch (switch v.(type)) handles multiple types cleanly. Empty interface is useful for generic containers but loses type safety — prefer generics.
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
fmt.Println(v)
}
printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})
// Type assertion
var i any = "hello"
s := i.(string) // panics if not string
fmt.Println(s)
// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
fmt.Println("string:", s)
}
// Type switch
func describe(v any) {
switch x := v.(type) {
case int:
fmt.Printf("int: %d\n", x)
case string:
fmt.Printf("string: %s\n", x)
case []int:
fmt.Printf("int slice: %v\n", x)
default:
fmt.Printf("unknown type: %T\n", x)
}
}Interface Composition
Interfaces can embed other interfaces (composition). io.Reader and io.Writer are Go's most important interfaces — implemented by files, network connections, buffers, etc. This enables powerful abstractions: functions taking io.Reader work with any readable source. Small, focused interfaces (1-3 methods) are idiomatic.
// Compose interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter combines Reader and Writer
type ReadWriter interface {
Reader
Writer
}
// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn
func copyData(r io.Reader, w io.Writer) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if n > 0 {
if _, err := w.Write(buf[:n]); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}Pointer vs Value Receivers
Pointer receivers can modify the struct and avoid copying large values. Value receivers are safe (can't modify) and allow the method on both values and pointers. If any method has a pointer receiver, all should (for consistency). Pointer receiver methods satisfy interfaces for both T and *T; value receiver methods only for T.
type Counter struct {
count int
}
// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
return c.count
}
// Pointer receiver: can modify the original
func (c *Counter) Increment() {
c.count++
}
c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2
// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring bothStringer Interface
The Stringer interface (String() string) controls how a type appears in Print/Printf — like toString() in Java/JS. %v uses String(); %+v shows field names; %#v shows Go syntax. The error interface works the same way: implement Error() string to make any type an error. These are Go's most common built-in interfaces.
// Stringer interface (like toString() in other languages)
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Implement Stringer
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}
// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an errorType Embedding & Composition
Go uses composition instead of inheritance. Embedding a struct promotes its fields and methods to the outer struct. You can override promoted methods by defining a method with the same name on the outer type. Embedding an interface allows decorating/delegating — the outer type satisfies the interface and can forward calls.
// Embed a struct (composition over inheritance)
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — promotes Name and Speak()
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" (promoted)
fmt.Println(d.Speak()) // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works
// Override a promoted method
func (d Dog) Speak() string {
return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"
// Embed an interface
type Logger struct {
io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded WriterError Handling
Error Basics
Go handles errors as values, not exceptions. Functions return (result, error) — always check err != nil immediately. errors.New() creates simple errors; fmt.Errorf() adds formatting. Sentinel errors (var ErrX = errors.New()) enable comparison with ==. Never ignore errors (use _ only when intentional).
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
log.Fatal(err) // or handle gracefully
}
fmt.Println(result)
// fmt.Errorf for formatted errors
func validate(age int) error {
if age < 0 {
return fmt.Errorf("invalid age: %d (must be positive)", age)
}
return nil
}
// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
// handle not found
}Custom Error Types
Custom error types implement the error interface (Error() string). They carry structured data (fields, codes, context) beyond a simple message. Use type assertion (*ValidationError) to access the data. This is essential for domain-specific error handling — e.g., HTTP status codes, validation details, retry logic.
// Custom error type (implements error interface)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}
// Use the custom error
func validateUser(name string) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "cannot be empty",
}
}
return nil
}
// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
fmt.Println("Field:", ve.Field) // "name"
fmt.Println("Message:", ve.Message) // "cannot be empty"
}errors.Is & errors.As (Go 1.13+)
Go 1.13+ added error wrapping with %w (fmt.Errorf). errors.Is() checks if an error matches a sentinel (unwrapping the chain). errors.As() extracts a specific error type from the chain. This enables layered error handling: low-level errors wrapped with context, checked at high levels. Always wrap with %w (not %v) to preserve the chain.
import "errors"
// Wrapped errors with %w
var ErrNotFound = errors.New("not found")
func getUser(id int) error {
if id == 0 {
return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
}
return nil
}
// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("user not found")
}
// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
}
// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)Panic & Recover
panic() is for unrecoverable errors (bugs, invariant violations) — not for normal error handling. recover() catches panics but only works in deferred functions. Use panic/recover for: programming errors (index out of range), package initialization failures, and protecting goroutines from crashing the program. Prefer returning errors for expected failures.
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("invalid number: %s", s))
}
return n
}
// Recover: catch a panic (only in deferred functions)
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("something went wrong")
}
// Practical: safe goroutine
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("goroutine panic:", r)
}
}()
fn()
}()
}Error Wrapping Patterns
Wrap errors with context at each layer using fmt.Errorf with %w. This creates an error chain: top-level sees the full path (getUserProfile → fetchUser → sql error). The context (function name, parameters) helps debugging. Use errors.Is/As to check for specific errors in the chain. Avoid wrapping the same error multiple times with the same context.
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ... WHERE id = ?", id)
var u User
if err := row.Scan(&u.Name, &u.Age); err != nil {
return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
}
return &u, nil
}
func getUserProfile(id int) (*Profile, error) {
user, err := fetchUser(id)
if err != nil {
return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
}
// ...
}
// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
log.Printf("error: %v", err)
// Output: getUserProfile(42): fetchUser(42): sql: no rows
}
// golang.org/x/xerrors or pkg/errors for stack tracesDefer, Panic, Recover Together
Combine defer (cleanup), panic (fatal errors), and recover (catch panics) for robust resource management. Defers run in LIFO order, even during panics. The named return value (err error) can be set inside a deferred recover. This pattern ensures files/connections are closed and transactions are rolled back, even if code panics.
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
file, e := os.Open(path)
if e != nil {
return e
}
defer func() {
// Close file regardless of panic
file.Close()
// Recover from panic and convert to error
if r := recover(); r != nil {
err = fmt.Errorf("panic in processFile: %v", r)
}
}()
// Do work that might panic
data := parseFile(file) // might panic
return saveData(data)
}
// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
return fn(tx)
}Concurrency
Goroutines
Goroutines are Go's lightweight threads — start with 'go'. They're cheap (~2KB stack) and managed by Go's runtime scheduler (M:N scheduling). The main function doesn't wait for goroutines — use sync.WaitGroup or channels for synchronization. Never use time.Sleep for synchronization in production (use WaitGroup).
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Println(name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Launch goroutine with 'go' keyword
go sayHello("Alice")
go sayHello("Bob")
// Anonymous goroutine
go func() {
fmt.Println("anonymous goroutine")
}()
// Wait for goroutines (simple approach)
time.Sleep(1 * time.Second)
fmt.Println("done")
}
// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrentlyChannels
Channels are typed conduits for goroutine communication. Unbuffered (make(chan T)) blocks until both sender and receiver are ready (synchronous). Buffered (make(chan T, n)) blocks only when full (asynchronous). The sender should close channels, never the receiver. Range over a channel until it's closed. Channels enable 'share memory by communicating'.
// Unbuffered channel (synchronous)
ch := make(chan string)
// Send and receive
go func() {
ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)
// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1
// Close a channel (sender closes, never receiver)
close(buf)
// Range over channel (until closed)
for v := range buf {
fmt.Println(v)
}
// Check if closed
v, ok := <-buf
if !ok {
fmt.Println("channel closed")
}Select Statement
select lets a goroutine wait on multiple channel operations — it picks the first ready one (random if multiple). The default case makes it non-blocking. time.After() creates timeout channels. Select is the heart of concurrent Go: event loops, fan-in/fan-out, timeouts. Always include timeouts to avoid deadlocks.
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
// Wait for first to arrive
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received:", msg1)
case msg2 := <-ch2:
fmt.Println("received:", msg2)
}
}
// Timeout with select
select {
case result := <-slowOperation():
fmt.Println(result)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
// Non-blocking receive (default case)
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("no message") // runs if no data
}sync.WaitGroup
sync.WaitGroup waits for a group of goroutines to finish. Add(n) increments the counter, Done() decrements it (use defer), Wait() blocks until zero. Always pass loop variables as parameters to goroutines to avoid closure capture bugs (fixed in Go 1.22 but still recommended). WaitGroup is simpler than channels for fire-and-forget concurrency.
import "sync"
func main() {
var wg sync.WaitGroup
// Launch 5 goroutines
for i := 0; i < 5; i++ {
wg.Add(1) // increment counter
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // block until all Done() called
fmt.Println("all workers finished")
}
// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)Mutex & Sync
sync.Mutex protects shared state from concurrent access — Lock/Unlock with defer. RWMutex allows multiple readers or one writer (better for read-heavy workloads). sync.Once ensures initialization happens exactly once (singleton pattern). Prefer channels for communication, mutexes for protecting shared state. 'Share memory by communicating; don't communicate by sharing memory.'
import "sync"
// Mutex: mutual exclusion lock
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: multiple readers, one writer
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple allowed)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}
// sync.Once: run exactly once
var (
once sync.Once
instance *Database
)
func GetDB() *Database {
once.Do(func() {
instance = &Database{}
})
return instance
}Concurrency Patterns
Worker pool: fixed number of goroutines process jobs from a channel, sending results to another. This limits concurrency and prevents resource exhaustion. Fan-out/fan-in: distribute work across goroutines, then merge results. The key insight: close channels when no more data will be sent, so range loops terminate. These patterns are the foundation of concurrent Go.
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second) // simulate work
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
output := make(chan int)
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for v := range input {
output <- v * 2
}
}()
}
go func() { wg.Wait(); close(output) }()
return output
}File I/O & OS
Reading Files
os.ReadFile() reads an entire file into memory (simple but not for large files). For large files, open with os.Open(), read in chunks, and always defer Close(). bufio.Scanner reads line by line — ideal for text processing. Always check errors, especially io.EOF to know when reading is done.
import (
"io"
"os"
)
// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Read in chunks
buf := make([]byte, 1024)
for {
n, err := file.Read(buf)
if n > 0 {
// process buf[:n]
}
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
}
// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}Writing Files
os.WriteFile() creates/truncates and writes atomically (simple). os.OpenFile() with flags gives control: O_APPEND (add to end), O_CREATE (create if missing), O_TRUNC (truncate). Use bufio.Writer for many small writes (buffers in memory, flush at end). File mode 0644: owner can read/write, others can read.
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
log.Fatal(err)
}
// Open for writing (with options)
file, err := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Write
if _, err := file.WriteString("log entry\n"); err != nil {
log.Fatal(err)
}
// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!
// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNCFile Operations
os.Mkdir/MkdirAll create directories. os.ReadDir lists entries efficiently. filepath.WalkDir recursively traverses directories. os.Stat gives file info (size, mod time, permissions). os.IsNotExist checks if a file is missing. os.Remove deletes one file; RemoveAll deletes directories recursively. Always check errors.
// Create directory
os.Mkdir("mydir", 0755) // one level
os.MkdirAll("a/b/c", 0755) // nested directories
// List directory
entries, err := os.ReadDir(".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println(entry.Name(), entry.IsDir())
}
// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(path)
return nil
})
// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size()) // bytes
fmt.Println(info.ModTime()) // last modified
fmt.Println(info.IsDir())
// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
fmt.Println("file does not exist")
}
// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursiveEnvironment & Command Line
os.Getenv/LookupEnv/Setenv manage environment variables. os.Args gives raw command-line arguments. The flag package provides parsed flags with defaults and help text. LookupEnv distinguishes between unset and empty values. Environment variables are the standard way to configure 12-factor apps (API keys, database URLs).
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set
// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
fmt.Println("HOME:", val)
}
// All environment variables
for _, e := range os.Environ() {
fmt.Println(e)
}
// Command line arguments
// args := os.Args // []string, args[0] is program name
// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)
// Run: go run main.go -port 3000 -host 0.0.0.0Executing Commands
os/exec runs external commands. Command() creates the command; Output() captures stdout; Run() executes with custom Stdout/Stderr. Use CommandContext for timeouts (kills the process). Always check errors — exec.ExitError indicates non-zero exit codes. Be careful with user input to prevent command injection.
import "os/exec"
// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())
// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"
// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5sJSON & Encoding
JSON Marshal & Unmarshal
encoding/json converts between Go structs and JSON. Marshal (struct → JSON), Unmarshal (JSON → struct). Struct tags (json:"name") control field naming and visibility. omitempty skips zero/empty values. json:"-" excludes a field entirely. This is the standard way to handle API requests/responses in Go.
import "encoding/json"
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if empty
Password string `json:"-"` // never serialize
Age int `json:"age,omitempty"`
}
// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}
// Pretty print
pretty, _ := json.MarshalIndent(user, "", " ")
// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"
// Struct tags control field names:
// json:"name" -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-" -> skip entirely
// json:",omitempty" -> keep name, skip if emptyJSON with Maps & Slices
For dynamic JSON (unknown structure), unmarshal into map[string]any (or interface{}). JSON numbers become float64 — type-assert to access. json.Decoder/Encoder work with streams (files, HTTP bodies) efficiently. Use struct unmarshaling when you know the schema; use maps for flexible/dynamic data.
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`
var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)
// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
fmt.Println(s.(float64)) // JSON numbers are float64
}
// Encode a map
m := map[string]any{
"count": 42,
"items": []string{"a", "b", "c"},
"meta": map[string]any{"page": 1},
}
data, _ := json.Marshal(m)
// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)
// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(user)Custom JSON Marshaling
Implement MarshalJSON/UnmarshalJSON for custom serialization. This is useful for: computed fields, alternative formats (Money as '99.99 USD'), sensitive data handling, and time formatting. The method receiver for UnmarshalJSON must be a pointer to modify the struct. time.Time automatically serializes as RFC 3339 strings.
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
Amount float64
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"amount": m.Amount,
"currency": m.Currency,
"display": fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
})
}
func (m *Money) UnmarshalJSON(data []byte) error {
var v struct {
Amount float64
Currency string
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
m.Amount = v.Amount
m.Currency = v.Currency
return nil
}
money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}
// Time formatting
type Event struct {
Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by defaultHTTP Server
The net/http package builds HTTP servers. http.HandleFunc registers handlers. http.ResponseWriter writes the response; *http.Request reads the request. For JSON APIs, set Content-Type and use json.NewEncoder(w).Encode(data). The standard library is production-ready — no framework needed for simple APIs. Use mux routers (gorilla/mux, chi) for complex routing.
import (
"encoding/json"
"net/http"
)
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
}
func main() {
// Simple handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
// JSON API endpoint
http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
// Start server
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Request methods:
// r.Method -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body -> request body (io.ReadCloser)HTTP Client
http.Get/Post are convenient shortcuts. For custom headers, methods, or bodies, use http.NewRequest + client.Do(). Always defer resp.Body.Close() to avoid connection leaks. Set a timeout on the client (default is no timeout — dangerous). For production, reuse http.Client (it connection-pools) and use context for cancellation.
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewBuffer(body),
)
defer resp.Body.Close()
// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")Testing & Benchmarking
Unit Tests
Test files end with _test.go, test functions start with Test. Table-driven tests are the idiomatic pattern: define test cases in a slice, loop with t.Run for subtests (named, individually runnable). Use t.Errorf for failures (continues), t.Fatalf for fatal failures (stops). Run with 'go test -v' for verbose output.
// math_test.go (file must end with _test.go)
package math
import "testing"
// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
{"mixed", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
// Run: go test -v
// Run specific: go test -run TestAddTable/positiveBenchmarks
Benchmark functions start with Benchmark and use *testing.B. The b.N loop is adjusted by the runtime to get stable measurements. Run with 'go test -bench=.'. b.ResetTimer() excludes setup time. b.ReportAllocs() shows memory allocations. Compare implementations with 'benchstat' to verify improvements are significant.
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < 100; j++ {
s += "a"
}
}
}
// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8 300 4234567 ns/op
// BenchmarkStringConcat-8 200 6789012 ns/op 100 B/op 1 allocs/op
// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
data := setup() // setup not measured
b.ResetTimer() // start timing here
for i := 0; i < b.N; i++ {
process(data)
}
}
// Memory allocations
func BenchmarkAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
make([]int, 100)
}
}Test Helpers & Mocking
t.Helper() improves error messages by skipping helper functions in stack traces. Mock by implementing interfaces (Go's approach to mocking — no mock framework needed). t.Cleanup() registers cleanup functions (like defer, but for test scope). For complex mocking, use testify/assert and mockery or gomock for generating mocks from interfaces.
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// Interfaces for mocking
type DataStore interface {
Get(key string) (string, error)
}
type MockStore struct {
data map[string]string
}
func (m *MockStore) Get(key string) (string, error) {
if v, ok := m.data[key]; ok {
return v, nil
}
return "", errors.New("not found")
}
func TestService(t *testing.T) {
store := &MockStore{data: map[string]string{"key": "value"}}
svc := NewService(store)
result := svc.GetValue("key")
assertEqual(t, result, "value")
}
// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
t.Cleanup(func() {
// runs after test (LIFO order)
os.Remove("tempfile")
})
// test code
}Standard Library Highlights
Go's standard library is comprehensive. time handles dates/times (note: format uses reference time 2006-01-02). regexp for pattern matching. context for cancellation/timeouts across goroutines. sync.Pool for object reuse (reduces GC pressure). Go 1.21+ adds the slices and maps packages with generic utilities.
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")
// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)
// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork
// sync.Pool (reuse objects)
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)Goroutines Deep Dive
Starting Goroutines & WaitGroup
Goroutines are lightweight threads managed by the Go runtime (not OS threads) — you can spawn hundreds of thousands. sync.WaitGroup coordinates goroutine completion: Add(1) before starting, Done() when finished (use defer), and Wait() to block until the counter reaches zero. Always pass a pointer to the WaitGroup so all goroutines share the same counter.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signal completion when function returns
fmt.Printf("Worker %d started\n", id)
// ... do work ...
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // increment counter before starting goroutine
go worker(i, &wg)
}
wg.Wait() // block until all goroutines call Done()
fmt.Println("All workers finished")
}GOMAXPROCS & Scheduling
GOMAXPROCS controls how many OS threads run goroutines simultaneously — it defaults to the number of CPU cores and is almost always optimal. Go uses M:N scheduling: many goroutines on few OS threads. Blocking I/O or channel operations cause the scheduler to run other goroutines on the same thread. runtime.Gosched() explicitly yields. You rarely need to tune GOMAXPROCS.
package main
import (
"fmt"
"runtime"
)
func main() {
// GOMAXPROCS: number of OS threads available to run goroutines
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores
// Set it manually (rarely needed — default is optimal)
runtime.GOMAXPROCS(2)
// Goroutines are multiplexed onto OS threads (M:N scheduling)
// - Blocking syscalls (I/O) don't block other goroutines
// - The scheduler steals work across threads
// - A goroutine blocked on channel/IO yields its thread
// Number of goroutines currently running
go func() { runtime.Gosched() }() // yield to other goroutines
fmt.Println("Goroutines:", runtime.NumGoroutine())
}Goroutine Leaks & Prevention
A goroutine leak occurs when a goroutine blocks forever (e.g., sending on an unbuffered channel nobody reads) — it's never garbage collected. Prevent leaks by: buffering channels, using select with a cancellation channel/context, and always providing an exit path. Leaked goroutines accumulate memory and CPU. Use runtime.NumGoroutine() and pprof to detect leaks in production.
package main
// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
ch := make(chan int)
go func() {
ch <- 42 // blocks forever if nobody reads from ch
}()
return ch
}
// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
ch := make(chan int, 1) // buffered — sender never blocks
go func() {
select {
case ch <- 42:
case <-ctx.Done(): // exit when context is cancelled
return
}
}()
return ch
}
// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle controlsync.Once (One-Time Initialization)
sync.Once ensures a function executes exactly once across all goroutines — the standard way to implement thread-safe singletons and lazy initialization. It's more efficient than mutex-protected flag checking. The Do method blocks concurrent callers until the first invocation completes. sync.Once is also used internally in many standard library patterns like sync.OnceValue (Go 1.21+).
package main
import (
"fmt"
"sync"
)
var (
instance *Database
once sync.Once
)
type Database struct{ name string }
func GetDB() *Database {
// sync.Once guarantees the function runs exactly once,
// even if called from many goroutines simultaneously
once.Do(func() {
instance = &Database{name: "production"}
fmt.Println("Database initialized")
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
GetDB() // "Database initialized" prints only once
}()
}
wg.Wait()
}Worker Pool with Goroutines
The worker pool pattern limits concurrency to a fixed number of goroutines processing jobs from a channel. This prevents resource exhaustion from spawning unbounded goroutines. Workers use 'for job := range jobs' which exits when the channel is closed. close(jobs) signals all workers to stop. This is the canonical Go concurrency pattern for bounded parallelism.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { // exits when jobs channel is closed
results <- job * job // process and send result
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // signal workers to stop (range loop exits)
wg.Wait()
close(results)
for r := range results {
fmt.Println("Result:", r)
}
}Channels & Select
Channel Basics (Unbuffered & Buffered)
Unbuffered channels (make(chan T)) synchronize sender and receiver — the send blocks until a receiver is ready. Buffered channels (make(chan T, n)) allow n sends without a receiver, decoupling sender/receiver timing. Only the sender should close a channel (to signal 'no more values'). Receiving from a closed channel returns the zero value with ok=false. Range loops exit when the channel is closed.
package main
import "fmt"
func main() {
// Unbuffered: send blocks until someone receives (synchronous)
ch := make(chan int)
go func() {
ch <- 42 // blocks until main reads
}()
fmt.Println(<-ch) // 42
// Buffered: send blocks only when buffer is full
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block — buffer full
fmt.Println(<-buf) // 1 (FIFO)
// Close a channel (sender closes, never the receiver)
close(buf)
// Check if closed (ok is false when closed and empty)
val, ok := <-buf
fmt.Println(val, ok) // 2 true
// Range over channel until closed
nums := make(chan int, 3)
nums <- 10; nums <- 20; nums <- 30
close(nums)
for n := range nums {
fmt.Println(n) // 10, 20, 30
}
}Select Statement
select lets a goroutine wait on multiple channel operations simultaneously — it chooses the first ready case (randomly if multiple are ready). The default case makes select non-blocking. time.After creates a timeout channel. select is the heart of Go's concurrency coordination: multiplexing, timeouts, cancellation, and fan-in/fan-out patterns all build on it.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// select waits on multiple channel operations
// picks the first one that's ready (random if multiple ready)
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
// Non-blocking receive with default
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("no message ready") // runs immediately
}
// Timeout with time.After
select {
case msg := <-ch1:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("timed out")
}
}Fan-In & Fan-Out Patterns
Fan-out distributes work across multiple goroutines for parallelism; fan-in merges multiple channels into one. Together they form a parallel pipeline: fan-out workers process independently, then fan-in collects results. The fan-in uses a WaitGroup to close the merged channel only after all input channels are exhausted. These patterns are fundamental to Go's concurrent data processing.
package main
import (
"fmt"
"sync"
)
// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
outputs := make([]<-chan int, n)
for i := 0; i < n; i++ {
outputs[i] = process(input) // each worker reads from same input
}
return outputs
}
func process(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for v := range input {
output <- v * v // transform
}
}()
return output
}
// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}Directional Channels (Send/Receive Only)
Directional channel types (<-chan T for receive-only, chan<- T for send-only) enforce channel usage at compile time. A bidirectional channel implicitly converts to a directional type when passed to a function. This documents intent and prevents bugs — a producer function literally cannot receive from its own output channel. Use directional types in function signatures to make contracts clear.
package main
import "fmt"
// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
for i := 0; i < 3; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) { // can only receive
for v := range in {
fmt.Println("consumed:", v)
}
}
func main() {
ch := make(chan int)
// Directional restrictions are enforced at compile time
go producer(ch) // bidirectional chan converts to send-only
consumer(ch) // bidirectional chan converts to receive-only
// This prevents bugs: producer can't accidentally read,
// consumer can't accidentally send or close
}Ticker & Timer Channels
time.Ticker fires repeatedly at intervals — use ticker.C as a channel in select for periodic tasks. time.Timer fires once after a duration. Always call Stop() on tickers and timers to release resources and avoid leaks. time.After is a convenient one-shot timer that returns a channel (but can't be cancelled, so prefer NewTimer in select loops to avoid accumulation). Reset lets you reschedule a timer.
package main
import (
"fmt"
"time"
)
func main() {
// Ticker: fires repeatedly at intervals
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // always stop to release resources
go func() {
for t := range ticker.C { // ticker.C is a channel
fmt.Println("Tick at", t)
}
}()
// Timer: fires once after a duration
timer := time.NewTimer(2 * time.Second)
<-timer.C // blocks until timer fires
fmt.Println("Timer fired!")
// time.After: one-shot timer as a channel (no Stop needed)
select {
case <-time.After(1 * time.Second):
fmt.Println("1 second elapsed")
}
// Reset a timer (cancel and reschedule)
timer2 := time.NewTimer(5 * time.Second)
timer2.Reset(100 * time.Millisecond)
<-timer2.C
}Context Package
context.WithCancel & WithTimeout
context.Context carries cancellation, timeouts, and request-scoped values across goroutine boundaries. WithCancel returns a context and a cancel function; WithTimeout auto-cancels after a duration. Goroutines check ctx.Done() (a channel) in a select to know when to stop. ALWAYS call the cancel function (use defer) to release resources, even if the timeout fires — otherwise the context leaks.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done(): // cancelled or timed out
fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// WithCancel: manual cancellation
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, 1)
time.Sleep(2 * time.Second)
cancel() // stop the worker
time.Sleep(500 * time.Millisecond)
// WithTimeout: auto-cancel after duration
ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel2() // always call cancel to release resources
go worker(ctx2, 2)
time.Sleep(2 * time.Second) // worker stops after 1.5s
}Propagating Context Through Calls
Context should be the first parameter of every function that does I/O, and it should be propagated through the entire call chain. http.Request.Context() is automatically cancelled when the client disconnects. Passing ctx to database/HTTP operations (QueryRowContext, NewRequestWithContext) ensures they abort when the context is cancelled — preventing wasted work and resource leaks. Never store contexts in structs.
package main
import (
"context"
"database/sql"
"net/http"
)
// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
// Pass ctx to all blocking operations so they cancel together
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
return name, err
}
func handler(w http.ResponseWriter, r *http.Request) {
// r.Context() is cancelled when the client disconnects
ctx := r.Context()
name, err := GetUser(ctx, db, 42)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte(name))
}
// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)Context Values (Request-Scoped Data)
context.WithValue stores request-scoped data (like user IDs, trace IDs, auth tokens) that flows through the call chain. Use a custom key type (not a string) to avoid key collisions. Values should be data the request needs, not function parameters — the Go team recommends using it sparingly, mainly for cross-cutting concerns like tracing/auth. Always type-assert when retrieving values.
package main
import (
"context"
"fmt"
"net/http"
)
// Define a custom key type to avoid collisions
type contextKey string
const userIDKey contextKey = "userID"
// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := 42 // authenticate...
// WithValue creates a new context with the value
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
if v, ok := ctx.Value(userIDKey).(int); ok {
return v, true
}
return 0, false
}
func handler(w http.ResponseWriter, r *http.Request) {
if uid, ok := getUserID(r.Context()); ok {
fmt.Fprintf(w, "User ID: %d", uid)
}
}Graceful Shutdown with Context
Graceful shutdown lets in-flight requests complete before the server exits. signal.Notify captures OS signals (Ctrl+C, SIGTERM from container orchestrators). server.Shutdown(ctx) stops accepting new connections and waits for active ones to finish (up to the context timeout). This is essential for production servers — without it, active requests are abruptly terminated, causing errors and data corruption.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080", Handler: mux}
// Listen for OS interrupt signals (Ctrl+C, SIGTERM)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
<-stop // block until signal received
log.Println("Shutting down...")
// Give in-flight requests 10 seconds to finish
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Forced shutdown: %v", err)
}
log.Println("Server stopped gracefully")
}Context Deadline & Error Handling
context.WithDeadline cancels at an absolute time; WithTimeout cancels after a relative duration (WithTimeout is just WithDeadline(now+timeout)). ctx.Err() returns context.DeadlineExceeded or context.Canceled so you can distinguish why it stopped. Use errors.Is() to check context errors. Always check ctx.Err() at the start of long operations and use select with ctx.Done() during blocking waits.
package main
import (
"context"
"errors"
"fmt"
"time"
)
func slowOperation(ctx context.Context) (string, error) {
// Check if already cancelled before starting
if err := ctx.Err(); err != nil {
return "", err
}
select {
case <-time.After(3 * time.Second): // simulate slow work
return "result", nil
case <-ctx.Done(): // cancelled or deadline exceeded
return "", ctx.Err()
}
}
func main() {
// Deadline: cancel at a specific time
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
result, err := slowOperation(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Operation timed out")
} else if errors.Is(err, context.Canceled) {
fmt.Println("Operation was cancelled")
}
return
}
fmt.Println(result)
}HTTP Server & Client
HTTP Server (net/http)
net/http provides a production-ready HTTP server. http.HandleFunc registers handlers by path. Use json.NewEncoder(w).Encode() to write JSON responses and json.NewDecoder(r.Body).Decode() to parse request bodies. Always set Content-Type headers and check r.Method. http.Error sends an error status. ListenAndServe starts the server; wrap with log.Fatal to catch errors.
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
user.ID = 99
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users/1", getUser)
http.HandleFunc("/users", createUser)
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}HTTP Client & Requests
The default http.Client has NO timeout — always set one to avoid hanging forever on unresponsive servers. Use http.NewRequestWithContext to attach a context for cancellation/timeouts. ALWAYS defer resp.Body.Close() to avoid connection leaks. For production, reuse a single http.Client (it manages connection pooling). http.Get is a shortcut but lacks timeout and customization.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
panic(err)
}
defer resp.Body.Close() // ALWAYS close the body
var users []map[string]any
json.NewDecoder(resp.Body).Decode(&users)
fmt.Println(users)
// Custom client with timeout (default has NO timeout!)
client := &http.Client{Timeout: 30 * time.Second}
// POST with JSON body and context
body, _ := json.Marshal(map[string]string{"name": "Bob"})
req, _ := http.NewRequestWithContext(context.Background(),
"POST", "https://api.example.com/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err = client.Do(req)
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}Middleware Pattern
Middleware wraps handlers to add cross-cutting concerns (logging, auth, CORS, rate limiting) without modifying the handler itself. The signature func(http.Handler) http.Handler is the standard middleware type. Chain applies them in order (outermost first). This pattern is the basis of frameworks like Chi, Echo, and Gin. The ResponseWriter can be wrapped to capture status codes for logging.
package main
import (
"log"
"net/http"
"time"
)
// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call the wrapped handler
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
})
// Apply middlewares: Auth -> Logging -> handler
http.Handle("/", Chain(handler, Logging, Auth))
http.ListenAndServe(":8080", nil)
}Serve Static Files & Templates
http.FileServer serves static files from a directory; http.StripPrefix removes the path prefix so file paths resolve correctly. html/template renders HTML safely (auto-escapes to prevent XSS). template.Must panics on parse errors (fine for startup). Templates use {{.Field}} for data and {{range}} for iteration. For production, consider embedding files with go:embed instead of reading from disk.
package main
import (
"html/template"
"net/http"
)
func main() {
// Serve static files (CSS, JS, images)
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// HTML templates
tmpl := template.Must(template.ParseFiles("templates/index.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := struct {
Title string
Items []string
}{
Title: "My Page",
Items: []string{"Apple", "Banana", "Cherry"},
}
tmpl.Execute(w, data)
})
http.ListenAndServe(":8080", nil)
}
// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>Graceful Shutdown & go:embed
go:embed bundles files into the compiled binary at build time — enabling true single-binary deployments with no external file dependencies. Use //go:embed directives above a var declaration. embed.FS is a read-only virtual filesystem. fs.Sub creates a sub-filesystem (strips the directory prefix). This works for static assets, HTML templates, SQL migrations, config files, and more.
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// Embed static files into the binary (no external files needed)
// Access the embedded filesystem
sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
// go:embed benefits:
// - Single binary deployment (no external files)
// - Files are compiled into the executable
// - Works with templates, configs, migrations, etc.
// Embed a single file
//go:embed config.json
// var configData []byte
// Embed with pattern
//go:embed templates/*.html
// var templates embed.FS
http.ListenAndServe(":8080", nil)
}Sync Package
sync.Mutex & sync.RWMutex
sync.Mutex provides exclusive locking — only one goroutine can hold it at a time. sync.RWMutex allows multiple concurrent readers OR one exclusive writer — use it when reads far outnumber writes. Always pair Lock with defer Unlock to prevent deadlocks if the function panics. Embed the mutex in the struct (lowercase mu) to keep it private. Never copy a mutex (always use pointers).
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple readers OK)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *SafeCache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}sync.Map (Concurrent Map)
sync.Map is a concurrent-safe map optimized for specific scenarios: write-once-read-many (caches) or disjoint-key access across goroutines. It avoids locks on reads via atomic operations. However, for general-purpose concurrent maps, a regular map protected by sync.RWMutex is often faster and more ergonomic. sync.Map's API uses any (interface{}) for keys and values, losing type safety.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Store and Load (thread-safe, no locks needed)
m.Store("name", "Alice")
m.Store("age", 30)
val, ok := m.Load("name")
fmt.Println(val, ok) // Alice true
// LoadOrStore: atomic get-or-set
actual, loaded := m.LoadOrStore("name", "Bob")
fmt.Println(actual, loaded) // Alice true (already existed)
// Delete
m.Delete("age")
// Range over all entries
m.Range(func(key, value any) bool {
fmt.Printf("%v = %v\n", key, value)
return true // continue; return false to stop
})
// Use sync.Map when:
// 1. Keys are written once, read many times (caches)
// 2. Multiple goroutines read/write disjoint keys
// For most cases, a regular map + Mutex is simpler and faster
}sync.Cond (Condition Variables)
sync.Cond implements condition variables — goroutines wait for a condition to become true. Wait() atomically releases the lock and sleeps; Signal() wakes one waiter, Broadcast() wakes all. Always use a for loop around Wait() (not if) to handle spurious wakeups. Cond is useful for producer-consumer queues and waiting for state changes, though channels often provide a simpler alternative.
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
items []int
mu sync.Mutex
cond *sync.Cond
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
return q
}
func (q *Queue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // wake one waiting goroutine
}
func (q *Queue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
}
item := q.items[0]
q.items = q.items[1:]
return item
}
func main() {
q := NewQueue()
go func() {
time.Sleep(1 * time.Second)
q.Put(42)
}()
fmt.Println("Waiting for item...")
fmt.Println("Got:", q.Get()) // blocks until item available
}sync.Pool (Object Reuse)
sync.Pool reuses objects to reduce allocations and GC pressure — ideal for frequently allocated short-lived objects like bytes.Buffer. Get() returns a pooled object (calling New if empty); Put() returns it. Pools are cleared during GC, so don't rely on them for persistence. Always reset objects before reuse. The standard library uses sync.Pool extensively (http, json, fmt).
package main
import (
"bytes"
"sync"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer) // create when pool is empty
},
}
func Process(data []byte) string {
// Get a buffer from the pool (or create via New)
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf) // return it when done
buf.Reset() // clear before reuse
buf.Write(data)
buf.WriteString("-processed")
return buf.String()
}
// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictableerrgroup (Group with Error Propagation)
errgroup (from golang.org/x/sync) is a WaitGroup that returns the first error and can cancel remaining goroutines via context. g.Go() starts a goroutine; g.Wait() blocks and returns the first non-nil error. WithContext creates a context that's cancelled when any goroutine returns an error — so other goroutines stop early. This is the idiomatic way to run parallel operations that should all succeed or fail together.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
)
// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
g, ctx := errgroup.WithContext(context.Background())
results := make([]*http.Response, len(urls))
for i, url := range urls {
i, url := i, url // capture loop variables
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // cancels ctx, stopping other goroutines
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // first error returned
}
return results, nil
}
func main() {
urls := []string{
"https://example.com",
"https://golang.org",
}
resps, err := fetchAll(urls)
fmt.Println(len(resps), err)
}Generics (Go 1.18+)
Generic Functions & Type Parameters
Go generics (1.18+) use type parameters in square brackets: [T any] declares T as any type. The compiler infers types from arguments, so you rarely specify them explicitly. Generics enable type-safe reusable functions like Map/Filter/Reduce without interface{} and type assertions. 'any' is an alias for interface{} introduced with generics.
package main
import "fmt"
// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
squared := Map(nums, func(n int) int { return n * n })
fmt.Println(squared) // [1 4 9 16 25]
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
// Works with any type
words := []string{"go", "rust", "python"}
lengths := Map(words, func(s string) int { return len(s) })
fmt.Println(lengths) // [2 4 6]
}Type Constraints & comparable
Type constraints limit which types a generic accepts. 'comparable' is a built-in constraint for types supporting == (needed for map keys and comparisons). Custom constraints use type unions (int | float64) to allow specific types. The golang.org/x/exp/constraints package provides Ordered (for <, >, etc.). Constraints can combine type sets and methods.
package main
import "fmt"
// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Custom constraint with a type set
type Number interface {
int | int64 | float64 | float32
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
return struct{ First T; Second U }{first, second}
}
func main() {
fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
fmt.Println(Contains([]int{1, 2, 3}, 5)) // false
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
p := Pair("Alice", 30)
fmt.Println(p) // {Alice 30}
}Generic Data Structures
Generic types (Stack[T any]) create type-safe data structures that work with any type without interface{} boxing or type assertions. The type parameter T is part of the type, so Stack[int] and Stack[string] are distinct, compile-time-checked types. This eliminates an entire class of runtime type errors. Generic structs, methods, and interfaces all support type parameters.
package main
import "fmt"
// Generic Stack — works with any type, type-safe
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
// Type-safe stack of ints
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()
fmt.Println(val) // 2
// Type-safe stack of strings
strStack := &Stack[string]{}
strStack.Push("hello")
s, _ := strStack.Pop()
fmt.Println(s) // hello
// intStack.Push("oops") // compile error! type-safe
}Generic Constraints with ~ (Underlying Type)
The ~ prefix in a constraint matches any type whose underlying type is the named type — so ~string matches both string and type MyString string. Without ~, a constraint only matches the exact named type, which is rarely useful for custom types. Use ~ when you want generics to work with type aliases and named types derived from primitives (common in domain modeling).
package main
import "fmt"
// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
~string
}
func Join[S StringLike](items []S, sep string) string {
result := ""
for i, item := range items {
if i > 0 {
result += sep
}
result += string(item) // convert underlying type to string
}
return result
}
// Numeric constraint with ~
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func Double[T Integer](v T) T {
return v * 2
}
// Custom type based on int
type Score int
func main() {
type MyString string
items := []MyString{"a", "b", "c"}
fmt.Println(Join(items, "-")) // a-b-c
s := Double(Score(21))
fmt.Println(s) // 42
}Generics vs Interfaces (When to Use)
Choose generics when the algorithm is identical across types (collections, math, transforms) — they provide compile-time type safety with zero runtime overhead. Choose interfaces when different types need different implementations (polymorphism) or when you need runtime dispatch (dependency injection, mocking). They're complementary: generics can be constrained by interfaces (T Stringer) for the best of both worlds.
package main
import "fmt"
// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
String() string
}
type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }
type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }
func Print[T Stringer](v T) {
fmt.Println(v.String())
}
func main() {
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(3.14, 2.71)) // 3.14
Print(Dog{"Rex"}) // Dog: Rex
Print(Cat{"Whiskers"}) // Cat: Whiskers
}
// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> GenericsReflection
reflect.Type & reflect.Value
reflect.TypeOf returns the runtime type; reflect.ValueOf returns the runtime value. From a Type you can inspect struct fields, methods, and tags. From a Value you can read and (with CanSet) modify fields. Reflection is powerful but slow (10-100x slower than direct access) and bypasses compile-time type safety — use it sparingly, mainly for serialization, ORMs, and frameworks.
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Alice", Age: 30}
// Get type and value at runtime
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("Type:", t.Name()) // User
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("NumFields:", t.NumField())
// Iterate struct fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
tag := field.Tag.Get("json")
fmt.Printf("%s (%s) = %v [json:%s]\n",
field.Name, field.Type, value, tag)
}
// Name (string) = Alice [json:name]
// Age (int) = 30 [json:age]
}Modifying Values with Reflection
To modify a value via reflection, you must pass a pointer and call .Elem() to dereference it. CanSet() reports whether a field is assignable (exported fields of an addressable value). Unexported (lowercase) fields can be read but not set. SetInt/SetString/Set are type-checked at runtime and panic on mismatch. Reflection-based mutation is the basis of config parsers and ORMs.
package main
import (
"fmt"
"reflect"
)
func main() {
type Config struct {
Port int
Host string
}
cfg := Config{Port: 8080, Host: "localhost"}
// MUST pass a pointer to modify (ValueOf of a struct is a copy)
v := reflect.ValueOf(&cfg).Elem()
// Check if a field is settable
portField := v.FieldByName("Port")
fmt.Println("Settable:", portField.CanSet()) // true
// Modify fields by name
portField.SetInt(9090)
v.FieldByName("Host").SetString("0.0.0.0")
fmt.Println(cfg) // {9090 0.0.0.0}
// Set via interface (type-checked at runtime)
v.FieldByName("Port").Set(reflect.ValueOf(7070))
fmt.Println(cfg) // {7070 0.0.0.0}
// Unexported fields are NOT settable (CanSet returns false)
}Calling Methods Dynamically
Reflection can call methods dynamically by name — useful for RPC frameworks, plugin systems, and routing. MethodByName returns a Value; Call() invokes it with a slice of reflect.Value arguments and returns a slice of results. NumMethod/Method enumerate methods. Method calls via reflection are slow and bypass type safety, so use them only when the method name isn't known at compile time.
package main
import (
"fmt"
"reflect"
)
type Calculator struct{}
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }
func main() {
calc := Calculator{}
v := reflect.ValueOf(calc)
// Find a method by name
method := v.MethodByName("Add")
fmt.Println("Found:", method.IsValid()) // true
// Call with arguments (each arg must be a reflect.Value)
args := []reflect.Value{
reflect.ValueOf(3),
reflect.ValueOf(4),
}
results := method.Call(args)
fmt.Println("3 + 4 =", results[0].Int()) // 7
// List all methods
t := reflect.TypeOf(calc)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
}
// Method: Add, Type: func(main.Calculator, int, int) int
// Method: Mul, Type: func(main.Calculator, int, int) int
}Practical Use: Struct to Map (Tag-based)
This pattern — iterating struct fields and reading tags — is how encoding/json, YAML parsers, ORMs, and validation libraries work under the hood. reflect makes it possible to write generic code that processes any struct based on its tags. The json:'-' tag convention (skip this field) is standard. This is one of the most legitimate uses of reflection in Go.
package main
import (
"fmt"
"reflect"
)
// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
result := make(map[string]any)
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
// Dereference pointers
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Use the tag value if present, else the field name
key := field.Tag.Get(tag)
if key == "" {
key = field.Name
}
// Skip fields tagged with "-"
if key == "-" {
continue
}
result[key] = v.Field(i).Interface()
}
return result
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Pass string `json:"-"` // excluded
}
func main() {
u := User{Name: "Alice", Age: 30, Pass: "secret"}
m := StructToMap(u, "json")
fmt.Println(m) // map[age:30 name:Alice]
}Build & Tooling
go build, go run & go install
go build compiles to an executable; go run compiles to a temp file and runs it (great for development); go install puts the binary in $GOPATH/bin for global access. Go's standout feature is easy cross-compilation via GOOS/GOARCH — no toolchain needed. -ldflags='-s -w' strips debug info for ~30% smaller binaries. -X injects values (like version strings) at build time for CI/CD.
# Build an executable (outputs to current directory)
go build -o myapp # custom output name
go build -o bin/myapp ./cmd # build a specific package
# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac # Apple Silicon
# Run without building an executable (compiles to temp, runs, deletes)
go run main.go # run single file
go run ./cmd/server # run a package
# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp # now 'myapp' is on your PATH
# Build flags
go build -v # verbose: print packages being compiled
go build -race # enable race detector (for testing)
go build -ldflags="-s -w" # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0" # inject version at build timego mod (Module Management)
go mod manages dependencies via go.mod (dependency list) and go.sum (checksums for security). go get adds/upgrades deps; go mod tidy syncs the files (run before commits). Go uses Semantic Import Versioning: v2+ requires a /v2 path suffix. The module cache is shared at $GOPATH/pkg/mod. go mod vendor creates a vendor/ directory for reproducible, offline, or audited builds.
# Initialize a new module
go mod init github.com/user/myproject
# This creates go.mod:
# module github.com/user/myproject
# go 1.21
# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected] # specific version
# Tidy: add missing deps, remove unused ones
go mod tidy
# Download dependencies to local cache
go mod download
# Verify dependencies haven't been modified
go mod verify
# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor
# Upgrade dependencies
go get -u github.com/lib/pq # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected] # pin specific version
# View dependency graph
go mod graph | headgo test & Benchmarking
go test runs _test.go files; functions named TestXxx(t *testing.T) are tests, BenchmarkXxx(b *testing.B) are benchmarks. -race enables the data race detector (essential for concurrent code). -cover shows test coverage; -coverprofile generates a detailed report. Benchmarks run b.N iterations where N is auto-tuned. Table-driven tests (slices of input/expected) are the idiomatic Go testing style.
# Run all tests in the current package
go test ./...
# Verbose output
go test -v ./...
# Run a specific test
go test -run TestAdd -v
# Run benchmarks
go test -bench=. -benchmem
# Race detector (find data races)
go test -race ./...
# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # view in browser
# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
# got := Add(2, 3)
# if got != 5 {
# t.Errorf("Add(2,3) = %d, want 5", got)
# }
# }
#
# func BenchmarkAdd(b *testing.B) {
# for i := 0; i < b.N; i++ {
# Add(2, 3)
# }
# }go fmt, go vet & golangci-lint
go fmt/gofmt enforces a single canonical format — Go has no formatting debates. go vet catches common bugs (printf mismatches, lock copies, bad struct tags). golangci-lint aggregates dozens of linters and is the industry standard for CI. Run gofmt and go vet before every commit; add golangci-lint to CI for deeper analysis. Consistent tooling is a major reason Go codebases look uniform.
# go fmt: format code (the ONE true Go style)
go fmt ./... # format all files
gofmt -d main.go # show diff without changing
gofmt -w main.go # write changes in place
# go vet: static analysis for common mistakes
go vet ./... # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables
# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run # run all enabled linters
golangci-lint run --enable=gosec,goconst # enable specific linters
# Common linters:
# errcheck - check unchecked errors
# gosec - security issues
# govet - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused - find unused code
# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.goProfiling & pprof
pprof is Go's built-in profiler — CPU, memory, goroutine, and mutex profiling. For long-running servers, import _ 'net/http/pprof' to expose a /debug/pprof/ endpoint for live profiling without restarts. go tool pprof gives an interactive shell (top, list, web) or a web UI. The execution tracer (go tool trace) visualizes goroutine scheduling and blocking. Profiling is essential for performance-critical Go code.
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)
# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof
# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30
# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...
# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out # opens in browserDatabase (SQL)
database/sql Basics
database/sql is the standard interface for SQL databases. The driver is imported for its side effects (registering itself). sql.Open does not establish a connection; use Ping to verify. Always defer db.Close().
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()Query Rows
Query returns multiple rows. Always defer rows.Close() to release resources. Scan copies column values into variables. Check rows.Err() after the loop for iteration errors.
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
var id int; var name string
if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
fmt.Printf("%d: %s\n", id, name)
}Query Single Row
QueryRow returns a single row. Scan returns sql.ErrNoRows if no row matches. Cleaner than Query for single-row lookups. Always handle ErrNoRows explicitly.
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}Prepared Statements
Prepare creates a reusable statement, improving performance for repeated queries. Prevents SQL injection. Always defer stmt.Close(). Use for queries executed multiple times.
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")Transactions
Begin starts a transaction. All operations within tx are atomic. defer Rollback is safe: it is a no-op after Commit. If any operation fails, Rollback undoes all changes.
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback() // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()Testing Deep Dive
Basic Test
Test functions start with Test and take *testing.T. t.Errorf logs failure and continues. t.Fatalf logs and stops. Run with go test. Use testify/assert for cleaner assertions.
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d; want %d", got, want)
}
}Table-Driven Tests
Table-driven tests are idiomatic in Go. Define test cases as a slice of structs. Loop through and run each. t.Run creates subtests for individual failures.
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
}
}
}Subtests
Subtests use t.Run with a name and function. Run specific subtests with -run pattern. Provides better test organization and output.
func TestAdd(t *testing.T) {
t.Run("positive", func(t *testing.T) {
if Add(1, 2) != 3 { t.Error("failed") }
})
t.Run("negative", func(t *testing.T) {
if Add(-1, -2) != -3 { t.Error("failed") }
})
}
// Run specific: go test -run TestAdd/negativeTest Main
TestMain runs once for the package, replacing the default test runner. Use for global setup/teardown. Must call m.Run() to execute tests. os.Exit propagates the exit code.
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }Mocking Interfaces
Go mocking relies on interfaces. Define an interface, implement a mock, and inject it. Tools like mockery and mockgen auto-generate mocks. Enables unit testing without external dependencies.
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
if v, ok := m.data[id]; ok { return v, nil }
return "", errors.New("not found")
}
// Use in tests by injecting MockStoreBenchmarking
Basic Benchmark
Benchmark functions start with Benchmark and take *testing.B. b.N is adjusted by the framework to get stable measurements. Run with go test -bench. Output shows ns/op (nanoseconds per operation).
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8 1000000000 0.3 ns/opSub-Benchmarks
Sub-benchmarks use b.Run. ResetTimer excludes setup time. ReportAllocs shows memory allocations. Compare performance across input sizes.
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := generateData(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sort(data)
}
})
}
}Memory Allocations
ReportAllocs shows memory allocations per operation. Reducing allocations is key to Go performance. Use sync.Pool, pre-allocate slices, and avoid unnecessary string concatenation.
func BenchmarkConcat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := "a" + "b" + "c"
_ = s
}
}
// Output includes allocs/op and B/opParallel Benchmarks
RunParallel runs benchmarks concurrently across multiple goroutines. Useful for testing thread-safe code. pb.Next() iterates the work distribution. Measures throughput under concurrency.
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(2, 3)
}
})
}Benchmark Comparison
benchstat compares benchmark results statistically. Run benchmarks multiple times with -count=10 for reliable comparison. Helps verify performance improvements and detect regressions.
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name old time/op new time/op delta
# Add-8 2.3ns 1.8ns -21.7%Profiling (pprof)
CPU Profiling
StartCPUProfile writes CPU profile to a file. StopCPUProfile flushes data. Analyze with go tool pprof. Use top, list, web commands in pprof. Focus on functions consuming the most CPU.
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.profMemory Profiling
WriteHeapProfile captures the current heap state. Call runtime.GC() first for accurate results. Set MemProfileRate = 1 to profile every allocation (slower but precise).
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocationsHTTP Profiling
net/http/pprof registers profiling endpoints on the default mux. Access profiles via HTTP without restarting. Useful for production diagnostics. Secure the endpoint in production.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profilepprof Commands
top shows functions consuming most resources. list shows annotated source code. web opens an SVG call graph. tree shows the call hierarchy. Use focus to filter.
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10 # Top functions by CPU
(pprof) list Add # Show source with timings
(pprof) web # Open graph in browser
(pprof) tree # Call tree
(pprof) png > out.png # Save graph as PNGTrace
runtime/trace captures execution traces: goroutine scheduling, GC, syscall blocking. go tool trace opens a web UI. Useful for diagnosing latency and concurrency issues.
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.outCGO Basics
Basic CGO
CGO enables calling C from Go. C code is in comments above import "C". The import must be immediately after the comment. CGO slows builds and prevents cross-compilation.
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
func main() {
C.hello()
}Passing Strings
C.CString allocates a C string (must free with C.free). CString copies the data. Use unsafe.Pointer for conversion. Always free C-allocated memory to avoid leaks.
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)Calling C Libraries
#cgo LDFLAGS links C libraries. #cgo CFLAGS sets compiler flags. Include system headers with #include. CGO bridges Go and existing C libraries like libm, libcrypto.
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result) // 4C to Go Callback
Go functions exported with //export can be called from C. The function must be in package main. Enables C libraries to call back into Go. Used in FFI bindings.
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
fmt.Printf("Called with %d\n", n)
}Performance Notes
CGO calls have significant overhead compared to Go function calls. Avoid in performance-critical code. Batch operations to reduce cross-boundary calls. Set CGO_ENABLED=0 for pure Go builds.
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go buildWeb Frameworks
net/http Server
net/http is the standard library HTTP server. HandleFunc registers handlers. ListenAndServe starts the server. Default mux is fine for simple apps; use custom mux for production.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))Gin Framework
Gin is a high-performance HTTP framework. Param extracts URL parameters. JSON serializes responses. Gin provides routing, middleware, and JSON validation. Faster than net/http due to httprouter.
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")Middleware
Middleware wraps handlers to add cross-cutting concerns: logging, auth, CORS. c.Next() calls the next handler. gin.Recovery() prevents crashes from panics. Order matters.
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
}
}
r := gin.New()
r.Use(Logger(), gin.Recovery())Echo Framework
Echo is another popular framework, similar to Gin. Handlers return errors for centralized error handling. Built-in middleware for CORS, JWT, rate limiting. Clean API design.
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")Static Files
FileServer serves static files. StripPrefix adjusts the URL path. Useful for serving HTML, CSS, JS, and images. For production, use a CDN or nginx for static assets.
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")gRPC
Define Proto
Protocol Buffers define the service contract. proto3 is the latest syntax. service defines RPC methods. message defines data structures. Generate Go code with protoc.
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }Generate Code
protoc generates Go code from .proto files. --go_out generates message types. --go-grpc_out generates service stubs. The generated code is not edited manually.
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfacesServer Implementation
Embed UnimplementedGreeterServer for forward compatibility. Implement the service methods. grpc.NewServer creates the server. Register the service before serving.
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)Client
grpc.Dial establishes a connection. WithInsecure disables TLS (use credentials.NewTLS for production). The client stub provides typed methods. Connections are pooled and reused.
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)Streaming
gRPC supports three streaming patterns. stream keyword marks streaming. Server streaming: one request, many responses. Bidirectional: both sides stream. Useful for real-time data.
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);Error Wrapping
Wrapping Errors
Use %w verb to wrap errors, preserving the original. This creates an error chain. Avoid %v for wrapping as it loses the chain. Wrapping adds context without losing the root cause.
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}Unwrapping
errors.Is checks if any error in the chain matches. errors.As extracts a specific error type from the chain. Use Is for sentinel values, As for typed errors. Both traverse the wrap chain.
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}Custom Errors
Custom error types implement the error interface. They carry structured data for error handling. Use errors.As to extract the custom type. Prefer typed errors over string comparisons.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
return nil
}Sentinel Errors
Sentinel errors are package-level error variables. Use for expected error conditions. Check with errors.Is, never with ==. Export them for users to check against.
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
if id > 100 { return nil, ErrNotFound }
return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }Error Handling Patterns
Handle errors immediately when possible. For deferred operations like Close, capture the error. Named return values allow deferred functions to modify the return value. Always check Close errors.
// Immediate handling
if err := doSomething(); err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
f, e := os.Open("file.txt")
if e != nil { return e }
defer func() {
if cerr := f.Close(); err == nil { err = cerr }
}()
// ...
}Common Pitfalls
Goroutine Leaks
Goroutines leak when they block forever. Always provide an exit path: context cancellation, close channels, or buffered channels. Use runtime.NumGoroutine() to detect leaks.
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
go func() { ch <- 1 }() // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
go func() {
select {
case ch <- 1:
case <-ctx.Done():
}
}()
}Channel Close
Only the sender should close a channel, never the receiver. Closing signals no more values. Receiving from a closed channel returns the zero value. Sending to a closed channel panics.
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
defer close(ch) // Sender closes
for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }Loop Variable Capture
Before Go 1.22, loop variables were shared across iterations. Goroutines capturing them see the final value. Go 1.22+ fixes this by creating a new variable per iteration. Pass as parameter for older versions.
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }() // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
go func(i int) { fmt.Println(i) }(i)
}Map Concurrency
Maps are not safe for concurrent use. Concurrent reads and writes cause a runtime panic. Use sync.Mutex for explicit locking or sync.Map for read-heavy concurrent access.
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()Nil Interface
A nil pointer wrapped in an interface is not nil. The interface has a type even if the value is nil. Always return nil directly, not a nil typed pointer. Check with reflect or return nil explicitly.
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
var err *MyError = nil
return err // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
return nil
}Related Go snippets
Copy-paste ready code for common tasks.
goroutine
Implement concurrency using goroutines.
channel
Communicate between goroutines using channels.
select
Multiplex channels using select.
mutex Mutex
Protect shared data using sync.Mutex.
defer Deferred Call
Usage and execution order of defer.
error Handling
Go's error handling pattern.
interface
Define and implement interfaces.
Struct Embedding
Implement composition via embedding.
Generics
Using generics in Go 1.18+.
context
Control timeout and cancellation using context.
File Operations
Read and write file operations.
HTTP Server
Create an HTTP server.
HTTP Client
Send HTTP requests.
JSON Encoding/Decoding
Convert between structs and JSON.
String Processing
Common operations in the strings package.
Slice Operations
Common slice operations.
map Operations
CRUD operations on map.
Time Handling
Common operations in the time package.
Regular Expressions
Using the regexp package.
Testing
Write unit tests.
Was this helpful?