Boolean — Professional Level¶
Table of Contents¶
- How It Works Internally
- Runtime Deep Dive
- Compiler Perspective
- Memory Layout
- OS / Syscall Level
- Source Code Walkthrough
- Assembly Output Analysis
- Performance Internals
- Garbage Collector Interaction
- Scheduler Interaction
- Escape Analysis
- Compiler Flags & Build Tags
- SSA Form & Optimization Passes
- Unsafe Operations
- ABI & Calling Conventions
- Tricky Questions
How It Works Internally¶
Focus: "What happens under the hood?"
The bool Type in the Go Type System¶
In Go's type system (src/go/types/basic.go), bool is defined as:
// From Go source: src/go/types/type.go
type BasicKind int
const (
Invalid BasicKind = iota
Bool // bool
Int // int
// ...
)
type Basic struct {
kind BasicKind
info BasicInfo
name string
}
The bool type's kind is Bool = 1. It's a "basic type" — one of the fundamental types built into the language, not defined in terms of other types.
Internal Representation¶
At runtime, bool values are stored as bytes: - false = 0x00 - true = 0x01
This is not specified by the language spec (which only says "true and false"), but it is guaranteed by the implementation. The Go runtime, compiler-generated code, and reflect package all rely on this.
From src/reflect/value.go:
The reflect Package View¶
import "reflect"
var b bool = true
t := reflect.TypeOf(b)
fmt.Println(t.Kind()) // bool
fmt.Println(t.Size()) // 1
fmt.Println(t.Align()) // 1 (1-byte alignment)
v := reflect.ValueOf(b)
fmt.Println(v.Bool()) // true
reflect.Type.Size() returns 1 byte; Align() returns 1 (no alignment requirement beyond byte boundary).
Runtime Deep Dive¶
Bool in the Go Runtime (src/runtime)¶
The Go runtime does not have special handling for bool at the runtime level. It is treated as a 1-byte scalar value. The scheduler, GC, and memory allocator treat it identically to uint8.
Bool Zeroing During Allocation¶
When Go allocates a struct or array, it zeros all memory (runtime.memclrNoHeapPointers). This is why var b bool is guaranteed to be false — the zeroed byte 0x00 is interpreted as false.
// From src/runtime/memclr_amd64.s (simplified concept):
// When allocating `var b bool`, Go calls
// runtime.memclrNoHeapPointers(ptr, 1)
// which writes 0x00 to that byte
Bool in Interface Values¶
When a bool is stored in an interface{}:
This creates an "iface" or "eface" pair on the heap:
eface {
_type: *runtime._type // pointer to bool's type descriptor
data: unsafe.Pointer // for small values like bool: points to static true/false
}
The Go runtime has static true and false values to avoid heap allocation for interface-wrapped bools:
// From src/cmd/compile/internal/walk/convert.go (conceptually)
// Small constants like true/false are stored as static data
// boolStaticData = [2]bool{false, true}
// interface wrapping true uses &boolStaticData[1]
Compiler Perspective¶
How the Compiler Handles Bool Expressions¶
The Go compiler (cmd/compile) processes boolean expressions through several phases:
- Parsing:
ast.BinaryExpr{Op: token.LAND}for&& - Type checking: verifies both operands are
bool - AST to IR: converts to
ir.OANDANDnode - SSA generation: generates SSA form with basic blocks
Short-Circuit in SSA¶
Consider a && b:
SSA (simplified):
b1: entry block
if a goto b2 else b3
b2: a was true, evaluate b
if b goto b4 else b3
b3: result = false
goto b5
b4: result = true
goto b5
b5: phi(b3=false, b4=true) → final result
The phi node merges the two paths. This is how SSA represents conditional values without mutation.
The phi Node¶
A phi node says "this variable has different values depending on which basic block we came from." This is fundamental to how SSA (Static Single Assignment) represents branching logic.
Memory Layout¶
Bool Alignment and Padding¶
// Single bool: 1 byte, alignment 1
var b bool
// Address: any byte boundary
// Bool in struct: alignment rules apply to the STRUCT, not to bool itself
type A struct {
B bool // offset 0
N int64 // offset 8 (padded to 8-byte boundary for int64 alignment)
}
// sizeof(A) = 16
type B struct {
N int64 // offset 0
B bool // offset 8
// 7 bytes padding to maintain struct alignment (= max alignment of any field = 8)
}
// sizeof(B) = 16
type C struct {
B1 bool // offset 0
B2 bool // offset 1
// 6 bytes padding
N int64 // offset 8
}
// sizeof(C) = 16
Check with unsafe.Offsetof:
type MyStruct struct {
Flag1 bool
Value int64
Flag2 bool
}
fmt.Println(unsafe.Offsetof(MyStruct{}.Value)) // 8
fmt.Println(unsafe.Sizeof(MyStruct{})) // 24
Bool Slice Memory¶
var flags []bool = make([]bool, 1000000)
// Memory: 1,000,000 bytes = ~1MB
// Each bool is 1 byte
// Compare to bit-packed:
// 1,000,000 bits = 125,000 bytes = ~125KB
// 8x memory reduction
Bool Array vs. Slice Headers¶
// Array: stored inline in the enclosing struct/stack frame
var arr [3]bool
// Memory: 3 bytes, no indirection
// Slice: 24-byte header on stack + data on heap
var sl []bool = make([]bool, 3)
// Stack: {ptr(8) + len(8) + cap(8)} = 24 bytes header
// Heap: 3 bytes data
OS / Syscall Level¶
Bool at the Syscall Boundary¶
When Go calls OS syscalls, booleans must be converted to OS-native types. For example, syscall.O_RDONLY (for file open flags) uses integer constants, not booleans.
Go's net package converts boolean socket options:
// From src/net/sockopt_posix.go
func setIPv6Only(fd *netFD, family int, ipv6only bool) error {
v := boolint(ipv6only) // convert bool to int
return fd.pfd.SetsockoptInt(syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, v)
}
func boolint(b bool) int {
if b { return 1 }
return 0
}
The kernel expects 0/1 integers for socket boolean options — Go must convert.
Bool in CGO¶
// cgo bool handling
/*
#include <stdbool.h>
bool my_func(bool input);
*/
import "C"
func callC(b bool) bool {
return bool(C.my_func(C.bool(b)))
}
Go's bool maps to C99's _Bool (or bool with <stdbool.h>). Both are 1 byte with 0/1 values.
Source Code Walkthrough¶
Key Files in Go Source¶
src/
builtin/builtin.go - bool type documented
go/types/basic.go - Bool BasicKind constant
cmd/compile/internal/
typecheck/typecheck.go - type checking && and ||
walk/expr.go - code generation for bool ops
ssagen/ssa.go - SSA generation for bool
ir/op.go - OANDAND, OOROR, ONOT opcodes
reflect/type.go - Bool kind in reflect
runtime/memclr_amd64.s - zeroing memory (sets bool to false)
Compiler IR Opcodes for Bool¶
// From src/cmd/compile/internal/ir/op.go
const (
ONOT Op = iota // !X
OANDAND // X && Y
OOROR // X || Y
OEQ // X == Y (returns bool)
ONE // X != Y
OLT // X < Y
OLE // X <= Y
OGT // X > Y
OGE // X >= Y
)
Each opcode corresponds directly to a Go operator. The SSA pass converts these into branches and phi nodes.
Assembly Output Analysis¶
Viewing Generated Assembly¶
# Generate assembly
go tool compile -S main.go
# Or with optimizations disabled for clarity
go tool compile -S -N -l main.go
Simple Bool Assignment¶
Generated x86-64 assembly (simplified):
Bool Comparison (a == b)¶
Assembly (amd64):
MOVBLZX "".a(SP), AX ; load a (zero-extend byte to int)
MOVBLZX "".b+1(SP), CX ; load b
CMPB AL, CL ; compare bytes
SETEQ AL ; set AL to 1 if equal, 0 otherwise
MOVB AL, "".~r2+2(SP) ; store result
RET
Short-Circuit Assembly¶
Assembly (simplified):
MOVBLZX "".a(SP), AX
TESTB AL, AL ; test if a is zero (false)
JEQ L_false ; if false, jump to return false
MOVBLZX "".b+1(SP), AX ; a was true, now check b
MOVB AL, "".~r2+2(SP) ; return b's value
RET
L_false:
MOVB $0, "".~r2+2(SP) ; return false
RET
The short-circuit is visible at the assembly level: JEQ L_false jumps past b's evaluation.
! (NOT) Assembly¶
Assembly:
XOR with 1 is the most efficient NOT for a boolean byte.
Performance Internals¶
Branch Prediction in Modern CPUs¶
Modern CPUs (Intel Ice Lake, AMD Zen 3) have sophisticated branch predictors. For bool in a branch:
- Strongly biased (>99% true or false): ~0.5 cycles/branch
- Weakly biased (60/40 split): ~1-2 cycles/branch
- Unpredictable (50/50 random): ~15 cycles/branch (misprediction penalty)
This is why sorting data before processing can dramatically improve performance of boolean-heavy loops.
SETCC Family Instructions¶
Boolean comparisons compile to SETCC instructions (SETEqual, SETLess, etc.) which set a byte register to 0 or 1 based on the flags register. This avoids branching entirely:
Assembly:
CMPQ "".x(SP), $0 ; compare x with 0
SETGT AL ; set AL = 1 if x > 0, else 0 (no branch!)
MOVB AL, "".~r1+8(SP)
RET
No branch instruction — the CPU computes the bool without branch prediction pressure.
CMOV (Conditional Move) for Branchless Bool¶
The compiler may generate:
Garbage Collector Interaction¶
Bool Pointers and GC¶
*bool pointers ARE tracked by the GC. A bool pointed to on the heap will not be collected as long as a live pointer exists.
Escape Analysis for Bool¶
func main() {
b := true // stack-allocated (does not escape)
fmt.Println(b) // passing to interface causes escape analysis
}
Check escape analysis:
go build -gcflags="-m" main.go
# Output: "b does not escape" (stack allocated)
# But: "b escapes to heap" if passed to an interface
When a bool is stored in an interface{}, it MIGHT escape to the heap. The compiler has an optimization: for small constant values like true and false, it uses static storage instead of heap allocation.
// These do NOT cause heap allocation (compiler optimization):
var i interface{} = true // uses static bool data
var j interface{} = false // uses static bool data
GC Pointer Maps for Bool¶
The GC needs to know which words in a struct/stack frame are pointers (to trace). bool is a scalar — not a pointer — so it is NOT included in the GC's pointer maps. This means GC scanning overhead is zero for bool fields.
Scheduler Interaction¶
Bool in select Statements¶
done := make(chan bool, 1)
select {
case v := <-done:
// v is bool
if v {
fmt.Println("success")
}
case <-time.After(5 * time.Second):
fmt.Println("timeout")
}
The Go scheduler (goroutine scheduler) handles select with selectgo (in src/runtime/select.go). The bool value is passed through the channel's data buffer, which is sized by unsafe.Sizeof(bool{}) = 1 byte.
Bool Channels and Goroutine Signaling¶
// Idiomatic signaling: use struct{} for pure signaling (no data needed)
done := make(chan struct{})
go func() {
// ... work ...
close(done) // signal completion
}()
<-done // wait
// Use bool channel only when the value matters:
result := make(chan bool, 1)
go func() {
result <- validate(input)
}()
if <-result {
fmt.Println("valid")
}
chan struct{} is preferred for signaling because struct{} has zero size (no data to copy through the channel). chan bool copies 1 byte.
Escape Analysis¶
Detailed Escape Analysis for Bool¶
package main
func stackBool() bool {
b := true // stack
return b // value copy, b stays on stack
}
func heapBool() *bool {
b := true // ESCAPES to heap (address taken, returned)
return &b
}
func interfaceBool() interface{} {
b := true // May escape to heap (interface wrapping)
return b // Compiler optimizes: uses static data for true
}
Run with:
When Does Bool Escape?¶
- Address taken and returned:
return &b→ heap - Stored in a heap-allocated struct:
s.flag = &bwheresis on heap → heap - Passed to
interface{}: usually stack (compiler optimization for bool constants) - Captured in a goroutine closure:
go func() { use(b) }()→ heap
Compiler Flags & Build Tags¶
Dead Code Elimination with Const Bool¶
// build_debug.go — only compiled with `-tags debug`
//go:build debug
package main
const isDebugMode = true
// build_release.go — compiled without debug tag
//go:build !debug
package main
const isDebugMode = false
// main.go
func processRequest() {
if isDebugMode {
logDebugInfo() // compiled OUT in release builds
}
}
The compiler eliminates dead code when it can prove at compile time that a const bool is false. This is more efficient than runtime flags.
Link-Time Bool Optimization¶
go build -ldflags="-X 'main.isProduction=true'"
# Note: ldflags set strings, not bools
# You'd need: var isProduction string = "false"
# Then: if isProduction == "true" { ... }
# Or better: use build tags
SSA Form & Optimization Passes¶
How && Becomes SSA¶
Source:
SSA (simplified representation):
b0:
If a → b1, b2
b1: ; a was true
If b → b3, b2
b2: ; result is false (either a or b was false)
goto b4
b3: ; both true
call doWork()
goto b4
b4: ; merge point
...
SSA Optimization: Constant Propagation¶
const debug = false
if debug && expensiveCheck() { // → if false && X → always false
// This entire block is removed
}
SSA pass: constant fold false && X → false. Then dead code elimination removes the block.
SSA Optimization: Boolean Simplification¶
SSA's value numbering recognizes that both x != 0 nodes are identical, replaces the second with the first. Result: x != 0.
Viewing SSA¶
Unsafe Operations¶
Inspecting Bool Memory¶
import "unsafe"
b := true
ptr := unsafe.Pointer(&b)
byteVal := *(*byte)(ptr) // read bool as byte
fmt.Printf("bool %v = byte %d\n", b, byteVal) // bool true = byte 1
// DANGEROUS: setting bool to values other than 0 or 1
*(*byte)(ptr) = 42 // undefined behavior!
fmt.Println(b) // could print true, but behavior is undefined
Never set a bool to a value other than 0 or 1 via unsafe. The compiler may generate code that assumes bool is exactly 0 or 1 (e.g., the XOR-1 NOT optimization would break).
Comparing Bool Memory Directly¶
a := true
b := true
// Direct memory comparison
aPtr := (*byte)(unsafe.Pointer(&a))
bPtr := (*byte)(unsafe.Pointer(&b))
fmt.Println(*aPtr == *bPtr) // true (same byte value)
This is unnecessary (just use a == b) but illustrates the underlying storage.
ABI & Calling Conventions¶
Register-Based ABI (Go 1.17+)¶
Since Go 1.17, the compiler uses a register-based ABI (Application Binary Interface) for function calls on amd64. bool values are passed in integer registers:
ABI: - Input: x in AX register - Output: bool in AX register (as 0 or 1)
Before Go 1.17 (stack-based ABI): - Input: x at 8(SP) - Output: bool at 16(SP)
The register-based ABI significantly reduces function call overhead for small types like bool.
Bool in Variadic Functions¶
When passing bool to interface{} in variadic calls, the compiler uses static addresses for true and false to avoid allocation. This is an important optimization for logging and debugging code.
Tricky Questions¶
Q1: What guarantees that var b bool is false and not some other zero byte? A: Two things: (1) the Go spec guarantees bool's zero value is false; (2) the runtime zeroes all allocated memory, and false is represented as 0x00.
Q2: Why does XOR $1, AL implement boolean NOT? A: Because Go booleans are stored as 0 (false) or 1 (true). XOR with 1 flips bit 0: 0 XOR 1 = 1 (false→true), 1 XOR 1 = 0 (true→false). All other bits are zero, so XOR 1 is equivalent to NOT for this single-bit representation.
Q3: What happens if you use unsafe to set a bool byte to 2? A: Undefined behavior. The compiler generates code assuming bool is exactly 0 or 1. For example, !b compiles to XOR $1, b — if b is 2, then !b = 2 XOR 1 = 3, which is neither 0 nor 1. The program will behave incorrectly.
Q4: Does interface{} = true allocate on the heap? A: No. The compiler uses a static address for the true constant (an optimization). This avoids GC pressure for common bool-to-interface conversions.
Q5: Why is SETGT instruction branchless but if x > 0 { return true } might generate a branch? A: The compiler chooses between SETCC (branchless) and JCC (branch) based on profiling and heuristics. For simple comparisons without complex control flow, SETCC is preferred. Use -gcflags="-N -l" to see unoptimized output; the optimized output usually uses SETCC.
Q6: What is the wire size of bool in Go's binary encoding (encoding/gob, encoding/json)? A: JSON: "true" (4 bytes) or "false" (5 bytes). Gob: 1 byte (0 or 1). Protobuf: varint, 1 byte (0x00 or 0x01). The wire representation is much larger than the in-memory 1-byte representation for text formats.
Diagrams & Visual Aids¶
Bool Representation in Memory¶
Memory address: 0x...
┌──────────────────┐
│ bool byte │
│ 0x00 = false │
│ 0x01 = true │
│ (0x02..0xFF: UB) │
└──────────────────┘
SSA for a && b¶
┌─────────────┐
│ b0: entry │
│ if a: b1/b2│
└─────────────┘
/ \
┌──────────────────┐ ┌──────────────────┐
│ b1: a was true │ │ b2: a was false │
│ if b: b3/b4 │ │ result = false │
└──────────────────┘ └──────────────────┘
/ \ \
┌──────┐ ┌──────┐ ┌──────────────┐
│ b3: │ │ b4: │ │ b5: merge │
│true │ │false │──────────│ phi(b3,b4,b2)│
└──────┘ └──────┘ └──────────────┘