The panic that teaches the lesson
You are writing a check on a configuration object. You want to verify that the object exists and that its port number is valid. You write the condition, run the program, and watch it crash with a nil pointer dereference. The bug isn't that you checked the wrong thing. The bug is the order. Go evaluates conditions from left to right and stops as soon as the result is determined. This behavior is called short-circuit evaluation. It saves you from unnecessary work and protects you from panics, but only if you put the guard on the left.
How short-circuit works
Go uses two logical operators for boolean conditions: && (AND) and || (OR). Both operators evaluate the left operand first. If the left operand is enough to decide the final result, Go skips the right operand entirely.
For &&, the result is true only if both sides are true. If the left side is false, the whole expression is false. Go stops there. For ||, the result is true if either side is true. If the left side is true, the whole expression is true. Go stops there.
This is the same behavior you see in Python or JavaScript. Go guarantees left-to-right evaluation order for all expressions. This guarantee makes short-circuit reliable. You can depend on the left side running before the right side, and you can depend on the right side running only when needed.
Think of a safety checklist. You check "Engine off" before you check "Key removed". If the engine is running, you don't bother looking for the key. The result is already "Unsafe". Short-circuit is the code equivalent of that checklist.
The minimal safe check
Here's the pattern that prevents nil pointer panics. You check the pointer first, then access the field.
package main
import "fmt"
type Config struct {
Port int
}
func main() {
var cfg *Config // Zero value is nil; no memory allocated for Config yet.
// cfg != nil checks the pointer first.
// If cfg is nil, Go stops here and skips cfg.Port.
// This prevents a nil pointer dereference panic.
if cfg != nil && cfg.Port > 80 {
fmt.Println("Port is high")
}
}
The && operator ensures cfg.Port is accessed only when cfg is valid. If cfg is nil, the second part never runs. The program stays safe.
Runtime walkthrough
When Go runs cfg != nil && cfg.Port > 80, it follows a strict sequence.
- Evaluate
cfg != nil. - If the result is false, the expression is false. Skip
cfg.Port > 80. - If the result is true, evaluate
cfg.Port > 80. - Combine the results.
This sequence is deterministic. Go does not reorder these operations. The compiler generates code that performs the check, branches if false, and only continues to the access if the branch is taken. This is efficient. You avoid the cost of accessing memory or calling functions when the result is already known.
Short-circuit also applies to ||. If you write cfg == nil || cfg.Port == 0, Go checks cfg == nil first. If cfg is nil, the expression is true and cfg.Port == 0 is skipped. If cfg is not nil, Go proceeds to check the port. The order still matters. Putting cfg.Port == 0 on the left would panic when cfg is nil.
Real-world pattern: guard clauses
Short-circuit evaluation enables the guard clause pattern. Go code favors flat control flow over nested blocks. You can chain conditions to exit early when something is wrong.
// ValidateRequest checks if the request is usable.
// It returns true only if the request is not nil and has a valid method.
func ValidateRequest(req *Request) bool {
// req != nil guards against nil pointer dereference.
// req.Method != "" is evaluated only when req is valid.
return req != nil && req.Method != ""
}
This function returns false immediately if req is nil. It avoids nesting and keeps the logic readable. You can chain more checks. return req != nil && req.Method != "" && req.Body != nil. Each check protects the next one.
Go developers often use short-circuit to flatten error handling. Instead of nesting if err == nil { if cfg != nil { ... } }, you write if err != nil || cfg == nil { return }. The || operator returns true if either condition is true, so the block runs when there is an error or the config is missing. This keeps indentation low and makes the unhappy path visible.
Convention aside: Go code accepts the verbosity of if err != nil because it makes error handling explicit. Short-circuit conditions help you combine checks without adding nesting depth. Flat code is easier to read and maintain.
Pitfalls and compiler errors
Short-circuit is powerful, but a few mistakes cause panics or compile errors.
Order matters. The most common bug is reversing the order. If you write cfg.Port > 80 && cfg != nil, Go evaluates cfg.Port > 80 first. If cfg is nil, the program panics with invalid memory address or nil pointer dereference. The compiler cannot catch this at compile time because cfg might be non-nil in other paths. The panic happens at runtime. Always put the safety check on the left.
Side effects may be skipped. If the right operand has a side effect, that side effect only runs when the left operand allows it. This is a feature, not a bug, but it can hide logic errors.
func checkAndLog(val *int) {
// val != nil guards the dereference.
// logValue is called only if val is not nil.
// If val is nil, logValue is skipped entirely.
if val != nil && logValue(*val) {
// ...
}
}
If you expect logValue to run every time, this code fails when val is nil. Short-circuit prevents the call. If you need the side effect to always run, use a separate statement. Don't hide side effects in conditions. Keep conditions pure.
Bitwise vs logical. Newcomers sometimes confuse & with &&. The & operator is bitwise. It works on integers, not booleans. If you use & with booleans, the compiler rejects the code with invalid operation: operator & not defined on a (variable of type bool). Use && for boolean logic. Use & for bit manipulation. The compiler enforces this distinction.
|| order traps. With ||, the trap is less obvious. If you write x.Value == 0 || x == nil, Go checks x.Value == 0 first. If x is nil, this panics. The safe order is x == nil || x.Value == 0. If x is nil, the expression is true and the value check is skipped. If x is not nil, the value check runs safely.
The left-to-right guarantee
Go guarantees left-to-right evaluation order for all expressions. This is stronger than some other languages. In C or C++, the order of evaluation for function arguments is unspecified. Go does not have this ambiguity. If you write f() && g(), f always runs before g. If f returns false, g never runs.
This guarantee makes short-circuit predictable. You can rely on the order. You can also rely on the fact that Go does not reorder operands for optimization. The compiler might optimize the code, but it preserves the evaluation order and side effects. This is part of Go's design philosophy: simplicity and predictability over aggressive optimization.
Convention aside: Go's evaluation order is deterministic. You don't need to worry about undefined behavior related to evaluation order. Write your code with the assumption that left runs before right, and the compiler will honor that.
Bitwise vs logical
It is worth clarifying the difference between logical and bitwise operators. Logical operators work on booleans and short-circuit. Bitwise operators work on integers and do not short-circuit.
| Operator | Type | Short-circuit? |
|---|---|---|
&& |
Bool | Yes |
| ` | ` | |
& |
Integer | No |
| ` | ` | Integer |
^ |
Integer | No |
If you use & on booleans, the compiler rejects it. If you use && on integers, the compiler rejects it with invalid operation: operator && not defined on a (variable of type int). The type system prevents mixing these up. Use the right tool for the job.
Decision matrix
Use && when you need all conditions to be true and want to guard against errors in later checks. Use || when you need at least one condition to be true or want to provide a fallback. Use separate if statements when the second condition has side effects that must always run. Use a helper function when the logic is too complex to read in a single line. Use bitwise & when you are manipulating bits, not booleans.
Short-circuit is a safety net, not a logic trick. Put the cheap check first. Put the safety check first. Keep conditions pure.