Go - Memory & Data Semantics

Concept: Go is a statically-typed language where types are paramount.

Why it matters: The type system catches entire classes of bugs at compile time; understanding it is the foundation of correct Go.

package main

import "fmt"

// compiler rejects the assignment — types are checked before the program runs
func main() {
	var count int = 42
	var label string = "items"

	// count = label // compile error: cannot use string as int
	// label = count // compile error: cannot use int as string

	fmt.Println(count, label) // only reachable if types are correct
}

Gotcha: Reaching for interface{} to "avoid type errors" defers them to runtime panics instead.


Concept: Variables provide a symbolic name to a memory location.

Why it matters: Understanding that a variable IS a memory address (not a value box) explains why pointer semantics work the way they do.

package main

import "fmt"

func main() {
	x := 42

	// &x is the actual memory address x refers to
	fmt.Printf("value=%d  address=%p\n", x, &x)

	// two variables at different addresses, same value
	y := 42
	fmt.Printf("x addr=%p  y addr=%p  same? %v\n", &x, &y, &x == &y)
}

Gotcha: Assuming two variables with the same value share the same address — they don't unless aliased via pointer.


Concept: Type defines the amount of memory a variable uses and how that memory is interpreted.

Why it matters: The same 8 bytes mean completely different things as int64, float64, or [8]byte — the type is the interpreter.

package main

import (
	"fmt"
	"math"
	"unsafe"
)

func main() {
	var i int64   = 1
	var f float64 = 1.0

	// same bit pattern means different things
	fmt.Printf("int64  size=%d bytes  value=%d\n", unsafe.Sizeof(i), i)
	fmt.Printf("float64 size=%d bytes  value=%f\n", unsafe.Sizeof(f), f)

	// float64 bit representation of 1.0 is NOT the integer 1
	fmt.Printf("float64(1.0) bits = %064b\n", math.Float64bits(1.0))
}

Gotcha: Using unsafe.Pointer to reinterpret memory bypasses the type system — the GC may move the data underneath you.


Concept: Byte — the fundamental unit of memory in Go, consisting of 8 bits.

Why it matters: Every type is ultimately measured in bytes; understanding byte size predicts memory layout and performance.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// every type measured in bytes — unsafe.Sizeof reveals the cost
	fmt.Printf("bool    = %d byte\n", unsafe.Sizeof(true))
	fmt.Printf("int8    = %d byte\n", unsafe.Sizeof(int8(0)))
	fmt.Printf("int32   = %d bytes\n", unsafe.Sizeof(int32(0)))
	fmt.Printf("int64   = %d bytes\n", unsafe.Sizeof(int64(0)))
	fmt.Printf("float64 = %d bytes\n", unsafe.Sizeof(float64(0)))
	fmt.Printf("string  = %d bytes (header)\n", unsafe.Sizeof(""))
}

Gotcha: Using int when you need a fixed-size type — int is 4 bytes on 32-bit and 8 bytes on 64-bit platforms.


Concept: Zero Value — the default value a variable gets upon declaration if no explicit initialization is provided.

Why it matters: Go guarantees zero values — no uninitialized memory bugs; you can always reason about the state of a declared variable.

package main

import "fmt"

type Config struct {
	Host    string  // ""
	Port    int     // 0
	TLS     bool    // false
	Timeout float64 // 0.0
}

func main() {
	var cfg Config // zero value — usable immediately, no constructor needed

	// zero value is a valid default — set only what differs
	cfg.Host = "localhost"
	cfg.Port = 8080

	fmt.Printf("%+v\n", cfg)
}

Gotcha: Checking if cfg.Port == 0 to detect "not set" breaks when 0 is a valid value — use a pointer or explicit boolean flag.


Concept: Word — a generic memory allocation unit (4 bytes on 32-bit, 8 bytes on 64-bit).

Why it matters: Pointers, integers, and slice headers are all word-sized; knowing this predicts struct size and alignment.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// pointer size == word size — reveals the platform word width
	var p *int
	wordSize := unsafe.Sizeof(p)
	fmt.Printf("word size = %d bytes\n", wordSize) // 8 on 64-bit

	// interface is 2 words: type pointer + data pointer
	var i interface{}
	fmt.Printf("interface size = %d bytes (2 words)\n", unsafe.Sizeof(i))

	// slice is 3 words: pointer + len + cap
	var s []int
	fmt.Printf("slice header = %d bytes (3 words)\n", unsafe.Sizeof(s))
}

Gotcha: Assuming pointer size is always 8 bytes — code that embeds pointer size as a literal breaks on 32-bit platforms.


Concept: var keyword is preferred for zero-value variable declaration.

Why it matters: var signals intent — "I want the zero value" — making the code's purpose clear to the reader.

package main

import "fmt"

func main() {
	// var signals "zero value is intentional"
	var count int        // 0  — waiting to be incremented
	var name string      // "" — waiting to be assigned
	var ready bool       // false — not ready yet
	var scores []int     // nil slice — no backing array yet

	fmt.Println(count, name, ready, scores)

	// contrast: := signals "I have a non-zero value right now"
	total := 100
	label := "items"
	fmt.Println(total, label)
}

Gotcha: Using := 0 or := "" for zero-value initialization — var is more idiomatic and communicates intent.


Concept: Short variable declaration operator (:=) should be used for non-zero value initialization.

Why it matters: := declares AND initializes in one step — it signals the variable has a meaningful starting value, not just a zero.

package main

import "fmt"

func main() {
	// := when value is known and non-zero at declaration time
	name := "Harish"
	port := 8080
	ratio := 1.5
	tags := []string{"go", "backend"}

	fmt.Println(name, port, ratio, tags)

	// := also used for multi-return unwrapping
	result, err := fmt.Println("hello") // result and err declared + initialized
	_ = result
	_ = err
}

Gotcha: Accidentally shadowing an outer variable with := inside an if block — the outer variable remains unchanged.


Concept: Conversion is preferred over casting for type safety.

Why it matters: Conversion creates a new value of the target type safely; casting (via unsafe) reinterprets raw memory which can produce garbage values.

package main

import "fmt"

func main() {
	var x int32 = 1000

	// conversion: safe — new value created, compiler validates compatibility
	y := int64(x)
	z := float64(x)
	fmt.Println(y, z)

	// conversion with potential data loss — compiler allows it, you are responsible
	var big int32 = 300
	small := int8(big) // 300 overflows int8 (max 127) — result is 44
	fmt.Println(small) // 44 — data lost, but behavior is defined

	// never use unsafe.Pointer for numeric conversion — that is casting
}

Gotcha: Converting float64 to int truncates (doesn't round) — int(3.9) is 3, not 4.


Concept: Struct types are user-defined composite data types that allow developers to model data according to their needs.

Why it matters: Structs are the primary tool for modeling domain concepts; how you define them directly determines correctness and performance.

package main

import "fmt"

// model the domain — fields named for what they represent, not how they're stored
type Employee struct {
	ID         int
	Name       string
	Department string
	Salary     float64
}

func main() {
	// struct literal: all fields explicit — no magic positional arguments
	e := Employee{
		ID:         101,
		Name:       "Harish",
		Department: "Engineering",
		Salary:     120000.00,
	}
	fmt.Printf("%+v\n", e)
}

Gotcha: Using positional struct literals Employee{101, "Harish", "Engineering", 120000} — adding a field breaks every call site silently.


Concept: Structs are allocated in contiguous memory blocks.

Why it matters: Contiguous layout means the CPU can load the entire struct in one or two cache lines — random pointer chasing cannot achieve this.

package main

import (
	"fmt"
	"unsafe"
)

type Point struct {
	X float64 // offset 0
	Y float64 // offset 8
}

func main() {
	p := Point{X: 1.5, Y: 2.5}

	// both fields are contiguous — loading X into cache also loads Y
	fmt.Printf("Point size  = %d bytes\n", unsafe.Sizeof(p))
	fmt.Printf("X offset    = %d\n", unsafe.Offsetof(p.X))
	fmt.Printf("Y offset    = %d\n", unsafe.Offsetof(p.Y))
	fmt.Printf("X addr      = %p\n", &p.X)
	fmt.Printf("Y addr      = %p\n", &p.Y) // exactly 8 bytes after X
}

Gotcha: Storing *Point in a slice instead of Point breaks contiguity — each element is a pointer to a random heap location.


Concept: Memory alignment — certain data types must be stored at memory addresses that are multiples of their size.

Why it matters: Misaligned access causes the CPU to perform two memory reads for one value — some platforms trap on misalignment entirely.

package main

import (
	"fmt"
	"unsafe"
)

// misaligned struct — compiler inserts padding automatically
type Misaligned struct {
	A bool    // 1 byte, offset 0
	// 7 bytes padding inserted here
	B float64 // 8 bytes, must start at offset 8 (multiple of 8)
	C bool    // 1 byte, offset 16
	// 7 bytes padding at end
} // total: 24 bytes

func main() {
	var m Misaligned
	fmt.Printf("size=%d\n", unsafe.Sizeof(m))           // 24
	fmt.Printf("A offset=%d\n", unsafe.Offsetof(m.A))   // 0
	fmt.Printf("B offset=%d\n", unsafe.Offsetof(m.B))   // 8
	fmt.Printf("C offset=%d\n", unsafe.Offsetof(m.C))   // 16
}

Gotcha: Assuming unsafe.Sizeof(struct{A bool; B float64}{}) is 9 bytes — it's 16 due to alignment padding.


Concept: Padding — unused bytes inserted by the compiler within a struct to ensure proper memory alignment.

Why it matters: Padding wastes memory; reordering fields largest-to-smallest eliminates it without changing behavior.

package main

import (
	"fmt"
	"unsafe"
)

// wasteful: 24 bytes due to padding
type Wasteful struct {
	A bool    // 1 byte + 7 pad
	B float64 // 8 bytes
	C bool    // 1 byte + 7 pad
}

// efficient: 10 bytes needed, 16 with minimal padding (float64 alignment)
type Efficient struct {
	B float64 // 8 bytes, offset 0
	A bool    // 1 byte, offset 8
	C bool    // 1 byte, offset 9
	// 6 bytes padding at end for struct alignment
}

func main() {
	fmt.Printf("Wasteful:  %d bytes\n", unsafe.Sizeof(Wasteful{}))   // 24
	fmt.Printf("Efficient: %d bytes\n", unsafe.Sizeof(Efficient{}))  // 16
}

Gotcha: Reordering fields for memory without updating comments — reviewers won't understand why the order looks wrong.


Concept: Optimize struct field order for correctness (readability); reorder only if memory constraints require it.

Why it matters: Readability-first ordering groups related fields logically; memory optimization is a last resort, not a default.

package main

import "fmt"

// fields ordered for readability — related fields grouped together
type HTTPRequest struct {
	Method  string // what
	URL     string // where
	Headers map[string]string // metadata
	Body    []byte // payload
	Timeout int    // control
}

func main() {
	req := HTTPRequest{
		Method:  "POST",
		URL:     "/api/users",
		Headers: map[string]string{"Content-Type": "application/json"},
		Body:    []byte(`{"name":"Harish"}`),
		Timeout: 30,
	}
	fmt.Printf("method=%s url=%s\n", req.Method, req.URL)
}

Gotcha: Reordering struct fields without updating tests that rely on positional literals — silent breakage.


Concept: Type Compatibility — two types are compatible if they have the same underlying memory layout.

Why it matters: Go allows explicit conversion between structurally identical types, enabling type-safe domain modeling without runtime cost.

package main

import "fmt"

type Meters float64
type Feet float64

func main() {
	var m Meters = 100.0

	// explicit conversion required — types are distinct even if layout is identical
	f := Feet(m * 3.28084)
	fmt.Printf("%.2f meters = %.2f feet\n", m, f)

	// compiler prevents accidental mixing
	// var bad Feet = m // compile error: cannot use Meters as Feet
}

Gotcha: Using type Speed = float64 (alias) instead of type Speed float64 (new type) — aliases provide no type safety.


Concept: Anonymous Struct — a struct defined inline without a specific name.

Why it matters: Anonymous structs are ideal for one-off groupings (test tables, JSON responses) without polluting the package namespace.

package main

import "fmt"

func main() {
	// anonymous struct: used once, no need for a named type
	point := struct {
		X, Y int
	}{X: 10, Y: 20}

	fmt.Println(point.X, point.Y)

	// common use: table-driven test cases
	cases := []struct {
		input    int
		expected int
	}{
		{2, 4},
		{3, 9},
		{4, 16},
	}
	for _, c := range cases {
		fmt.Printf("%d² = %d\n", c.input, c.input*c.input)
		_ = c.expected
	}
}

Gotcha: Using anonymous structs for types that appear in more than one place — create a named type instead for consistency.


Concept: Everything in Go is pass-by-value.

Why it matters: This single rule explains all of Go's memory semantics — there is no hidden sharing unless you explicitly use a pointer.

package main

import "fmt"

type Config struct{ Debug bool }

// function receives a COPY — mutation does not affect the caller
func configure(cfg Config) {
	cfg.Debug = true // modifies the copy only
	fmt.Println("inside:", cfg.Debug)
}

func main() {
	c := Config{Debug: false}
	configure(c)
	fmt.Println("outside:", c.Debug) // still false — copy was modified, not original
}

Gotcha: Expecting map mutation to be invisible to the caller — maps are reference types, so the pointer (not the data) is copied.


Concept: Goroutine — an independent execution path, similar to a lightweight thread.

Why it matters: Goroutines are cheap (2KB stack) and multiplexed onto OS threads by the scheduler — the foundation of Go concurrency.

package main

import (
	"fmt"
	"sync"
)

// goroutines are cheap enough to create per-request — unlike OS threads
func handle(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("goroutine %d running\n", id)
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go handle(i, &wg) // go keyword: launch goroutine, return immediately
	}
	wg.Wait()
}

Gotcha: Launching goroutines without tracking them — if main returns, all goroutines are killed instantly regardless of their state.


Concept: Stack — a memory region allocated for each goroutine to manage function calls and local variables; stacks grow dynamically.

Why it matters: Go's dynamically growing stack means you never set a stack size limit — goroutines start small (2KB) and grow as needed.

package main

import (
	"fmt"
	"runtime"
)

// deep recursion grows the stack automatically — no stack overflow in Go
func recurse(n int) int {
	if n == 0 {
		return 0
	}
	return n + recurse(n-1)
}

func main() {
	fmt.Println(recurse(10000))

	// goroutine stack starts at ~2KB — much smaller than OS thread (1-8MB)
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
	fmt.Printf("heap alloc: %d KB\n", stats.HeapAlloc/1024)
}

Gotcha: Storing a pointer to a stack variable and sharing it across goroutines — the stack may be relocated on growth, but Go handles this; the danger is conceptual confusion.


Concept: Frame — a segment on the stack dedicated to a specific function call; stores local variables, parameters, and return information.

Why it matters: Each function call creates a frame; understanding frames explains why local variables are "free" and why returning a pointer causes heap escape.

package main

import "fmt"

// each call to addFrame gets its own frame — a, b, result are local to that call
func addFrame(a, b int) int {
	result := a + b // result lives in this frame — freed when function returns
	return result   // value is COPIED out of the frame to the caller's frame
}

func main() {
	// three separate stack frames created and destroyed
	x := addFrame(1, 2) // frame 1
	y := addFrame(3, 4) // frame 2
	z := addFrame(5, 6) // frame 3
	fmt.Println(x, y, z)
}

Gotcha: Assuming a local variable persists after function return — it doesn't unless it escapes to the heap.


Concept: Value Semantics — functions operating on their own copy of data, preventing unintended modifications.

Why it matters: Value semantics are the safest default — no shared state means no data races and no unexpected side effects.

package main

import "fmt"

type Point struct{ X, Y float64 }

// value receiver: Scale works on a copy — original unchanged
func (p Point) Scale(factor float64) Point {
	return Point{X: p.X * factor, Y: p.Y * factor} // returns new value
}

func main() {
	original := Point{X: 3, Y: 4}
	scaled := original.Scale(2)

	fmt.Println("original:", original) // {3 4} — untouched
	fmt.Println("scaled:  ", scaled)   // {6 8}
}

Gotcha: Choosing value semantics for a type containing a mutex — copying a mutex is a data race.


Concept: Pointer Semantics — functions sharing access to data using pointers, allowing modifications to directly affect the original data.

Why it matters: Pointer semantics are necessary for mutation, large structs (avoid copying), and types that must not be copied (mutexes, file handles).

package main

import "fmt"

type Account struct{ balance float64 }

// pointer receiver: Deposit modifies the original — caller sees the change
func (a *Account) Deposit(amount float64) {
	a.balance += amount // writes through the pointer to the original
}

func (a *Account) Balance() float64 { return a.balance }

func main() {
	acc := Account{}
	acc.Deposit(100)
	acc.Deposit(50)
	fmt.Printf("balance: %.2f\n", acc.Balance()) // 150.00
}

Gotcha: Mixing value and pointer receivers on the same type — pick one semantic and apply it consistently.


Concept: Side Effects — changes in a program's state that occur as a result of function execution through pointers.

Why it matters: Unintended side effects are the root cause of most concurrency bugs and unexpected state mutations.

package main

import "fmt"

// side effect made explicit in the function signature via pointer parameter
func appendItem(items *[]string, item string) {
	*items = append(*items, item) // side effect: mutates the caller's slice
}

func main() {
	cart := []string{"apple"}
	appendItem(&cart, "banana") // caller explicitly opts into the side effect
	appendItem(&cart, "cherry")
	fmt.Println(cart) // [apple banana cherry]
}

Gotcha: Returning a modified copy (value semantic) when the function signature promises mutation via pointer — the caller's original is unchanged, causing bugs.


Concept: Factory Function — a function designed to create and initialize a value of a specific type and return it.

Why it matters: Factory functions enforce valid construction — a zero-value struct may be in an invalid state that the factory prevents.

package main

import (
	"errors"
	"fmt"
)

type Server struct {
	host string
	port int
}

// factory: enforces invariants — no Server with empty host or invalid port can exist
func NewServer(host string, port int) (*Server, error) {
	if host == "" {
		return nil, errors.New("NewServer: host cannot be empty")
	}
	if port < 1 || port > 65535 {
		return nil, fmt.Errorf("NewServer: invalid port %d", port)
	}
	return &Server{host: host, port: port}, nil
}

func main() {
	s, err := NewServer("localhost", 8080)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("server: %s:%d\n", s.host, s.port)
}

Gotcha: Exporting struct fields when using a factory — callers can bypass the factory and create invalid state directly.


Concept: Escape Analysis — a compiler analysis that determines if a value needs to be allocated on the heap.

Why it matters: Understanding escape analysis lets you write code that stays on the stack (fast, no GC) versus unnecessarily escaping to the heap (slow, GC pressure).

package main

import "fmt"

// stays on stack: value returned by copy — no escape
func stackValue() int {
	x := 42
	return x // x copied to caller's frame, original frame discarded
}

// escapes to heap: address of local variable returned — must outlive the frame
func heapValue() *int {
	x := 42
	return &x // x must survive after this frame is gone — heap allocated
}

func main() {
	v := stackValue() // no heap allocation
	p := heapValue()  // heap allocation — GC must track *p

	fmt.Println(v, *p)
}
// verify: go build -gcflags='-m' main.go
// output: "x escapes to heap"

Gotcha: Returning &localVar from a function thinking it saves a copy — it actually forces a heap allocation.


Concept: Sharing data up the call stack leads to escape to the heap.

Why it matters: "Sharing up" means a child function returns a pointer to data the parent must keep alive — the compiler moves it to the heap automatically.

package main

import "fmt"

// sharing UP: newUser shares its local variable with the caller above it
func newUser(name string) *struct{ Name string } {
	u := struct{ Name string }{Name: name}
	return &u // u shared UP the stack — escapes to heap
}

// sharing DOWN: main passes its variable down to print — stays on stack
func printUser(u *struct{ Name string }) {
	fmt.Println(u.Name) // receives pointer from above — no escape here
}

func main() {
	u := newUser("Harish") // heap allocated — newUser shared it up
	printUser(u)            // passed down — no additional escape
}

Gotcha: Thinking "sharing down" (passing a pointer into a function) causes heap escape — it doesn't, only sharing up does.


Concept: Use go build -gcflags '-m' to show escape analysis information.

Why it matters: Escape analysis output is the ground truth for allocation decisions — profiling without it is guesswork.

package main

import "fmt"

// run: go build -gcflags='-m -m' main.go to see escape decisions
type Config struct {
	Name string
	Port int
}

// stays on stack — no pointer returned, no interface conversion
func stackAlloc() Config {
	return Config{Name: "local", Port: 8080}
}

// escapes — pointer returned, caller uses it after this frame is gone
func heapAlloc() *Config {
	return &Config{Name: "heap", Port: 9090} // "Config literal escapes to heap"
}

func main() {
	s := stackAlloc()
	h := heapAlloc()
	fmt.Println(s.Name, h.Name)
}
// go build -gcflags='-m' .
// look for: "escapes to heap" and "does not escape"

Gotcha: Running -gcflags='-m' on only one file instead of ./... — inter-package escapes are missed.


Concept: Go stacks are contiguous and can grow dynamically by doubling in size.

Why it matters: Contiguous stacks allow O(1) local variable access; doubling growth amortizes the copying cost.

package main

import (
	"fmt"
	"runtime"
)

// each goroutine starts with a small stack — grown automatically as needed
func deepCall(n int) int {
	if n == 0 {
		var s runtime.MemStats
		runtime.ReadMemStats(&s)
		fmt.Printf("stack sys: %d KB\n", s.StackSys/1024)
		return 0
	}
	var padding [256]byte // force stack growth faster
	_ = padding
	return deepCall(n - 1)
}

func main() {
	result := deepCall(100)
	fmt.Println("result:", result)
}

Gotcha: Holding a pointer into a goroutine's stack from another goroutine — when the stack grows and moves, the pointer becomes invalid (Go's GC updates references, but unsafe.Pointer bypasses this).


Concept: Stack growth can involve moving data in memory and updating pointers.

Why it matters: Go handles pointer updates during stack growth transparently — this is why goroutine stacks can grow safely but unsafe.Pointer to stack memory is dangerous.

package main

import "fmt"

// Go transparently updates all pointers when the stack is moved — safe by default
func stackGrowth(n int, ptr *int) *int {
	if n == 0 {
		return ptr
	}
	local := n                      // local variable on this stack frame
	return stackGrowth(n-1, &local) // pass pointer down — Go updates it if stack moves
}

func main() {
	start := 42
	p := stackGrowth(1000, &start)
	fmt.Println(*p) // valid — Go updated the pointer when the stack was copied
}

Gotcha: Using unsafe.Pointer to cache a stack address across function calls — the cached address becomes stale after stack growth.


Concept: Goroutines cannot share data on their stacks with other goroutines.

Why it matters: Stack isolation prevents goroutines from accidentally corrupting each other's local state.

package main

import (
	"fmt"
	"sync"
)

// wrong: trying to share a stack address across goroutines
func wrongSharing(wg *sync.WaitGroup) {
	defer wg.Done()
	x := 42
	// sending &x to another goroutine is dangerous — x is on this goroutine's stack
	// if this goroutine returns, x is gone; the receiver holds a dangling pointer
	fmt.Println(x) // safe: used only within this goroutine's scope
}

// correct: heap-allocate data that must be shared between goroutines
func correctSharing(shared *int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println(*shared) // safe: shared is on the heap, not any goroutine's stack
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go wrongSharing(&wg)

	heapVal := 99 // escapes to heap because address is shared
	wg.Add(1)
	go correctSharing(&heapVal, &wg)

	wg.Wait()
}

Gotcha: Taking the address of a loop variable and passing it to a goroutine — by the time the goroutine runs, the loop variable has changed.


Concept: Sharing values between goroutines necessitates heap allocation.

Why it matters: The heap is the only memory region accessible by all goroutines — stack memory is goroutine-private.

package main

import (
	"fmt"
	"sync"
)

// heap-allocated result shared safely between producer and consumer goroutines
func produce(wg *sync.WaitGroup) *[]int {
	results := make([]int, 0, 5) // make allocates on heap — safe to share
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			results = append(results, i*i)
		}
	}()
	return &results // heap pointer — valid after goroutine runs
}

func main() {
	var wg sync.WaitGroup
	rp := produce(&wg)
	wg.Wait()
	fmt.Println(*rp) // [0 1 4 9 16] — heap data outlives producer goroutine
}

Gotcha: Sharing a heap-allocated value across goroutines without synchronization — heap allocation alone doesn't prevent data races.


Concept: The garbage collector reclaims unused memory on the heap.

Why it matters: The GC is your memory manager — understanding when it runs and what it costs lets you write GC-friendly code.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// force a GC cycle and observe heap before/after
	var before, after runtime.MemStats

	runtime.GC()
	runtime.ReadMemStats(&before)

	// create heap garbage
	for i := 0; i < 10000; i++ {
		_ = make([]byte, 1024) // 1KB allocations, immediately unreferenced
	}

	runtime.GC() // GC reclaims the unreferenced allocations
	runtime.ReadMemStats(&after)

	fmt.Printf("GC cycles: %d\n", after.NumGC)
	fmt.Printf("heap after GC: %d KB\n", after.HeapAlloc/1024)
}

Gotcha: Calling runtime.GC() in production to "help" performance — it pauses all goroutines and should be left to the runtime's pacer.


Concept: Go's garbage collector is concurrent — operates while the application runs.

Why it matters: Concurrent GC means your program doesn't fully stop while memory is reclaimed — latency spikes are minimized.

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// concurrent GC: application goroutines and GC goroutines run simultaneously
	// observe with: GODEBUG=gctrace=1 go run main.go

	done := make(chan struct{})
	go func() {
		// this goroutine keeps running during GC — concurrent collector
		count := 0
		for {
			select {
			case <-done:
				fmt.Printf("counted %d iterations during GC\n", count)
				return
			default:
				count++
			}
		}
	}()

	// generate heap garbage to trigger GC
	for i := 0; i < 100000; i++ {
		_ = make([]byte, 1024)
	}

	time.Sleep(10 * time.Millisecond)
	close(done)

	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	fmt.Printf("NumGC: %d\n", stats.NumGC)
}

Gotcha: Assuming concurrent GC means zero impact — it still uses CPU and causes short stop-the-world pauses.


Concept: The pacer determines when to start a garbage collection cycle.

Why it matters: The pacer uses GOGC (default 100%) to decide when to trigger GC — tuning it trades memory for CPU.

package main

import (
	"fmt"
	"os"
	"runtime"
)

func main() {
	// pacer triggers GC when heap doubles from last collection
	// GOGC=100 (default): heap can double before GC runs
	// GOGC=50: GC runs more often, lower memory, more CPU
	// GOGC=200: GC runs less often, more memory, less CPU

	fmt.Printf("GOGC env: %s\n", os.Getenv("GOGC")) // "" means default 100

	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	fmt.Printf("NextGC target: %d KB\n", stats.NextGC/1024)
	fmt.Printf("HeapAlloc:     %d KB\n", stats.HeapAlloc/1024)
	// NextGC ≈ HeapAlloc * (1 + GOGC/100)
}
// tune: GOGC=50 go run main.go (latency-sensitive)
// tune: GOGC=400 go run main.go (throughput-focused, more memory OK)

Gotcha: Setting GOGC=off in production to eliminate GC — the heap grows unbounded until the process is OOM-killed.


Concept: Stop-the-world pauses — Go aims to keep them under 100 microseconds.

Why it matters: STW pauses block ALL goroutines — even 1ms pauses cause P99 latency spikes in high-throughput services.

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// measure the actual STW pause your program experiences
	var maxPause time.Duration

	go func() {
		for {
			before := time.Now()
			runtime.Gosched() // yield — measures scheduler + GC latency
			pause := time.Since(before)
			if pause > maxPause {
				maxPause = pause
			}
		}
	}()

	// generate GC pressure
	for i := 0; i < 1000000; i++ {
		_ = make([]byte, 128)
	}

	time.Sleep(100 * time.Millisecond)
	fmt.Printf("observed max pause: %v\n", maxPause)
	// target: < 100µs for low-latency services
}

Gotcha: Measuring application latency with time.Since and ignoring STW pauses — the tail latency spike is invisible without tracing.


Concept: Heap Pressure — how quickly the heap is being filled; high pressure leads to more frequent GC cycles.

Why it matters: Every unnecessary allocation increases heap pressure, which increases GC frequency, which steals CPU from your application.

package main

import (
	"fmt"
	"runtime"
	"strings"
)

// high pressure: allocates on every call
func joinHigh(parts []string) string {
	result := ""
	for _, p := range parts {
		result += p // new string allocation every iteration
	}
	return result
}

// low pressure: single allocation via Builder
func joinLow(parts []string) string {
	var b strings.Builder
	for _, p := range parts {
		b.WriteString(p) // writes to pre-allocated buffer
	}
	return b.String() // one allocation
}

func main() {
	parts := make([]string, 1000)
	for i := range parts {
		parts[i] = "x"
	}

	var before, after runtime.MemStats
	runtime.ReadMemStats(&before)
	for i := 0; i < 1000; i++ {
		_ = joinHigh(parts)
	}
	runtime.ReadMemStats(&after)
	fmt.Printf("high pressure allocs: %d\n", after.Mallocs-before.Mallocs)
}

Gotcha: Ignoring heap pressure in initialization code — startup allocations are one-time, but per-request allocations compound.

Concept: Constants in Go are not variables — they exist only at compile time.

Why it matters: Constants are substituted at compile time — they have no runtime memory address and impose zero runtime cost.

package main

import "fmt"

const (
	MaxRetries = 3
	Timeout    = 30 // seconds
	AppName    = "treasury-data-hub"
)

func main() {
	fmt.Println(MaxRetries, Timeout, AppName)

	// constants have no address — this would be a compile error:
	// p := &MaxRetries // invalid: cannot take address of constant

	// constants are evaluated at compile time
	const result = 1<<10 + 2<<10 // computed at compile time, not runtime
	fmt.Println(result)           // 3072
}

Gotcha: Using var instead of const for truly fixed values — var allocates memory and can be accidentally mutated.


Concept: Typed constants adhere to the constraints of their declared type; untyped constants have minimum precision of 256 bits.

Why it matters: Untyped constants avoid precision loss in arithmetic — they carry full mathematical precision until assigned to a variable.

package main

import "fmt"

// typed constant: constrained to float64 precision
const TypedPi float64 = 3.14159265358979323846

// untyped constant: 256-bit precision at compile time — no truncation yet
const UntypedPi = 3.14159265358979323846264338327950288

func main() {
	// untyped adapts to the context it's used in
	var f32 float32 = UntypedPi // truncated to float32 precision at assignment
	var f64 float64 = UntypedPi // truncated to float64 precision at assignment

	fmt.Printf("float32: %.10f\n", f32)
	fmt.Printf("float64: %.20f\n", f64)
}

Gotcha: Declaring const x float64 = 1/3 — integer division happens first, result is 0; use const x = 1.0/3.0.


Concept: Kind promotion — float promotes over int for untyped constants.

Why it matters: Kind promotion allows mixed arithmetic on untyped constants without explicit conversions — the result takes the most capable kind.

package main

import "fmt"

func main() {
	// untyped int + untyped float = untyped float (float promotes)
	const a = 1   // untyped int kind
	const b = 1.5 // untyped float kind
	const c = a + b // c is untyped float kind — a was promoted

	fmt.Println(c) // 2.5

	// untyped constant assigned to typed variable — kind must be compatible
	var x float64 = c  // ok: untyped float → float64
	var y int = a       // ok: untyped int → int (a is exactly representable)
	// var z int = b    // compile error: constant 1.5 truncates to int

	fmt.Println(x, y)
}

Gotcha: Assuming const x = 1 / 2 gives 0.5 — both operands are untyped int kind, result is 0; write 1.0 / 2.


Concept: Iota — a powerful tool for creating blocks of related constants, automatically incrementing with each declaration.

Why it matters: iota eliminates manual enumeration errors and makes the relationship between constants explicit and maintainable.

package main

import "fmt"

type Weekday int

const (
	Sunday    Weekday = iota // 0
	Monday                   // 1
	Tuesday                  // 2
	Wednesday                // 3
	Thursday                 // 4
	Friday                   // 5
	Saturday                 // 6
)

func (d Weekday) String() string {
	names := [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
	if d < Sunday || d > Saturday {
		return fmt.Sprintf("Weekday(%d)", d)
	}
	return names[d]
}

func main() {
	fmt.Println(Wednesday) // Wednesday (not 3)
	fmt.Println(int(Friday)) // 5
}

Gotcha: Using iota in separate const blocks — iota resets to 0 in each new const block.


Concept: Iota especially useful for defining bitmaps or sequences of integer-based constants.

Why it matters: Bit-shifting iota creates power-of-two flag values — each constant represents a single bit, enabling efficient bitwise flag combinations.

package main

import "fmt"

type Permission uint

const (
	Read    Permission = 1 << iota // 1  (001)
	Write                          // 2  (010)
	Execute                        // 4  (100)
)

func (p Permission) String() string {
	s := ""
	if p&Read != 0    { s += "r" } else { s += "-" }
	if p&Write != 0   { s += "w" } else { s += "-" }
	if p&Execute != 0 { s += "x" } else { s += "-" }
	return s
}

func main() {
	// combine permissions with bitwise OR
	userPerm := Read | Write
	fmt.Println(userPerm) // rw-

	// test a specific permission with bitwise AND
	fmt.Println(userPerm&Execute != 0) // false — no execute
	fmt.Println(userPerm&Read != 0)    // true
}

Gotcha: Combining non-power-of-two iota values with bitwise OR — overlapping bits produce incorrect combined values.