if Statement — Professional Level (Internals & Under the Hood)¶
1. How the Go Compiler Represents if Statements¶
The Go compiler translates if statements through several phases: parsing, type checking, SSA (Static Single Assignment) construction, and code generation.
Phase 1: Parsing (go/parser)
The parser produces an *ast.IfStmt node:
// From go/ast/ast.go:
type IfStmt struct {
If token.Pos // position of "if" keyword
Init Stmt // initialization statement; or nil
Cond Expr // condition
Body *BlockStmt // if body
Else Stmt // else branch; or nil (can be *IfStmt for else-if)
}
Phase 2: Type Checking (go/types)
The type checker verifies that Cond has type bool. Unlike C/C++, Go rejects non-boolean conditions at compile time — no implicit integer-to-bool conversion.
Phase 3: SSA (cmd/compile/internal/ssa)
The if statement becomes a conditional branch in SSA form:
b1:
v1 = ... // condition evaluation
If v1 → b2, b3
b2:
// if block
→ b4
b3:
// else block
→ b4
b4:
// continue after if
2. Assembly Output for if Statements¶
Generated assembly (amd64):
"".isPositive STEXT nosplit size=16 args=0x10 locals=0x0
TEXT "".isPositive(SB), NOSPLIT|ABIInternal, $0-16
MOVQ AX, CX // x in AX (register ABI, Go 1.17+)
XORL AX, AX // AX = 0 (false)
TESTQ CX, CX // CX & CX sets flags
SETLE AL // AL = 1 if CX <= 0 (NOT greater than 0)
XORB $1, AL // invert: 1 if CX > 0
RET
Key insight: For simple if statements returning a boolean, the compiler generates SETcc instructions (conditional set) — no branch instruction at all. This is branchless code, immune to branch misprediction.
3. Conditional Move Optimization (CMOV)¶
For simple value selection:
Assembly (amd64):
"".max STEXT nosplit
CMPQ AX, BX // compare a and b
CMOVLLEQ BX, AX // if a <= b, move b into AX
RET // return AX
CMOVLE is a conditional move — no branch, no misprediction. The compiler automatically applies this optimization for simple if-else patterns that select between two values.
When CMOV is applied: - Simple value assignment in both branches - No function calls in branches - Small, register-sized values
When CMOV is NOT applied: - Complex branches with function calls - Memory operations - Multiple assignments
4. Branch Prediction and Alignment¶
The CPU's branch predictor maintains a table of recent branch outcomes. For loops and common patterns, it achieves ~99% accuracy. For truly random branches, it's ~50% accurate.
// CPU profiling shows branch mispredictions:
// go test -bench=. -cpuprofile=cpu.prof
// go tool pprof cpu.prof
// > weblist myFunction
// Look for: `br_miss_pred_retired` in hardware counters
// perf (Linux) to see misprediction rate:
// perf stat -e branch-misses,branches ./myapp
Go compiler applies profile-guided optimization (PGO) in Go 1.21+ to reorder branches based on actual runtime frequency data.
5. SSA Analysis: How the Compiler Eliminates Dead Branches¶
The Go compiler's SSA pass performs dead code elimination on if statements:
const debug = false
func process() {
if debug { // condition is constant false
expensiveLog() // this code is NEVER emitted to binary
}
doWork()
}
The compiler evaluates debug at compile time, sees it's false, and the entire if body is eliminated from the binary. This is why build tags and compile-time constants work for conditional compilation.
# Verify: check binary size with vs without debug constant
go build -ldflags="-X main.debug=true" -o app_debug .
go build -o app_nodebug .
ls -la app_debug app_nodebug
6. The if Init Statement — Compiler Scope Analysis¶
The init statement creates a new lexical scope at the compiler level. Variables in the init statement are allocated to this scope.
In the compiler's IR, this creates a new scope block:
// Compiler's internal representation:
Block {
VarDecl: x = compute()
IfStmt {
Cond: x > 0
Body: use(x)
}
}
The escape analysis determines whether x is stack-allocated or heap-allocated. Since x doesn't outlive the if block, it's typically stack-allocated.
7. The bool Type in Go: Memory Layout¶
In Go, bool is a 1-byte type. In practice:
Memory layout: 0 = false, 1 = true
Other values: undefined behavior (but typically still false in Go)
In registers: typically stored in 8-bit register (AL, BL, etc.)
On stack: aligned to 1-byte boundary
In structs: may be padded for alignment
type Data struct {
Flag1 bool // 1 byte
// 7 bytes padding
Value int64 // 8 bytes (aligned to 8-byte boundary)
Flag2 bool // 1 byte
// 7 bytes padding
}
// sizeof(Data) = 24 bytes (not 10!)
// Optimized:
type Data2 struct {
Value int64 // 8 bytes first
Flag1 bool // 1 byte
Flag2 bool // 1 byte
// 6 bytes padding
}
// sizeof(Data2) = 16 bytes
8. Short-Circuit Evaluation at the Assembly Level¶
Assembly:
TESTB AX, AX // test a
JE .Lfalse // jump if a == false (short-circuit)
TESTB BX, BX // test b
JE .Lfalse // jump if b == false
MOVB $1, AX // return true
RET
.Lfalse:
XORL AX, AX // return false
RET
The JE (jump if equal/zero) instruction implements short-circuit: if a is false (zero), b is never evaluated (the code for evaluating b is jumped over).
9. Escape Analysis with if Init Variables¶
The compiler proves that x doesn't outlive the if block (no goroutine creation, no pointer to x returned), so x is stack-allocated — no GC pressure.
10. Profile-Guided Optimization (PGO) and if¶
Go 1.21+ uses PGO to optimize branch ordering:
# Step 1: Collect a profile from production
go build -o app .
./app # run with production workload
# (CPU profile collected via pprof)
# Step 2: Build with profile
go build -pgo=profile.pprof -o app_pgo .
With PGO: - Frequently-taken branches are placed first in generated code (better instruction cache use) - Cold branches (rarely taken) are moved to the end of the function - Branch prediction hints can be inserted
// PGO-aware pattern: hot path check first
func serve(r *Request) {
// PGO observes: 99% of requests are GET
if r.Method == "GET" { // hot branch — PGO puts this first
return serveGet(r)
}
// cold path
return serveOther(r)
}
11. The Go Specification: Formal if Statement Rules¶
From the Go specification:
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt |
IncDecStmt | Assignment | ShortVarDecl .
Key formal properties: 1. Expression must be of type bool — no implicit conversions 2. SimpleStmt creates a new scope that encloses both Block and else branch 3. The else branch can be another IfStmt (enabling else if chains) or a Block 4. Variables declared in SimpleStmt are in scope throughout the entire if-else chain
12. Boolean Expression Evaluation in the Type Checker¶
The type checker (go/types) ensures if conditions are strictly boolean:
// These all fail type checking:
var x int = 1
if x { } // ERROR: non-boolean condition in if statement (int)
var p *int = &x
if p { } // ERROR: non-boolean condition in if statement (*int)
var s string = "go"
if s { } // ERROR: non-boolean condition in if statement (string)
Internally, the type checker calls check.expr(x, s.Cond) and verifies the resulting type is untyped bool or named type bool.
13. Compiler Inlining and if Statements¶
The Go compiler inlines functions based on an inlining budget. if statements contribute to this budget.
// Inlineable: simple if with low budget
func isPositive(n int) bool {
if n > 0 {
return true
}
return false
}
// May not be inlined: complex if with many branches
func classifyHTTPStatus(code int) string {
if code >= 500 {
return "server error"
} else if code >= 400 {
return "client error"
} else if code >= 300 {
return "redirect"
} else if code >= 200 {
return "success"
}
return "informational"
}
// Check inlining decisions:
// go build -gcflags="-m=2" main.go 2>&1 | grep inline
14. The unsafe Package and Conditional Logic¶
In performance-critical code, unsafe allows bypassing if checks:
// Bounds-check elimination via unsafe:
// Normal: slice[i] with bounds check
func safeAccess(s []int, i int) int {
if i < 0 || i >= len(s) { panic("out of bounds") }
return s[i]
}
// With unsafe (no bounds check, no if):
func unsafeAccess(s []int, i int) int {
return *(*int)(unsafe.Pointer(
uintptr(unsafe.Pointer(&s[0])) + uintptr(i)*unsafe.Sizeof(s[0]),
))
}
// Better: use //go:nosplit and trust the compiler:
//go:nosplit
func fastAccess(s []int, i int) int {
_ = s[i] // hint to compiler: i is in range
return s[i]
}
15. The go vet Analysis of if Statements¶
go vet runs several analyzers that inspect if statements:
copylocks: Detects mutex copying in if-init:
printf: Checks format strings in if branches:
shadow: (not built-in, but via staticcheck) Detects shadowed variables:
sigchanyzer: Detects incorrect signal channel usage in if conditions.
16. Memory Model Implications of if¶
The Go memory model states that within a single goroutine, operations appear in source-code order. This means:
// Within one goroutine: safe — compiler doesn't reorder these
x := readData()
if x != nil {
process(x) // guaranteed: x is the value read above
}
// Across goroutines: NOT guaranteed without synchronization
var shared *Data
// Goroutine 1:
shared = newData()
// Goroutine 2:
if shared != nil { // MAY see stale nil even after goroutine 1 wrote!
use(shared) // data race!
}
// Fix:
var mu sync.Mutex
mu.Lock()
shared = newData()
mu.Unlock()
mu.Lock()
if shared != nil {
use(shared)
}
mu.Unlock()