Does Go Really Eliminate Undefined Behavior? An In‑Depth Investigation
The article examines Go’s claim of eradicating undefined behavior, showing that while the language defines many formerly UB cases such as integer overflow and out‑of‑bounds access, it still harbors UB in concurrent data races, interface type confusion, and certain unspecified behaviors.
Determinism Over Performance – Go’s Explicit Philosophy
Ian Lance Taylor explains that C/C++ keep UB for performance, allowing compilers to assume "bad things never happen". Go, however, prioritises determinism: the spec explicitly defines almost all operations that are UB in C/C++, guaranteeing predictable behavior even in error cases.
Deterministic Integer Overflow
Unsigned integers wrap modulo 2^n; signed integers may legally overflow with a defined result based on two's‑complement representation, and the overflow does not cause a runtime panic. The compiler is forbidden from assuming overflow never occurs, e.g. it may not optimise x < x + 1 to always be true.
// https://go.dev/play/p/5CZVVU-SITX
package main
import "fmt"
func main() {
// 1. Signed overflow
var a int8 = 127
// In C this is UB, but in Go it is defined
b := a + 1
fmt.Printf("int8: %d + 1 = %d
", a, b) // prints: 127 + 1 = -128 (wrap‑around)
// 2. Compiler‑prohibited optimisation
// If the compiler assumed overflow never happens, it could drop this check
if b < a {
fmt.Println("overflow occurred: b is less than a")
} else {
fmt.Println("no overflow logic (this path is unreachable in Go)")
}
// 3. Unsigned overflow
var c uint8 = 255
d := c + 1
fmt.Printf("uint8: %d + 1 = %d
", c, d) // prints: 255 + 1 = 0 (strict modulo)
}Array Bounds “Kill Switch”
Go inserts a bounds‑check before every array or slice access (unless static analysis proves safety). Out‑of‑bounds accesses trigger a runtime panic with a clear stack trace, at the cost of some performance.
// https://go.dev/play/p/-CqDpIDr0BC
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
index := getIndex()
fmt.Println("trying index:", index)
// This triggers a runtime panic: index out of range [3] with length 3
val := s[index]
fmt.Println("this line is never reached", val)
}
func getIndex() int { return 3 }Nil Pointer “Hard Landing”
Dereferencing a nil pointer causes a panic that the runtime converts from a hardware SIGSEGV into a recoverable Go panic.
// https://go.dev/play/p/hlyZks1dGRf
package main
import "fmt"
type User struct { Name string }
func main() {
var u *User // nil by default
fmt.Println("about to access nil pointer...")
defer func() {
if r := recover(); r != nil {
fmt.Println("caught panic:", r) // runtime error: invalid memory address or nil pointer dereference
}
}()
fmt.Println(u.Name) // triggers panic
}The Elephant in the Room – Data Races
"However, Go does have undefined behavior: if your program has a race condition, the behaviour is undefined."
Data races are the only true source of undefined behavior in Go. When multiple goroutines access the same memory concurrently and at least one write occurs, the language provides no guarantees, potentially leading to memory corruption or even remote code execution via type confusion.
Interface “Tearing”
Go interfaces are represented as two machine words {type_ptr, data_ptr}. Without proper synchronization, one goroutine may store an A{} value while another stores B{}, resulting in a mixed interface where type_ptr points to A but data_ptr points to B. Calling a method may then invoke A 's method table on B 's layout, causing a panic or, in the worst case, a controllable memory‑corruption exploit.
Slice “Out‑of‑Bounds”
A slice header consists of {ptr, len, cap}. A data race can make len appear larger than the underlying array while ptr still points to the original memory, yielding a slice that can read or write beyond its true capacity – a modern analogue of C buffer overflows.
Unspecified and Implementation‑Defined Behaviors
Map iteration order is intentionally undefined; the runtime randomises the start bucket on each iteration to prevent reliance on a particular hash order.
Expression evaluation order is left‑to‑right for function calls, channel receives, etc., but the relative ordering of side‑effecting sub‑expressions (e.g., map key vs. value evaluation) remains unspecified.
Float‑to‑integer conversion historically differed between architectures; proposal #76264 aims to standardise saturated conversion across platforms.
// Example of unspecified evaluation order
a := 1
f := func() int { a++; return a }
// x may be [1,2] or [2,2] because a's read and f()'s side‑effect order is undefined
x := []int{a, f()}
println(a, x)
// Map literal key/value order example
b := 1
g := func() int { b++; return b }
// If b is evaluated first: key=1, value=2 → m = {1:2}
// If g() runs first: key=2, value=2 → m = {2:2}
m2 := map[int]int{b: g()}
println(b, m2[b])Surviving in an UB‑Prone World
Run unit tests with the -race flag; make race detection a mandatory CI step.
Avoid the unsafe package unless you are writing low‑level runtime or performance‑critical code and have read the Go memory model and GC write‑barrier rules.
Distinguish undefined behavior (crashes, data corruption) from implementation‑defined or unspecified behavior (map order, expression side‑effects) and do not rely on the latter.
Conclusion
Go has not completely eliminated undefined behavior. It has confined UB to concurrency‑related data races and a few gray‑area cases, trading some performance for deterministic guarantees in the vast majority of everyday code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
TonyBai
Tony Bai's tech world (tonybai.com). Not satisfied with just "knowing how", we strive for mastery. Focused on Go language internals, high-quality engineering practices, and cloud‑native architecture, exploring cutting‑edge intersections of Go and AI. Gophers who pursue technology are welcome—follow me and evolve with Go.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
