Go Handbook
Go (Golang) is a statically typed, garbage-collected language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson, released in 2009. It was explicitly designed to solve the pain points of large-scale software engineering: slow C++ build times, complex dependency management, and difficult concurrency. Go compiles to a single statically linked binary, ships a strong standard library (HTTP, JSON, crypto, SQL, testing — all built in), and has a built-in concurrency model based on goroutines and channels (CSP). Docker, Kubernetes, Terraform, Prometheus, and CockroachDB are written in Go.
Pick Go when
- Network services and microservices — Go's stdlib HTTP server is production-grade. Goroutines are cheap (2 KB stack) so you can handle 100k+ concurrent connections on a single box without async/await complexity.
- CLI tools and DevOps tooling — Go produces a single, cross-compiled binary with no runtime dependency. Distributing a Go tool is trivially easy. kubectl, Terraform, and gh are all Go CLIs.
- Cloud infrastructure — Kubernetes, Docker, Containerd, etcd, Prometheus, and most cloud-native projects are in Go. If you're building infrastructure tooling, Go is the default.
- Teams that value simplicity and code review speed — Go has a small language spec, strict formatting (gofmt), and opinionated tooling. PRs are easier to review because there is usually one idiomatic way to do something.
- Fast iteration on backend services — compilation is near-instant (seconds for large projects), tests run in parallel by default, and the tooling (go test, go build, go vet) works out of the box.
Think twice before choosing Go when
- You need rich generics or functional patterns — Go added generics in 1.18 but the implementation is deliberately constrained. No higher-kinded types, no sum types, no pattern matching. Haskell, Scala, or Rust are better for type-level programming.
- GUI or frontend applications — Go has no mature native GUI framework. Use Flutter/Dart, Swift, or Kotlin for desktop/mobile.
- Data science or ML — the ecosystem is thin. Use Python.
- Hard real-time or embedded — Go has a GC with sub-millisecond pauses (modern GOGC), but pauses are not zero. For hard real-time (medical devices, motor control), use C, Rust, or Ada.
- Error handling fatigue — Go's explicit
if err != nilpattern scales well but is verbose. Rust's?operator and TypeScript's union types are terser for error-heavy code.
Go vs. its closest alternatives
- Go vs Rust — Rust is faster (no GC) and memory-safe at compile time. Go is far easier to learn and faster to ship. Go for services; Rust for systems programming and maximum efficiency.
- Go vs Python — Go is 10–50× faster, statically typed, and compiles to a single binary. Python is more expressive and has a much larger ecosystem. Go for production services; Python for scripting, ML, and rapid prototyping.
- Go vs Java/C# — Go has faster startup, a smaller binary, and simpler concurrency. Java and C# have larger ecosystems, richer type systems, and decades of enterprise library support. Go for new cloud services; Java/C# for enterprise or heavily framework-dependent applications.
Resources
- go.dev — official Go website
- Go documentation — language spec, standard library, and guides
- A Tour of Go — interactive introduction to the language
- Effective Go — idiomatic Go patterns from the team
- Go by Example — hands-on introduction with annotated examples
- pkg.go.dev — documentation for all Go packages
Topics
Variables & Constants
go
package main
// var declaration — explicit type
var name string = 'Alice'
var age int = 30
// var block
var (
host string = 'localhost'
port int = 8080
)
// Short variable declaration (:=) — inside functions only
func main() {
x := 42 // int inferred
y := 3.14 // float64 inferred
ok := true // bool
s := 'hello' // string
// Multiple assignment
a, b := 1, 2
a, b = b, a // swap
// Blank identifier — discard values
val, _ := strconv.Atoi('42')
// Zero values (default when declared without initializer)
var i int // 0
var f float64 // 0.0
var b2 bool // false
var str string // ''
var p *int // nil
_ = i; _ = f; _ = b2; _ = str; _ = p; _ = val
}
// Constants
const Pi = 3.14159
const Msg = 'hello'
// Typed constants
const MaxSize int = 1024
// iota — auto-incrementing constant generator
type Direction int
const (
North Direction = iota // 0
East // 1
South // 2
West // 3
)
type ByteSize float64
const (
_ = iota // ignore first value
KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)Types & Aliases
go
package main
import 'fmt'
// Basic types
// bool, string
// int int8 int16 int32 int64
// uint uint8 uint16 uint32 uint64 uintptr
// byte (alias for uint8), rune (alias for int32)
// float32 float64
// complex64 complex128
// Type alias — same underlying type, different name
type Celsius float64
type Fahrenheit float64
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// Type definition — new distinct type
type UserID int64
type Email string
// Explicit type conversion (no implicit coercion)
var i int = 42
var f float64 = float64(i) // must convert
var u uint = uint(f)
_ = u
// Type assertions — extract concrete type from interface
var val interface{} = 'hello'
s, ok := val.(string) // safe assertion; ok = true
if ok {
fmt.Println(s)
}
// Type switch
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf('int: %d', v)
case string:
return fmt.Sprintf('string: %s', v)
case bool:
return fmt.Sprintf('bool: %t', v)
default:
return fmt.Sprintf('unknown: %T', v)
}
}Control Flow
go
package main
import 'fmt'
func control() {
// if — no parens required
x := 10
if x > 5 {
fmt.Println('big')
} else if x == 5 {
fmt.Println('five')
} else {
fmt.Println('small')
}
// if with init statement — scopes variable to block
if n, err := fmt.Println('hi'); err != nil {
_ = n
}
// for — the only loop keyword in Go
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// while-style
n := 0
for n < 3 {
n++
}
// infinite loop
// for { }
// range — iterate slices, maps, strings, channels
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf('index=%d val=%d\n', i, v)
}
for _, v := range nums { _ = v } // skip index
for i := range nums { _ = i } // skip value
m := map[string]int{'a': 1, 'b': 2}
for k, v := range m {
fmt.Printf('%s=%d\n', k, v)
}
// range over string yields runes
for i, r := range 'héllo' {
fmt.Printf('%d: %c\n', i, r)
}
// switch — no fallthrough by default
day := 'Mon'
switch day {
case 'Mon', 'Tue', 'Wed', 'Thu', 'Fri':
fmt.Println('weekday')
case 'Sat', 'Sun':
fmt.Println('weekend')
default:
fmt.Println('unknown')
}
// switch with no expression — replaces if-else chains
score := 75
switch {
case score >= 90:
fmt.Println('A')
case score >= 70:
fmt.Println('B')
default:
fmt.Println('C')
}
// defer — runs when surrounding function returns (LIFO order)
defer fmt.Println('world')
fmt.Println('hello')
// defer with loop — capture value immediately
for i := 0; i < 3; i++ {
i := i // shadow to capture current value
defer fmt.Println(i)
}
// break / continue with labels
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer
}
}
}
}Functions
go
package main
import (
'errors'
'fmt'
)
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New('division by zero')
}
return a / b, nil
}
// Named return values — usable but avoid for clarity
func minmax(nums []int) (min, max int) {
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min { min = n }
if n > max { max = n }
}
return // bare return uses named values
}
// Variadic function — last param can receive zero or more values
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Spread a slice into variadic param
func main() {
fmt.Println(sum(1, 2, 3))
nums := []int{4, 5, 6}
fmt.Println(sum(nums...)) // spread
// First-class functions
double := func(n int) int { return n * 2 }
fmt.Println(double(5))
// Closure — captures variables from enclosing scope
counter := func() func() int {
n := 0
return func() int {
n++
return n
}
}()
fmt.Println(counter(), counter()) // 1 2
// Functions as arguments
apply := func(f func(int) int, x int) int { return f(x) }
fmt.Println(apply(double, 7))
// Immediately invoked function expression
result := func(x int) int { return x * x }(9)
fmt.Println(result)
}
// panic / recover
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf('recovered: %v', r)
}
}()
if b == 0 {
panic('divide by zero')
}
return a / b, nil
}Pointers
go
package main
import 'fmt'
func increment(p *int) {
*p++ // dereference and mutate
}
func newInt(n int) *int {
return &n // safe: Go variables escape to heap when needed
}
func main() {
x := 42
p := &x // p is *int, holds address of x
fmt.Println(*p) // dereference: prints 42
*p = 100
fmt.Println(x) // x is now 100
increment(&x)
fmt.Println(x) // 101
// new() — allocates zeroed value, returns pointer
q := new(int) // *int pointing to 0
*q = 7
fmt.Println(*q)
// nil pointer — zero value for pointer types
var ptr *string
fmt.Println(ptr == nil) // true
// Pointer to struct — Go auto-dereferences for fields
type Point struct{ X, Y int }
pt := &Point{1, 2}
pt.X = 10 // equivalent to (*pt).X = 10
fmt.Println(*pt)
// Pointer comparison
a := 1
b := 1
pa, pb := &a, &b
fmt.Println(pa == pb) // false — different addresses
fmt.Println(*pa == *pb) // true — same value
}Structs & Methods
go
package main
import (
'fmt'
'math'
)
// Struct definition
type Point struct {
X, Y float64
}
// Methods — defined outside struct with a receiver
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// Pointer receiver — can mutate, preferred when struct is large
func (p *Point) Scale(factor float64) {
p.X *= factor
p.Y *= factor
}
// Struct literals
func main() {
p1 := Point{X: 3, Y: 4}
p2 := Point{3, 4} // positional (fragile, avoid for exported)
p3 := Point{} // zero value: {0, 0}
_ = p2; _ = p3
fmt.Println(p1.Distance())
p1.Scale(2)
fmt.Println(p1)
// Anonymous struct
person := struct {
Name string
Age int
}{'Bob', 30}
fmt.Println(person)
}
// Struct embedding — promotes methods and fields
type Animal struct {
Name string
}
func (a Animal) Speak() string { return a.Name + ' makes a sound' }
type Dog struct {
Animal // embedded (not named)
Breed string
}
func (d Dog) Speak() string { return d.Name + ' barks' } // override
// Composition with explicit delegation
type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Printf('[%s] %s\n', l.prefix, msg) }
type Server struct {
Logger // promoted: server.Log() works
host string
}
// Struct tags — used by encoding/json, database/sql, etc.
type User struct {
ID int `json:'id' db:'user_id'`
Name string `json:'name' db:'user_name'`
Email string `json:'email' db:'-'`
}Interfaces
go
package main
import (
'fmt'
'math'
)
// Interface — defined by method set
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
type Rect struct{ Width, Height float64 }
// Implicit implementation — no 'implements' keyword
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (r Rect) Area() float64 { return r.Width * r.Height }
func (r Rect) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func printShape(s Shape) {
fmt.Printf('area=%.2f perim=%.2f\n', s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{
Circle{5},
Rect{3, 4},
}
for _, s := range shapes {
printShape(s)
}
// Interface variable holds (value, type) pair
var s Shape = Circle{1}
fmt.Printf('%T %v\n', s, s)
// Empty interface — accepts any value (use sparingly)
var any interface{} = 42
any = 'now a string'
_ = any
// any alias (Go 1.18+)
var v any = true
_ = v
// Interface embedding
type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }
type ReadWriter interface {
Reader
Writer
}
// Stringer interface from fmt package
// implementing String() string makes fmt.Println use it
}
// Nil interface pitfall
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func bad() error {
var e *MyError = nil
return e // returns non-nil interface holding nil *MyError!
}
func good() error {
return nil // truly nil interface
}Goroutines
go
package main
import (
'fmt'
'sync'
'time'
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signals completion
fmt.Printf('worker %d starting\n', id)
time.Sleep(50 * time.Millisecond)
fmt.Printf('worker %d done\n', id)
}
func main() {
// go keyword launches goroutine — lightweight, ~2 KB stack
go func() {
fmt.Println('async')
}()
// WaitGroup — wait for multiple goroutines
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // increment before launching
go worker(i, &wg) // pass pointer to wg
}
wg.Wait() // block until all Done()
fmt.Println('all workers done')
// Goroutines share address space — need synchronization
// for shared data (use channels or sync primitives)
// Goroutine leak — avoid launching without a way to stop
// Always ensure goroutines can exit (via channel/context)
// GOMAXPROCS — number of OS threads (default = NumCPU)
// runtime.GOMAXPROCS(4)
}Channels
go
package main
import (
'fmt'
'time'
)
func main() {
// Unbuffered channel — send blocks until receive (synchronous)
ch := make(chan int)
go func() {
ch <- 42 // send
}()
v := <-ch // receive
fmt.Println(v)
// Buffered channel — send blocks only when buffer full
bch := make(chan string, 3)
bch <- 'a'
bch <- 'b'
bch <- 'c'
fmt.Println(<-bch, <-bch, <-bch)
// Close channel — signals no more values
nums := make(chan int, 5)
for i := 0; i < 5; i++ { nums <- i }
close(nums)
// Range over closed channel — drains then stops
for n := range nums {
fmt.Println(n)
}
// Check if channel closed
ch2 := make(chan int, 1)
ch2 <- 7
close(ch2)
v2, ok := <-ch2
fmt.Printf('v=%d ok=%t\n', v2, ok) // v=7 ok=true
v3, ok := <-ch2
fmt.Printf('v=%d ok=%t\n', v3, ok) // v=0 ok=false
// select — multiplex channel operations
c1 := make(chan string, 1)
c2 := make(chan string, 1)
c1 <- 'one'
c2 <- 'two'
select {
case msg := <-c1:
fmt.Println('c1:', msg)
case msg := <-c2:
fmt.Println('c2:', msg)
}
// select with default — non-blocking
ch3 := make(chan int)
select {
case v := <-ch3:
fmt.Println('got', v)
default:
fmt.Println('no value ready')
}
// Timeout pattern
result := make(chan int, 1)
go func() {
time.Sleep(100 * time.Millisecond)
result <- 1
}()
select {
case r := <-result:
fmt.Println('result:', r)
case <-time.After(50 * time.Millisecond):
fmt.Println('timed out')
}
// Directional channel types — enforce ownership
// chan<- int send-only
// <-chan int receive-only
ping := func(ch chan<- string) { ch <- 'ping' }
pong := func(ch <-chan string) { fmt.Println(<-ch) }
c := make(chan string, 1)
ping(c)
pong(c)
}Error Handling
go
package main
import (
'errors'
'fmt'
)
// error is a built-in interface: Error() string
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New('cannot divide by zero')
}
return a / b, nil
}
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf('validation error: %s — %s', e.Field, e.Message)
}
// fmt.Errorf with %w — wraps an error
func process(id int) error {
if id < 0 {
return fmt.Errorf('process: invalid id %d: %w', id, &ValidationError{
Field: 'id', Message: 'must be non-negative',
})
}
return nil
}
// errors.Is — checks error chain by value
// errors.As — checks error chain by type
func main() {
err := process(-1)
if err != nil {
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf('field=%s msg=%s\n', ve.Field, ve.Message)
}
}
// Sentinel errors — compare with errors.Is
var ErrNotFound = errors.New('not found')
wrapped := fmt.Errorf('lookup: %w', ErrNotFound)
fmt.Println(errors.Is(wrapped, ErrNotFound)) // true
// errors.Join (Go 1.20+) — combine multiple errors
e1 := errors.New('first')
e2 := errors.New('second')
combined := errors.Join(e1, e2)
fmt.Println(combined)
fmt.Println(errors.Is(combined, e1)) // true
// Common pattern: check then use
result, err2 := divide(10, 2)
if err2 != nil {
fmt.Println('error:', err2)
return
}
fmt.Println(result)
}Slices
go
package main
import 'fmt'
func main() {
// Slice literal
s := []int{1, 2, 3, 4, 5}
// make([]T, len, cap) — allocate with length and capacity
s2 := make([]int, 3) // len=3, cap=3, all zeros
s3 := make([]int, 3, 10) // len=3, cap=10
_ = s2; _ = s3
// append — may allocate new backing array
s = append(s, 6, 7)
other := []int{8, 9}
s = append(s, other...) // spread slice
// Slicing — shares backing array
a := s[1:4] // elements at index 1, 2, 3
b := s[:3] // from start
c := s[3:] // to end
d := s[:] // whole slice
_ = a; _ = b; _ = c; _ = d
// 3-index slice — limits capacity of result
e := s[1:4:5] // [1:4] with cap limited to 5-1=4
_ = e
// copy — copies min(len(dst), len(src)) elements
dst := make([]int, 3)
n := copy(dst, s)
fmt.Println(dst, n)
// nil slice vs empty slice
var nilSlice []int // nil, len=0, cap=0
emptySlice := []int{} // not nil, len=0, cap=0
emptySlice2 := make([]int, 0)
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
_ = emptySlice2
// Iterate
for i, v := range s {
_ = i; _ = v
}
// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, 3)
}
matrix[1][1] = 9
fmt.Println(matrix)
// Delete element at index i (preserve order)
i := 2
s = append(s[:i], s[i+1:]...)
// Delete (fast, unordered — swaps with last)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Insert at index i
s = append(s[:i+1], s[i:]...)
s[i] = 99
fmt.Println(s)
}Maps
go
package main
import 'fmt'
func main() {
// Map literal
m := map[string]int{
'alice': 90,
'bob': 85,
}
// make(map[K]V) — allocate empty map
scores := make(map[string]int)
// Create / Update
scores['carol'] = 95
scores['carol'] = 97 // overwrite
// Read
v := scores['carol'] // 97
fmt.Println(v)
// Existence check — always use two-value form
val, ok := scores['dave']
if !ok {
fmt.Println('dave not found')
}
_ = val
// Delete
delete(scores, 'carol')
// Iterate (order is randomized each run)
for k, v := range m {
fmt.Printf('%s: %d\n', k, v)
}
// Keys only
for k := range m { _ = k }
// nil map — reads return zero, writes panic
var nilMap map[string]int
fmt.Println(nilMap['x']) // 0 — safe read
// nilMap['x'] = 1 // panic: assignment to nil map
// Map of slices pattern
groups := make(map[string][]string)
groups['admin'] = append(groups['admin'], 'alice')
groups['admin'] = append(groups['admin'], 'bob')
fmt.Println(groups)
// Count occurrences
words := []string{'go', 'is', 'great', 'go', 'is', 'go'}
freq := make(map[string]int)
for _, w := range words {
freq[w]++
}
fmt.Println(freq)
// Set pattern (map to struct{} uses no memory for value)
seen := make(map[string]struct{})
seen['go'] = struct{}{}
_, exists := seen['go']
fmt.Println(exists)
}Strings & Runes
go
package main
import (
'fmt'
'strings'
'unicode/utf8'
'bytes'
)
func main() {
s := 'Hello, 世界'
// len returns byte count, not rune count
fmt.Println(len(s)) // 13 (UTF-8 bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9
// Byte indexing
fmt.Println(s[0]) // 72 (byte value of 'H')
fmt.Printf('%c\n', s[0]) // H
// Rune (codepoint) iteration
for i, r := range s {
fmt.Printf('%d: %c (%d bytes)\n', i, r, utf8.RuneLen(r))
}
// Convert to rune slice to index by character
runes := []rune(s)
fmt.Println(string(runes[7])) // 世
// strings package — common operations
fmt.Println(strings.ToUpper(s))
fmt.Println(strings.ToLower(s))
fmt.Println(strings.Contains(s, 'World'))
fmt.Println(strings.HasPrefix(s, 'Hello'))
fmt.Println(strings.HasSuffix(s, '界'))
fmt.Println(strings.Index(s, ','))
fmt.Println(strings.Count(s, 'l'))
fmt.Println(strings.Replace(s, 'Hello', 'Hi', 1))
fmt.Println(strings.ReplaceAll(s, 'l', 'L'))
fmt.Println(strings.TrimSpace(' hi '))
fmt.Println(strings.Trim('--go--', '-'))
fmt.Println(strings.Split('a,b,c', ','))
fmt.Println(strings.Join([]string{'x', 'y', 'z'}, '-'))
fmt.Println(strings.Fields(' foo bar baz ')) // split on whitespace
// strings.Builder — efficient string concatenation
var sb strings.Builder
for i := 0; i < 5; i++ {
fmt.Fprintf(&sb, 'item%d ', i)
}
fmt.Println(sb.String())
// bytes.Buffer — works with []byte
var buf bytes.Buffer
buf.WriteString('hello')
buf.WriteByte(' ')
buf.WriteString('world')
fmt.Println(buf.String())
// String <-> []byte conversion
b := []byte(s)
s2 := string(b)
_ = s2
// fmt.Sprintf — string formatting
formatted := fmt.Sprintf('name=%s age=%d pi=%.2f', 'Alice', 30, 3.14159)
fmt.Println(formatted)
}Packages & Imports
go
// Package declaration must match directory name
package mathutil
import (
'fmt' // standard library
'math' // standard library
'github.com/pkg/errors' // third-party (go.sum tracks hash)
myhttp 'myproject/internal/http' // aliased import
_ 'myproject/side-effects' // blank import (init() only)
. 'myproject/dot-import' // dot import (adds names to scope, avoid)
)
// Exported names start with uppercase
const MaxRetries = 3
// Unexported — package-private
var defaultTimeout = 30
// init() — runs once at program start, after var init
// Multiple init() allowed per file; runs in source order
func init() {
fmt.Println('mathutil initialized')
_ = math.Pi
_ = errors.New
_ = myhttp.Client{}
_ = defaultTimeout
}
// Exported function
func Abs(x float64) float64 {
if x < 0 { return -x }
return x
}
// Internal package — can only be imported by parent subtree
// e.g. myproject/internal/http can only be imported by myproject/...Modules
go
# go.mod — module definition file (at repo root)
# Created with: go mod init <module-path>
module github.com/alice/myapp
go 1.21
require (
github.com/pkg/errors v0.9.1
golang.org/x/text v0.14.0
)
require (
// indirect dependencies
golang.org/x/sys v0.15.0 // indirect
)
# Common go commands
# go mod init github.com/alice/myapp — initialize module
# go get github.com/pkg/errors — add dependency
# go get github.com/pkg/errors@v0.9.1 — pin version
# go get github.com/pkg/errors@latest — update to latest
# go mod tidy — remove unused, add missing
# go mod download — download to cache
# go mod vendor — copy deps to vendor/
# go list -m all — list all dependencies
# go mod graph — show dependency graph
# go.sum — cryptographic checksums (commit to VCS)
# Workspace mode (Go 1.18+) — multi-module development
# go work init ./moduleA ./moduleB
# go work use ./moduleC
# go work sync — sync workspace deps
# Build constraints / tags
//go:build linux && amd64
// +build linux,amd64 (old syntax, before Go 1.17)
# Cross-compilation
# GOOS=linux GOARCH=amd64 go build -o app-linux .
# GOOS=windows go build -o app.exe .Generics (Go 1.18+)
go
package main
import (
'cmp'
'fmt'
)
// Type parameter with constraint
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func Filter[T any](s []T, f func(T) bool) []T {
var result []T
for _, v := range s {
if f(v) { result = append(result, v) }
}
return result
}
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// comparable — supports == and !=
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target { return true }
}
return false
}
// cmp.Ordered — int, float, string (Go 1.21+)
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
// Custom constraint — union of types
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// Generic struct
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 { return zero, false }
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
// Multiple type parameters
type Pair[A, B any] struct { First A; Second B }
func NewPair[A, B any](a A, b B) Pair[A, B] { return Pair[A, B]{a, b} }
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubled)
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens)
total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(total)
fmt.Println(Contains(nums, 3))
fmt.Println(Min(10, 5))
fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))
var st Stack[string]
st.Push('go')
st.Push('generics')
v, ok := st.Pop()
fmt.Println(v, ok)
}Testing
go
package calc_test
import (
'testing'
'time'
)
// Test function — must start with Test, takes *testing.T
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf('Add(2,3) = %d; want %d', got, want)
}
}
// t.Fatal — stops the current test immediately
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf('unexpected error: %v', err)
}
if result != 5 {
t.Errorf('got %v, want 5', result)
}
}
// Table-driven tests — idiomatic Go
func TestMultiply(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{'positive', 3, 4, 12},
{'zero', 0, 5, 0},
{'negative', -2, 3, -6},
}
for _, tc := range tests {
tc := tc // capture range variable (pre-Go 1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // run subtests in parallel
got := Multiply(tc.a, tc.b)
if got != tc.want {
t.Errorf('Multiply(%d,%d) = %d; want %d', tc.a, tc.b, got, tc.want)
}
})
}
}
// TestMain — setup/teardown for entire test binary
func TestMain(m *testing.M) {
// setup
code := m.Run()
// teardown
_ = code
// os.Exit(code)
}
// Benchmark — must start with Benchmark, takes *testing.B
// Run with: go test -bench=. -benchmem
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(100, 200)
}
}
// Benchmark with reset timer
func BenchmarkExpensive(b *testing.B) {
data := setup() // expensive setup
b.ResetTimer() // exclude setup from timing
for i := 0; i < b.N; i++ {
process(data)
}
}
// Example function — tested by go test, shown in godoc
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output:
// 3
}
// go test flags:
// go test ./... — run all tests recursively
// go test -v — verbose output
// go test -run TestAdd — run specific test
// go test -run TestMultiply/zero — run specific subtest
// go test -count=1 — disable caching
// go test -race — race detector
// go test -cover — coverage report
// go test -coverprofile=c.out && go tool cover -html=c.out
func setup() []int { return []int{1, 2, 3} }
func process([]int) {}
import 'fmt'Context
go
package main
import (
'context'
'fmt'
'time'
)
func doWork(ctx context.Context) error {
select {
case <-time.After(200 * time.Millisecond):
return nil // work done
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded or context.Canceled
}
}
func main() {
// Background — root context, never cancelled
bg := context.Background()
// TODO — placeholder, like Background but signals intent
// ctx := context.TODO()
// WithCancel — manual cancellation
ctx, cancel := context.WithCancel(bg)
defer cancel() // always defer cancel to avoid leak
go func() {
time.Sleep(100 * time.Millisecond)
cancel() // cancel the context
}()
err := doWork(ctx)
fmt.Println('WithCancel:', err) // context canceled
// WithTimeout — auto-cancel after duration
ctx2, cancel2 := context.WithTimeout(bg, 50*time.Millisecond)
defer cancel2()
err = doWork(ctx2)
fmt.Println('WithTimeout:', err) // context deadline exceeded
// WithDeadline — cancel at specific time
deadline := time.Now().Add(100 * time.Millisecond)
ctx3, cancel3 := context.WithDeadline(bg, deadline)
defer cancel3()
fmt.Println('deadline:', ctx3.Deadline())
// WithValue — pass request-scoped values (not for optional params)
type keyType string
const userKey keyType = 'userID'
ctx4 := context.WithValue(bg, userKey, 42)
if uid, ok := ctx4.Value(userKey).(int); ok {
fmt.Println('userID:', uid)
}
// Propagate context through call chain
_ = ctx4
// Check cancellation without blocking
select {
case <-ctx.Done():
fmt.Println('cancelled')
default:
fmt.Println('still running')
}
}I/O
go
package main
import (
'bufio'
'fmt'
'io'
'os'
'path/filepath'
'strings'
)
func main() {
// os.Open — read-only
f, err := os.Open('input.txt')
if err != nil {
fmt.Println(err)
return
}
defer f.Close() // always close
// Read entire file
data, err := os.ReadFile('input.txt')
if err == nil {
fmt.Println(string(data))
}
// Write entire file
err = os.WriteFile('output.txt', []byte('hello\n'), 0644)
if err != nil { fmt.Println(err) }
// Buffered reader — efficient line-by-line reading
f2, _ := os.Open('input.txt')
defer f2.Close()
scanner := bufio.NewScanner(f2)
for scanner.Scan() {
line := scanner.Text()
_ = line
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
// bufio.Writer — buffered writes
f3, _ := os.Create('output.txt')
defer f3.Close()
w := bufio.NewWriter(f3)
fmt.Fprintln(w, 'buffered write')
w.Flush() // must flush to write remaining buffer
// io.Reader / io.Writer — core interfaces
r := strings.NewReader('hello world')
buf := make([]byte, 4)
for {
n, err := r.Read(buf)
if err == io.EOF { break }
fmt.Print(string(buf[:n]))
}
fmt.Println()
// io.Copy — zero-alloc stream copy
src := strings.NewReader('copy me')
var dst strings.Builder
io.Copy(&dst, src)
fmt.Println(dst.String())
// filepath — cross-platform path operations
p := filepath.Join('dir', 'sub', 'file.txt')
fmt.Println(p)
fmt.Println(filepath.Dir(p))
fmt.Println(filepath.Base(p))
fmt.Println(filepath.Ext(p))
// Walk directory tree
filepath.WalkDir('.', func(path string, d os.DirEntry, err error) error {
if err != nil { return err }
fmt.Println(path, d.IsDir())
return nil
})
// os.Stdin, os.Stdout, os.Stderr
fmt.Fprintln(os.Stderr, 'error output')
}Sync Primitives
go
package main
import (
'fmt'
'sync'
'sync/atomic'
)
// Mutex — mutual exclusion for shared state
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
// RWMutex — multiple concurrent readers, one writer
type SafeMap struct {
mu sync.RWMutex
m map[string]string
}
func (s *SafeMap) Get(k string) string {
s.mu.RLock() // shared read lock
defer s.mu.RUnlock()
return s.m[k]
}
func (s *SafeMap) Set(k, v string) {
s.mu.Lock() // exclusive write lock
defer s.mu.Unlock()
s.m[k] = v
}
// sync.Once — run function exactly once
var (
instance *SafeMap
once sync.Once
)
func getInstance() *SafeMap {
once.Do(func() {
instance = &SafeMap{m: make(map[string]string)}
})
return instance
}
// sync.WaitGroup — wait for goroutines
func waitGroupDemo() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
}
// sync/atomic — lock-free operations on primitive types
type AtomicCounter struct {
count int64
}
func (c *AtomicCounter) Inc() { atomic.AddInt64(&c.count, 1) }
func (c *AtomicCounter) Get() int64 { return atomic.LoadInt64(&c.count) }
func (c *AtomicCounter) CAS(old, new int64) bool {
return atomic.CompareAndSwapInt64(&c.count, old, new)
}
// sync.Map — concurrent-safe map (use for high read-to-write ratio)
func syncMapDemo() {
var m sync.Map
m.Store('key', 'value')
v, ok := m.Load('key')
fmt.Println(v, ok)
m.Delete('key')
m.Range(func(k, v interface{}) bool {
fmt.Println(k, v)
return true // return false to stop iteration
})
}
// sync.Pool — reuse allocated objects to reduce GC pressure
var bufPool = sync.Pool{
New: func() interface{} { return make([]byte, 1024) },
}
func getBuffer() []byte { return bufPool.Get().([]byte) }
func putBuffer(b []byte) { bufPool.Put(b) }Reflection
go
package main
import (
'fmt'
'reflect'
)
type Person struct {
Name string `json:'name'`
Age int `json:'age'`
}
func main() {
p := Person{'Alice', 30}
// reflect.TypeOf — get the type
t := reflect.TypeOf(p)
fmt.Println(t.Name()) // Person
fmt.Println(t.Kind()) // struct
fmt.Println(t.NumField()) // 2
// Iterate struct fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf('field=%s type=%s tag=%s\n',
field.Name, field.Type, field.Tag.Get('json'))
}
// reflect.ValueOf — get the value
v := reflect.ValueOf(p)
fmt.Println(v.Field(0).String()) // Alice
fmt.Println(v.Field(1).Int()) // 30
// Modify via pointer
pp := &p
vp := reflect.ValueOf(pp).Elem() // dereference
vp.Field(0).SetString('Bob')
fmt.Println(p.Name) // Bob
// Check if a value implements an interface
var err error
errType := reflect.TypeOf(&err).Elem() // reflect.Type of error
fmt.Println(t.Implements(errType)) // false for Person
// Create new values dynamically
newT := reflect.New(t).Elem()
newT.Field(0).SetString('Carol')
newT.Field(1).SetInt(25)
fmt.Println(newT.Interface().(Person))
// Call methods via reflection
m := reflect.ValueOf(p).MethodByName('String')
if m.IsValid() {
results := m.Call(nil)
_ = results
}
// Slice/map reflection
nums := []int{1, 2, 3}
rv := reflect.ValueOf(nums)
fmt.Println(rv.Kind(), rv.Len())
for i := 0; i < rv.Len(); i++ {
fmt.Println(rv.Index(i).Int())
}
// reflect.DeepEqual — structural equality
a := []int{1, 2, 3}
b := []int{1, 2, 3}
fmt.Println(reflect.DeepEqual(a, b)) // true
}CGo
go
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// C function defined inline
int add(int a, int b) {
return a + b;
}
char* greet(const char* name) {
char* buf = malloc(64);
snprintf(buf, 64, "Hello, %s!", name);
return buf;
}
*/
import 'C' // must be immediately after the comment block
import (
'fmt'
'unsafe'
)
func main() {
// Call C function
result := C.add(3, 4)
fmt.Println(int(result)) // 7
// Pass Go string to C
name := C.CString('Go') // allocates C string — must free!
defer C.free(unsafe.Pointer(name))
greeting := C.greet(name)
defer C.free(unsafe.Pointer(greeting))
fmt.Println(C.GoString(greeting)) // Hello, Go!
// C types
var n C.int = 42
var f C.double = 3.14
_ = n; _ = f
// Convert Go []byte to C pointer
data := []byte{'h', 'e', 'l', 'l', 'o'}
ptr := (*C.char)(unsafe.Pointer(&data[0]))
_ = ptr
// CGo rules:
// - Do NOT pass Go pointer to C if it contains Go pointers
// - C memory must be freed with C.free (not Go GC)
// - CGo calls have ~200ns overhead vs ~1ns for Go calls
// - Build: go build (CGo enabled by default)
// - Disable: CGO_ENABLED=0 go build (pure Go, static binary)
}
// Export Go function to C with //export directive
//
//export GoAdd
func GoAdd(a, b C.int) C.int {
return a + b
}Best Practices
Go Idioms
go
// ✅ Accept interfaces, return concrete types
// Maximizes caller flexibility
func NewReader(r io.Reader) *MyReader { ... } // accept interface
func Open(path string) (*File, error) { ... } // return concrete
// ✅ Errors are values — handle explicitly, not via exceptions
result, err := doSomething()
if err != nil {
return fmt.Errorf('doSomething: %w', err)
}
// ✅ Use defer for cleanup — always close what you open
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
// ✅ Named single-return values only for documentation or naked returns
// Avoid naked returns in long functions — reduces readability
// ✅ Zero value should be useful
type Buffer struct {
buf []byte // nil slice is valid, append works on nil
}
var b Buffer // usable without constructor
// ✅ Prefer composition over inheritance
type Logger struct{ prefix string }
type Server struct {
Logger
port int
}
// ✅ Short variable names for small scopes
for i, v := range items { ... } // i, v are idiomatic
// Longer names for larger scopes
userRepository := NewUserRepository(db)
// ✅ Don't panic in library code — return errors
// panic is for truly unrecoverable programmer errors
// ✅ Use gofmt / goimports — always format code
// go fmt ./...
// goimports -w .
// ✅ Comment exported identifiers with godoc style
// MyFunc does X. It returns Y when Z.
func MyFunc() {}
// ✅ Functional options pattern
type ServerConfig struct {
port int
timeout time.Duration
}
type Option func(*ServerConfig)
func WithPort(p int) Option { return func(c *ServerConfig) { c.port = p } }
func WithTimeout(d time.Duration) Option { return func(c *ServerConfig) { c.timeout = d } }
func NewServer(opts ...Option) *Server {
cfg := &ServerConfig{port: 8080, timeout: 30 * time.Second}
for _, opt := range opts {
opt(cfg)
}
return &Server{config: cfg}
}Error Handling
go
// ✅ Add context to errors as they propagate
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf('readConfig: %w', err) // wrap with %w
}
...
}
// ✅ Sentinel errors for expected conditions
var (
ErrNotFound = errors.New('not found')
ErrForbidden = errors.New('forbidden')
)
// ✅ Custom error types when callers need to inspect
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf('%s %d not found', e.Resource, e.ID)
}
// ✅ errors.Is / errors.As for matching wrapped errors
if errors.Is(err, ErrNotFound) { ... }
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Println(nfe.Resource, nfe.ID)
}
// ✅ Return early on error — avoid else after return
func process(id int) (string, error) {
if id <= 0 {
return '', ErrNotFound
}
// ... no else needed
return doWork(id)
}
// ✅ Never discard errors silently
// ❌ os.Remove(tmpFile)
// ✅ if err := os.Remove(tmpFile); err != nil { log.Println(err) }
// ✅ panic only for programmer errors (index OOB, nil deref logic)
// Recover from panic in HTTP handlers / goroutine entry points
func safeHandler(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, 'internal error', 500)
}
}()
h(w, r)
}
}Concurrency
go
// ✅ Share memory by communicating, don't communicate by sharing memory
// Prefer channels over mutexes for ownership transfer
// ✅ Document goroutine ownership — who starts, who stops, how it exits
// Every goroutine must have a clear termination path
// ✅ Use context for cancellation — propagate down the call tree
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}
// ✅ Use WaitGroup correctly: Add before launching goroutine
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1) // ✅ before go
go func(v Item) {
defer wg.Done()
process(v)
}(item) // ✅ pass loop variable as argument
}
wg.Wait()
// ✅ Pipeline pattern — compose goroutines with channels
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums { out <- n }
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in { out <- n * n }
}()
return out
}
// ✅ Fan-out / fan-in for parallel work
// ✅ Use errgroup for goroutines that return errors
// golang.org/x/sync/errgroup
g, ctx := errgroup.WithContext(context.Background())
for _, url := range urls {
url := url
g.Go(func() error { return fetch(ctx, url) })
}
if err := g.Wait(); err != nil { ... }
// ✅ Always run tests with -race flag
// go test -race ./...Performance
go
// ✅ Profile before optimizing
// go test -cpuprofile=cpu.out -memprofile=mem.out -bench=.
// go tool pprof cpu.out
// ✅ Preallocate slices and maps when size is known
items := make([]Item, 0, expectedCount) // avoids reallocations
cache := make(map[string]Value, 1000)
// ✅ Use strings.Builder for string concatenation in loops
var sb strings.Builder
sb.Grow(estimatedLen) // preallocate
for _, s := range strs {
sb.WriteString(s)
}
// ✅ Reuse allocations with sync.Pool
var pool = sync.Pool{New: func() interface{} { return new(MyStruct) }}
obj := pool.Get().(*MyStruct)
defer pool.Put(obj)
// ✅ Avoid allocations in hot paths
// Value receivers for small structs (copy cheaper than pointer chase)
// Pointer receivers for large structs or when mutation needed
// ✅ Use byte slices instead of strings for I/O
buf := make([]byte, 4096)
n, _ := r.Read(buf)
process(buf[:n]) // no allocation
// ✅ Minimize goroutine creation overhead — use worker pools
type WorkerPool struct {
jobs chan Job
wg sync.WaitGroup
}
func (p *WorkerPool) Start(n int) {
for i := 0; i < n; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for job := range p.jobs { job.Do() }
}()
}
}
// ✅ Use GOMAXPROCS = NumCPU (default since Go 1.5)
// ✅ Avoid excessive CGo calls — 200ns overhead per call
// ✅ Use benchmarks to measure: go test -bench=. -benchmem -count=5Packages & Modules
go
// ✅ Package names: lowercase, single word, no underscores
package httputil // ✅
package http_util // ❌
package HTTPUtil // ❌
// ✅ Package should have one clear purpose
// ❌ package util / helpers / common (too broad)
// ✅ package strconv / filepath / httputil (specific)
// ✅ Use internal/ for implementation details
// myapp/internal/auth — not importable by external modules
// myapp/internal/db — forces clean API boundaries
// ✅ Avoid init() side effects in libraries
// ❌ func init() { http.HandleFunc(...) }
// ✅ Require explicit initialization by the caller
// ✅ Version your public APIs carefully
// Use Go module versions for breaking changes:
// module github.com/alice/mylib/v2
// ✅ go mod tidy in CI — catch missing/extra dependencies
// ✅ Pin indirect deps in go.sum — reproducible builds
// ✅ Use go get -u=patch for safe updates (patch versions only)
// ✅ Minimal public API surface — easier to extend than shrink
// Start unexported, export when external need is proven
// ✅ Separate commands from libraries
// cmd/myapp/main.go — entrypoint (thin, delegates to pkg)
// pkg/server/server.go — library (testable, importable)
// ✅ Use //go:generate for code generation
//go:generate stringer -type=Direction
//go:generate mockgen -source=iface.go -destination=mock.goTesting
go
// ✅ Table-driven tests are idiomatic Go
func TestAdd(t *testing.T) {
cases := []struct{ a, b, want int }{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tc := range cases {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf('Add(%d,%d) = %d; want %d', tc.a, tc.b, got, tc.want)
}
}
}
// ✅ Use t.Helper() in test utilities to point to the call site
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf('got %d, want %d', got, want)
}
}
// ✅ t.Parallel() for independent tests — faster CI
func TestSomething(t *testing.T) {
t.Parallel()
...
}
// ✅ testdata/ directory — test fixtures, Go tooling ignores it
// testdata/input.json, testdata/golden/expected.txt
// ✅ golden file testing — compare output to checked-in file
// Update with: go test -update-golden ./...
// ✅ Use interfaces + fakes over heavy mocking frameworks
type Store interface { Get(id int) (*User, error) }
type FakeStore struct { users map[int]*User }
func (f *FakeStore) Get(id int) (*User, error) {
u, ok := f.users[id]
if !ok { return nil, ErrNotFound }
return u, nil
}
// ✅ Test package naming
// package calc — white-box tests (access unexported)
// package calc_test — black-box tests (only exported API)
// ✅ Use -race in CI
// go test -race ./...
// ✅ Keep unit tests fast (< 1s), tag slow tests
//go:build integration
// go test -tags=integration ./...
// ✅ Benchmark to guard performance-sensitive code
func BenchmarkHotPath(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
hotPath()
}
}