Comparison Operators

A complete guide to Go's six comparison operators, covering assignability rules, comparable vs ordered types, deep comparison of structs and arrays, interface equality, and common pitfalls beginners face.

Comparison operators in Go produce a boolean result — true or false — by examining the relationship between two values. They are the building blocks of conditional logic, used everywhere from if statements to loop conditions to map key comparisons. Unlike some languages, Go has strict rules about which types can be compared and in what ways.

The Six Operators

Go provides six comparison operators, split into two groups based on the types they support.

OperatorNameOperand Requirement
==equalcomparable types
!=not equalcomparable types
<lessordered types
<=less or equalordered types
>greaterordered types
>=greater or equalordered types

All comparison operators yield an untyped boolean constant. Because it is untyped, the result can be assigned to any boolean variable regardless of its specific named type.

package main
import "fmt"
type Status bool
func main() {
    var s Status
    s = 5 > 3   // untyped boolean constant true is assignable to Status
    fmt.Println(s) // true
}

The untyped nature removes the need for explicit casts when using comparisons in conditional expressions. However, if you assign a variable of type bool to a Status, you will get a compile error — untyped constants are more flexible than typed values.

Untyped booleans are not the same as bool:

The result of a comparison is an untyped boolean constant, not a variable of type bool. This distinction matters when working with custom boolean types. An untyped constant will happily become any named boolean type; a bool variable will not.

The Assignability Rule

Before any comparison takes place, one operand must be assignable to the type of the other operand. The direction does not matter — it can be the left operand assignable to the right, or the right to the left. If neither direction works, the compiler rejects the comparison.

Consider this example:

type ID int
func main() {
    var x ID = 10
    fmt.Println(x == 10)   // valid: untyped 10 is assignable to ID
    fmt.Println(10 == x)   // valid: x is assignable to int? No, but untyped 10 works both ways
}

The constant 10 is untyped, so it is assignable to ID. The reverse — assigning x (of type ID) to the constant — is impossible because you cannot assign to a constant. However, Go's rule only requires one direction to work, so both comparisons succeed.

Now look at a case that fails:

var a int = 5
var b rune = '5'
fmt.Println(a == b) // compile error: mismatched types int and rune

Neither int is assignable to rune nor rune to int without a conversion, so the comparison is illegal. This is a deliberate design choice that catches accidental type mixing early.

Compile-time type mismatch:

Go will refuse to compile any comparison between fundamentally different numeric types (int, float64, rune, etc.) without an explicit conversion. This is not a runtime warning — it is a hard error. You must convert one operand explicitly.

Equality Operators (== and !=)

Equality operators require the operands to be comparable. A type is comparable if Go's specification defines it as such; otherwise, == and != will produce a compile-time error.

Comparable types include all basic types (booleans, integers, floats, complex numbers, strings), pointers, channels, interfaces, structs whose fields are all comparable, and arrays whose element type is comparable.

Booleans

Boolean values are compared straightforwardly: true == true is true, false != true is true. No hidden conversions exist.

Integers, Floats, and Complex Numbers

All integer and floating-point types are comparable. Two complex numbers are equal if both their real and imaginary parts are equal. There is no automatic widening — int and float64 cannot be compared directly; one must be converted.

var i int = 42
var f float64 = 42.0
fmt.Println(float64(i) == f) // true after explicit conversion

Floating-point comparisons come with the same precision caveats present in all languages.

Floating-point equality is unreliable:

Due to IEEE 754 representation, two floating-point numbers that are mathematically equal may not compare equal in Go. Avoid direct == on floats derived from arithmetic. Use a tolerance threshold instead.

Strings

Strings are comparable and also ordered. Equality checks whether the byte sequences match exactly.

fmt.Println("hello" == "hello") // true
fmt.Println("hello" == "Hello") // false

Pointers

Two pointers are equal if they point to the same variable or if both are nil. Equality does not compare the pointed-to values — it checks the addresses.

a := 10
b := 10
p1 := &a
p2 := &a
p3 := &b
fmt.Println(p1 == p2) // true (same variable)
fmt.Println(p1 == p3) // false (different variables, even though both hold 10)

A subtlety: distinct zero-size variables may share the same memory address, so comparing pointers to such variables yields unpredictable results.

var x [0]int
var y [0]int
fmt.Println(&x == &y) // may be true or false depending on the compiler

Do not rely on pointer equality of zero-size objects:

The language specification explicitly states that distinct zero-size variables may have identical addresses. Code that depends on their pointer equality is non-portable.

Channels

Channel values are comparable by identity, not by type or content. Two channel values are equal if they were created by the same make call or if both are nil.

ch1 := make(chan int)
ch2 := make(chan int)
ch3 := ch1
fmt.Println(ch1 == ch2) // false
fmt.Println(ch1 == ch3) // true

Interfaces

Two interface values are equal if both are nil or if their dynamic types are identical and their dynamic values are equal. If the dynamic types are not comparable (e.g., a map or slice), a runtime panic occurs.

var a interface{} = "hello"
var b interface{} = "hello"
fmt.Println(a == b) // true — dynamic type string is comparable

Now an example that panics:

var c interface{} = map[string]int{"k": 1}
var d interface{} = map[string]int{"k": 1}
// fmt.Println(c == d) // runtime panic: comparing uncomparable type map[string]int

Interface equality may panic at runtime:

If an interface value holds a dynamic type that is not itself comparable (maps, slices, functions), comparing the interface values will cause a runtime panic. Go does not warn at compile time because the dynamic type is only known at runtime.

Comparing a non-interface value with an interface value is allowed if the non-interface type implements the interface and is comparable. The comparison succeeds if the interface's dynamic type matches and the values are equal.

type Speaker interface {
    Speak()
}
type Person struct{ Name string }
func (p Person) Speak() {}
func main() {
    var s Speaker = Person{"Alice"}
    p := Person{"Alice"}
    fmt.Println(p == s) // true
}

Structs

A struct is comparable if all its fields are comparable. Two struct values are equal if all corresponding non-blank fields are equal. Fields are compared in source order, and comparison stops as soon as a mismatch is found.

type Point struct {
    X, Y int
    _    string // blank field, ignored in comparison
}
func main() {
    p1 := Point{X: 1, Y: 2, _: "a"}
    p2 := Point{X: 1, Y: 2, _: "b"}
    fmt.Println(p1 == p2) // true, blank field ignored
}

If any field has a non-comparable type (like a slice), the struct itself becomes non-comparable.

One uncomparable field makes the whole struct uncomparable:

Adding a slice, map, or function field to a struct renders the struct unusable as a map key and prevents direct == comparison. This is a common surprise when evolving a struct that was previously comparable.

Arrays

Arrays are comparable if their element type is comparable. Two arrays are equal if all corresponding elements are equal. Array length is part of the type — a [3]int and a [4]int cannot be compared because they are distinct types.

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false

Types That Are Not Comparable

Slices, maps, and functions are not comparable with ==, except against nil. Any attempt to use == between two non-nil values of these types results in a compile error.

var s1 []int
var s2 []int
fmt.Println(s1 == nil) // true
// fmt.Println(s1 == s2) // compile error: invalid operation: s1 == s2 (slice can only be compared to nil)

Ordering Operators (<, <=, >, >=)

Ordering operators apply to ordered types: integers, floating-point numbers, and strings. Booleans, complex numbers, pointers, channels, interfaces, structs, and arrays are not ordered — they do not support < or similar operators.

Integer and Float Ordering

Works as expected across all integer and float types (after type matching). The compiler enforces operand type uniformity just as with equality.

String Ordering

Strings are compared lexicographically, byte by byte, using the Unicode code point values of each byte. This is not full linguistic collation — it is a raw comparison of the UTF‑8 byte sequences.

fmt.Println("abc" < "abd")   // true
fmt.Println("a" < "A")       // false ('a' 97 > 'A' 65)
fmt.Println("aaaa" < "b")    // true, because 'a' < 'b'

This byte-wise ordering means strings with non-ASCII characters are sorted by their UTF‑8 encoding, which may not correspond to human alphabetical order in all languages.

String ordering is not culture-aware:

Go's default string comparison uses bytewise order, not locale-specific collation. If you need linguistic sorting, use the golang.org/x/text/collate package.

Comparison Across Types (Interface and Concrete)

Go allows comparing a non-interface value with an interface value under specific conditions. The non-interface type must implement the interface, and the type must be comparable. The comparison evaluates to true if the interface's dynamic type matches and the dynamic value equals the concrete value.

type Describable interface {
    Desc() string
}
type Item struct{ ID int }
func (it Item) Desc() string { return "item" }
func main() {
    var d Describable = Item{10}
    x := Item{10}
    y := Item{20}
    fmt.Println(x == d) // true
    fmt.Println(y == d) // false
}

This only works when the concrete value is on the left side of ==. The reverse — d == x — also works because the same assignability rule permits it.

Concrete‑to‑interface equality is safe when types are comparable:

If you have an interface value that you know holds a comparable concrete type (like a struct with all comparable fields), direct equality with a concrete value works and does not panic. This is a deliberate design point that makes interface‑based abstractions easier to work with.

Comparison and Map Keys

Any type that is comparable can be used as a map key. This means structs with all comparable fields, arrays of comparable elements, pointers, interfaces (with care), and all basic types are eligible. Slices, maps, and functions are not.

This directly ties into the comparability rules: if a type cannot be compared with ==, it cannot be a key.

Common Pitfalls

Floating-point equality

Repeating the warning: direct == on floats is unreliable. Instead, check that the absolute difference is below a threshold.

const epsilon = 1e-9
a := 0.1 + 0.2
b := 0.3
if math.Abs(a-b) < epsilon {
    // considered equal
}

Struct fields that silently break comparability

Adding a slice field to a struct that was previously used as a map key will break compilation at every key usage site. This is a maintenance hazard.

Nil comparisons on interfaces

An interface value that is not nil can still contain a nil concrete pointer. Comparing such an interface to another interface can be confusing. The equality rules handle this correctly, but the mental model takes practice.

Zero-size pointer unpredictability

As shown earlier, pointer equality on zero-size variables is implementation‑dependent. Never rely on it.

Summary

Comparison operators in Go are split into equality (==, !=) and ordering (<, <=, >, >=) groups, each with distinct type requirements. The assignability rule ensures that you cannot accidentally compare unrelated types. Comparable types span basic types, pointers, channels, interfaces, structs with comparable fields, and arrays of comparable elements — but exclude slices, maps, and functions except against nil.

The most important concept to internalize is the difference between comparable and ordered. A type can be comparable without being ordered (e.g., structs, pointers, complex numbers), and some types are ordered only if they are numeric or string. The strict separation eliminates entire categories of bugs common in languages that allow implicit coercion.