Comparison and Logical Operators
Understand how to compare values and combine boolean expressions in Go using comparison and logical operators
Overview
Every program that does something useful needs to decide what happens next. You check whether a user is logged in, see if a number is above a threshold, or stop a loop when a counter reaches zero. Those decisions rest on two kinds of operators: comparison operators, which turn two values into a true or false answer, and logical operators, which let you combine multiple true/false answers into one.
In Go, comparison operators give you a boolean – and only a boolean – from a pair of values. There is no “truthy” number, no “empty string is false” trick. If you need a true or false, you must explicitly compare. Logical operators then combine those booleans, letting you express conditions like “age is above 18 and has a valid license” as one expression.
Think of comparison operators as the eyes of your program: they look at data and report what they see. Logical operators are the brain: they decide what to make of multiple reports.
The next two sections unpack each category, from the straightforward == to the less obvious rules about short‑circuit evaluation.
Comparison Operators
Comparison operators look at two values and answer a simple question: are they equal? Is one bigger? The answer is always true or false – nothing else.
Go gives you six ways to compare:
==— equal to!=— not equal to<— less than<=— less than or equal to>— greater than>=— greater than or equal to
They work across numbers, strings, booleans, pointers, channels, interfaces, and – under certain rules – structs and arrays.
Basic Equality and Inequality
package main
import "fmt"
func main() {
a := 10
b := 20
fmt.Println(a == b) // false
fmt.Println(a != b) // true
name := "Gopher"
fmt.Println(name == "Gopher") // true
}
Two integers, 10 and 20, are not equal, so a == b is false. != works as the exact opposite; one is the negation of the other. The string comparison checks character-by-character: both strings contain the same bytes in the same order, so name == "Gopher" is true.
A detail that trips up newcomers: when you compare two numeric types that are different – say an int and an int32 – Go will not compile the code. The language requires operands to be of the same type. You must convert one explicitly.
Slices, maps, and functions cannot be compared with ==:
The operators == and != work only on comparable types. Slices, maps, and functions are not comparable. If you try s1 == s2 with two slices, the compiler stops with an error. To check whether two slices contain the same elements, use reflect.DeepEqual or write a loop. Maps and functions simply cannot be compared for equality at all, other than comparing them with nil.
Ordering Comparisons
The four ordering operators (<, <=, >, >=) let you rank values. For numbers the meaning is obvious. For strings, Go compares them lexicographically byte by byte – essentially dictionary order, but based on Unicode code points. That means "a" < "b" is true, and "go" > "Go" is true because lowercase g has a higher code point than uppercase G.
package main
import "fmt"
func main() {
fmt.Println(3.14 > 3) // true
fmt.Println("cat" < "dog") // true
fmt.Println("Go" < "go") // true
}
Comparing an int and a float64 directly would be a compile‑time error; here the untyped constant 3 adapts to float64 because 3.14 is float64. That’s a rare exception – in most real code you’ll be comparing two variables of the same type. When in doubt, make the types match before comparing.
Strings are compared byte‑by‑byte, not by any locale‑aware ordering. For case‑insensitive or language‑specific sorting, you need the strings or collate packages.
NaN is not equal to itself:
In floating‑point arithmetic, the special value NaN (“Not a Number”) is not equal to anything, including itself. So math.NaN() == math.NaN() is false. Use math.IsNaN() to check for NaN values. Comparisons with NaN using <, >, etc. are always false as well. This can cause subtle bugs when dealing with invalid numerical results.
Comparable Types and Limitations
A type is comparable if Go can apply == and != to it. Here is what you can compare:
- Numeric types (int, float64, etc.) – when the two operands are the same concrete type.
- Strings – always comparable.
- Booleans –
trueandfalse. - Pointers – two pointers are equal if they point to the same variable or both are
nil. - Channels – two channel values are equal if they were created by the same call to
make. - Interfaces – equality depends on the dynamic type and value inside the interface; if the dynamic type is not comparable, comparing interfaces panics at runtime.
- Structs – if all fields are comparable, the struct is comparable.
- Arrays – if the element type is comparable, the array is comparable (and arrays of different lengths are different types, so they cannot be compared directly anyway).
Slices, maps, and functions are never comparable. A struct containing a slice field becomes non‑comparable, and you cannot use == on it.
type Person struct {
Name string
Age int
}
p1 := Person{"Alice", 30}
p2 := Person{"Alice", 30}
fmt.Println(p1 == p2) // true, all fields are comparable
If you later add a Tags []string field, the struct becomes non‑comparable, and the compiler will reject p1 == p2. That is a compile‑time safety net: it forces you to think about how you want to define “equal” for that kind of data.
Practical Use in Conditionals
Most comparisons live inside if statements and for loop conditions.
score := 85
if score >= 60 {
fmt.Println("Passed")
}
for i := 0; i < 10; i++ {
// loop body
}
Because the result is a plain bool, you can store it in a variable and use it later, combine it with logical operators, or return it directly from a function.
func isAdult(age int) bool {
return age >= 18
}
Everything is explicit:
Go’s refusal to treat numbers or strings as booleans removes an entire class of bugs. You’ll never accidentally write if x where x is a number, because the compiler will reject it. That explicitness makes Go code easier to read and reason about.
Logical Operators
Logical operators work on boolean values and produce a boolean result. They let you build complex conditions from simple true/false checks.
Go provides three logical operators:
&&— logical AND||— logical OR!— logical NOT
They only accept boolean operands. If you try 1 && 0 or "hello" || "world", the compiler stops with an error.
AND, OR, NOT in Practice
isLoggedIn := true
isAdmin := false
fmt.Println(isLoggedIn && isAdmin) // false
fmt.Println(isLoggedIn || isAdmin) // true
fmt.Println(!isLoggedIn) // false
The AND operator returns true only when both sides are true. The OR operator returns true when at least one side is true. NOT simply flips true to false and vice‑versa.
Often you’ll combine comparison and logical operators in the same line:
age := 25
hasLicense := true
if age >= 18 && hasLicense {
fmt.Println("Allowed to drive")
}
Both conditions must be true for the body to execute.
No implicit conversion:
Languages like JavaScript or Python allow if (x) where x is a string or number. Go does not. The operands of &&, ||, and ! must be explicitly bool. If you have a non‑boolean value, compare it: if count > 0 rather than if count.
Short-Circuit Evaluation
When Go evaluates left && right, it checks left first. If left is false, it doesn’t bother looking at right — the whole expression is already false. Similarly, for left || right, if left is true, right is skipped because the result is already true.
This behavior is called short‑circuit evaluation, and it has practical consequences. You can guard a potentially unsafe operation behind a nil check:
var user *User
if user != nil && user.Name == "Alice" {
// Safe: user.Name is only accessed if user is not nil
}
If user is nil, the first half is false, and the second half never runs. Without short‑circuiting, the program would panic trying to access Name on a nil pointer.
Beware of functions with side effects:
If the right operand is a function call that does something (like logging or modifying state), that function may not run if the left operand already decides the result. This is correct behavior but can be surprising if you aren’t expecting it.
if someCondition && saveToFile() {
// saveToFile() only runs if someCondition is true
}
Always be intentional about functions in logical expressions: if the call is required regardless, run it before the if.
Operator Precedence and Grouping
Among logical operators, ! (NOT) binds tightest, then &&, then ||. This means:
a || b && c // interpreted as: a || (b && c)
Because && has higher precedence than ||, the expression groups as a || (b && c). Relying on this mental math, however, is a recipe for mistakes. Use parentheses to make your intent explicit:
if (age >= 18 && hasLicense) || isSupervisor {
// Clear: either (adult with license) OR supervisor
}
The parentheses cost nothing and save everyone reading the code from having to reconstruct the precedence rules in their head.
Don’t confuse && with & and || with |:
The operators & and | are bitwise operators, not logical operators. They work on integers and manipulate individual bits. Using & when you meant && will compile if both sides are integers, but the result will be an integer, not a boolean. If you later try to use that integer in an if condition, the compiler will reject it because an integer is not a boolean. Always double‑check you’ve typed two ampersands or two pipes.
Common Pattern: Guard Clauses
A function often needs to validate several conditions before performing its main task. Logical operators combined with early returns make this pattern clean and readable.
func processOrder(order *Order) error {
if order == nil || !order.IsPaid {
return errors.New("invalid order")
}
// proceed with shipping
return nil
}
The || ensures that if order is nil, order.IsPaid is never accessed, and we bail out early. That’s a guard clause backed by short‑circuit evaluation.
Short-circuit enables safer code:
When you see if ptr != nil && ptr.field == value, recognize that Go’s short‑circuit evaluation is what keeps the field access safe. This idiom appears throughout Go codebases and is one of the language’s design points that rewards explicit, defensive programming.