Logical Operators

How Go’s logical operators &&, ||, and ! work, what short‑circuit evaluation guarantees, and the most common mistakes beginners make when combining conditions

Go provides three logical operators: && (AND), || (OR), and ! (NOT). They let you combine or invert boolean conditions to build the decision logic that controls if statements, loop guards, and return paths. Unlike some languages that treat non‑zero numbers as “truthy,” every operand of a Go logical operator must be a bool. That restriction eliminates an entire class of silent bugs, but it also means you have to be explicit about what counts as true.

The AND Operator (&&)

&& returns true exactly when both the left and right operands are true. If either is false, the whole expression yields false.

LeftRightResult
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

The critical runtime behaviour is that && short‑circuits: if the left operand is false, the right operand is never evaluated. Go guarantees this, so you can safely hang a potentially expensive or even nil‑dependent check on the right side.

package main
import "fmt"
func expensiveCheck() bool {
    fmt.Println("expensiveCheck was called")
    return true
}
func main() {
    a := false
    if a && expensiveCheck() {
        fmt.Println("both true")
    } else {
        fmt.Println("short-circuited")
    }
    // Output:
    // short-circuited
}

The call to expensiveCheck never happens because a is false. This is not an optimisation hint — it is a language guarantee. If you swap the operands, the function runs. That guarantee is what makes patterns like pointer‑guarding possible: ptr != nil && ptr.Value > 0 will not dereference a nil pointer because the second half is skipped when the first half fails.

The OR Operator (||)

|| returns true when at least one operand is true. It only yields false when both are false.

LeftRightResult
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

|| also short‑circuits, but in the opposite direction: if the left operand is true, the right operand is never evaluated. The whole expression is already known to be true the moment the left side succeeds.

func fallback() string {
    fmt.Println("fallback called")
    return "default"
}
func main() {
    name := "Alice"
    result := name == "Alice" || fallback() == "default"
    fmt.Println(result)
    // Output:
    // true
    // (fallback is never called)
}

fallback is never invoked because the first comparison already satisfied the ||. This short‑circuit behavior is what lets you write concise fallback chains, though Go does not have a ||= operator — you have to write the assignment explicitly.

Correct Guard Pattern:

When you want to avoid a nil pointer dereference, place the nil check on the left of && so that short‑circuit evaluation prevents the field access on the right. The compiler does not reorder operands — it always respects the order you write.

The NOT Operator (!)

! is a unary prefix operator that flips a boolean value: !true is false, !false is true. It has no short‑circuit considerations because it only takes one operand, but its high precedence can cause confusion when placed next to comparison operators.

ready := false
if !ready {
    fmt.Println("still preparing")
}

A common readability mistake is burying ! inside a larger expression without parentheses. !a && b is (!a) && b, which reads differently from !(a && b). When in doubt, wrap the negated portion in parentheses — it costs nothing and makes the intent unmistakable.

Short‑Circuit Evaluation in Depth

Every evaluation of && and || follows a strict left‑to‑right order. The language specification explicitly says: “the right operand is evaluated conditionally.” That single sentence has far‑reaching consequences.

  1. Side‑effect safety. If the right operand calls a function, modifies a variable, or accesses a map entry that might not exist, the left operand acts as a guard. The right side runs only when the left side’s value makes it necessary.
  2. No reordering. Compilers may optimise many things, but they may not reorder logical operands or evaluate both sides speculatively. This is part of Go’s memory model guarantees.
  3. Combined with nil comparisons. You will see err != nil && err.Error() == "something" in real codebases. The expression err.Error() is only called when err is not nil, preventing a panic.

Reversing Operands Can Panic:

If you write ptr.Value > 0 && ptr != nil, the program will panic when ptr is nil because the field access happens before the nil check. Always put the safety condition on the left.

Short‑circuit evaluation also affects performance. An expensive validation function placed on the right of && with a cheap filter on the left avoids needless work:

if user.IsActive && performDeepPermissionCheck(user) {
    grantAccess()
}

If IsActive is false for 90% of users, the deep check runs only 10% of the time. Without short‑circuit semantics you would have to manually nest if statements to achieve the same effect.

Operator Precedence with &&, ||, and !

! binds tighter than &&, and && binds tighter than ||. In practice this means:

  • !a && b is (!a) && b
  • a && b || c is (a && b) || c

Because the second behaviour surprises many programmers, the Go compiler does not warn about it — it simply follows the rules. For any expression mixing && and ||, always use parentheses to make the grouping explicit, even when you intend the default precedence. It costs nothing in runtime and saves the next reader (who may be you, six months later) from a subtle bug.

Parentheses Prevent Precedence Errors:

Without parentheses, a || b && c means a || (b && c), not (a || b) && c. If you intended the latter, the code compiles silently and produces wrong logic. Parentheses remove the ambiguity.

Detailed precedence rules for all operators are covered in the Operator Precedence section. For now, remember: ! before &&, && before ||, and when you mix them, wrap each sub‑condition in parentheses.

Common Mistakes and How to Avoid Them

Using Non‑Boolean Operands

Go’s logical operators require bool operands. You cannot use integers, strings, or pointers directly.

var count int
// if count && flag {}          // compile error: operator && not defined on int
if count > 0 && flag {          // correct
}

Compile‑Time Error:

Attempting count && true will not compile. Go rejects truthy/falsy shortcuts on purpose. This forces you to write an explicit comparison, which makes the condition self‑documenting.

Confusing && with & and || with |

Bitwise operators & and | look similar but do not short‑circuit. They always evaluate both operands. Using them by accident on boolean values still works syntactically (the result is a boolean), but you lose short‑circuit protection.

a, b := false, true
// a && expensiveCall()   // expensiveCall never runs
// a & expensiveCall()    // expensiveCall RUNS – and returns an int, not a bool

This is especially dangerous when the function on the right has side effects or relies on a nil guard. Stick to && and || for logic.

Negation Misplacement

!a == b reads as “is (not a) equal to b?” while !(a == b) reads as “are a and b not equal?” The first is rarely what you intend. Most of the time, a != b is clearer than !(a == b), and !a should be written as a separate variable when it appears inside a complex expression.

Forgetting Short‑Circuit Order in Nil Checks

A nil map read (m["key"]) does not panic in Go — it returns the zero value. But a nil map write does panic. Short‑circuit evaluation cannot save you from a nil map assignment; it only prevents reading the value on the right when the map itself might be nil.

Practical Real‑World Usage

Logical operators are the glue that holds control flow together. Here are patterns you will encounter in production Go code.

Guard clauses and early returns

if err != nil {
    return err
}
if user == nil || !user.Active {
    return ErrInactiveUser
}

The || checks two reasons for an invalid user; if either is true, the function exits.

Input validation

if name == "" || len(name) > 100 {
    return fmt.Errorf("name must be 1‑100 characters")
}

|| avoids nested if blocks. If the first condition passes, the second is checked; if it fails, the second is skipped.

Feature flags with role checks

if features.NewDashboard && (user.Role == "admin" || user.BetaTester) {
    serveNewUI()
}

Parentheses around the || group make the logic explicit: the new UI is served only when the feature is on AND the user is either an admin or a beta tester.

Loop termination conditions

for i < len(data) && data[i] != sentinel {
    process(data[i])
    i++
}

The loop stops when either the slice is exhausted or the sentinel value is found, whichever comes first. Short‑circuit evaluation ensures data[i] is never accessed beyond the slice bounds.

Boolean Variables as Self‑Documenting Conditions:

Extract complex logical expressions into named bool variables. isEligible := age >= 18 && !banned is far easier to read when it appears in an if statement than a wall of operators.


Logical operators are small in number, but their short‑circuit guarantees shape how Go programmers write safe, readable conditions. The central insight is that order matters: && and || always evaluate left‑to‑right and stop the moment the result is decided. Understanding that lets you write nil‑safe guards, avoid unnecessary computation, and reason about side effects without guesswork.