Pointer Comparison

How to compare pointers in Go using equality operators, check for nil pointers, and understand the limitations and practical uses of pointer comparisons.

Comparing pointers means checking whether two pointer variables hold the same memory address. Go gives you the == and != operators for this. If two pointers point to the exact same location in memory, they are equal. If they point to different locations, they are not. A pointer that stores nothing — a nil pointer — is equal to another nil pointer.

The concept is simple, but using it correctly depends on understanding what you are actually comparing: the address stored inside the pointer variable, not the value sitting at that address.

Comparing Pointers with == and !=

The == operator returns true when both pointers hold the same address or when both are nil. The != operator returns the opposite. Both pointers must have the same base type — comparing a *int to a *string is a compile-time error.

Here is a straightforward example that shows equality in action.

package main
import "fmt"
func main() {
    x := 100
    y := 200
    p1 := &x   // points to x
    p2 := &y   // points to y
    p3 := &x   // also points to x
    fmt.Println("p1 == p2:", p1 == p2) // false, different addresses
    fmt.Println("p1 == p3:", p1 == p3) // true, same address (both point to x)
    fmt.Println("p2 != p3:", p2 != p3) // true, different addresses
}

Run this and you will see false, true, true. The first comparison fails because p1 holds the address of x and p2 holds the address of y — those are two separate integers in memory, so their addresses differ. The second comparison succeeds because p1 and p3 both store the address of x. The third uses != to confirm that p2 and p3 reference different locations.

A subtle point that trips up many newcomers is the difference between comparing the pointer value itself and comparing the address of the pointer variable.

var a, b int
pa := &a
pb := &b
fmt.Println(pa == pb)   // false: a and b are different variables
fmt.Println(&pa == &pb) // false: pa and pb are different pointer variables

Even if pa and pb pointed to the same integer, &pa == &pb would still be false because pa and pb are two distinct local variables on the stack, each with its own address. Asking “are these two pointer variables stored at the same address?” is almost never what you want. Stick with comparing the pointer values themselves.

Type Mismatch:

Comparing pointers of different types (for example, a *int and a *float64) causes a compile-time error. The Go compiler enforces that both operands of == or != have identical pointer types.

Comparing Pointers to nil

A pointer’s zero value is nil. It means “I point to nothing.” Checking whether a pointer is nil is the standard way to decide whether it is safe to dereference.

func printValue(p *int) {
    if p != nil {
        fmt.Println("Value:", *p)
    } else {
        fmt.Println("Pointer is nil, nothing to print.")
    }
}

Call printValue(nil) and it prints the fallback message. Call it with &someInt and it dereferences safely. This pattern appears in nearly every Go codebase that works with optional values, partially populated structs, or function return values that may indicate absence.

When you declare a pointer variable without initialising it, it is nil.

var ptr *int
fmt.Println(ptr == nil) // true

A pointer returned by new is never nilnew(int) allocates a zeroed integer and returns its address. A map lookup for a pointer-typed value, however, yields the zero value of that pointer type, which is nil if the key is not present. Both of these behaviours matter in everyday Go.

Nil Dereference Panics:

Accessing *p when p is nil triggers a runtime panic. Always guard dereferences with a nil check when there is any possibility the pointer could be nil.

Safe Access:

When a pointer passes a != nil check, the memory behind it is guaranteed to be valid at that moment. There is no dangling-pointer risk from the garbage collector — Go keeps the allocation alive as long as a live pointer references it.

Why Go Does Not Allow Ordering Comparisons on Pointers

You might be coming from C or C++, where comparing pointer addresses with < and > is allowed and sometimes used to walk arrays or implement data structures. In Go, the operators <, \u003c=, \u003e, and \u003e= are not defined on pointer types. The compiler will reject p1 < p2 with a clear error.

The reason is that Go’s runtime can move memory around. The garbage collector may relocate objects during collection, which means a pointer’s numeric address is not stable in a way that ordering could rely on. Two pointers that compare as one ordering before a garbage collection cycle could compare differently afterward. Rather than offer an operator that would be inherently unreliable, Go prohibits it entirely.

If you absolutely must obtain a numeric representation of a pointer for comparison, the unsafe package gives you a way out — but you must understand the tradeoffs.

import (
    "fmt"
    "unsafe"
)
func main() {
    a, b := 1, 2
    pa, pb := &a, &b
    addrA := uintptr(unsafe.Pointer(pa))
    addrB := uintptr(unsafe.Pointer(pb))
    if addrA < addrB {
        fmt.Println("Address of a is numerically smaller than address of b")
    } else {
        fmt.Println("Address of a is numerically larger than or equal to address of b")
    }
}

This compiles and prints a result based on the numeric addresses at that instant, but the ordering is meaningless for any persistent logic. A subsequent garbage collection cycle can change the relative positions of a and b, making the comparison stale. Using unsafe also bypasses Go’s type safety and memory guarantees — the garbage collector will not update the uintptr values if the objects move, so holding onto them creates a risk of dangling references.

Do Not Rely on Pointer Ordering:

Pointer ordering via unsafe is brittle and unreliable. It is unsuitable for hash functions, tree balancing, sorting, or any logic that expects the ordering to remain stable over time. Use it only for debugging or temporary inspection.

Practical Uses of Pointer Comparison

Even though you can only test equality, pointer comparison shows up regularly in real Go programs.

Sentinel values. A function can return a pointer to a known global variable to indicate a specific state. Compare the returned pointer to the address of that global to detect the state.

var sentinel int
func findValue(slice []int, target int) *int {
    for i := range slice {
        if slice[i] == target {
            return &slice[i]
        }
    }
    return &sentinel
}

Callers check if ptr == &sentinel to see if nothing was found. This is identity, not a deep comparison of the integer value inside.

Detecting the same object. When two variables should reference the same underlying struct — for example, a linked list node or a cache entry — pointer equality tells you whether they literally point to the same allocation.

type Node struct {
    Value int
    Next  *Node
}
func sameNode(a, b *Node) bool {
    return a == b
}

If you need to know whether two nodes contain the same data, you would compare the Value fields. If you need to know whether they are the exact same node (important when modifying the list structure), you compare the pointers.

Nil checks before interface assertions. When an interface holds a pointer, comparing to nil can be nuanced, but a plain nil check on a *T directly before wrapping it in an interface still relies on pointer comparison to nil.

Pointer comparison is a building block. It does not replace deep equality or structural comparison; it answers the single question “are these two entities the same location in memory?”

Common Mistakes

Comparing the wrong thing. Writing *p1 == *p2 compares the values that the pointers refer to, not the pointers themselves. That is a valid comparison but a completely different operation. It checks equality of two integers, strings, or structs, not address identity. Decide which one you need.

Value vs. Pointer Comparison:

p1 == p2 is pointer identity (same address). *p1 == *p2 is value equality (same data at the addresses). Mixing them up leads to logic bugs that compile fine but behave unexpectedly.

Thinking pointer addresses never change. A pointer holds an address that is valid as long as the allocation lives, but the numeric value of that address can change after a garbage collection cycle. This does not affect equality comparisons — if two pointers pointed to the same object before collection, they still point to it afterward. But it does mean you cannot stash a uintptr away and expect it to remain usable.

Ignoring nil when comparing pointers from function returns. If a function returns *T and can signal absence with nil, you must compare the result to nil before using it. Skipping this check is one of the most common sources of panics in new Go code.

Trying to use < or >. The compiler forbids it, so you will spot this quickly, but the reflex from other languages is common. If you find yourself wanting pointer ordering for a tree or map, ask whether you can use a slice or a map keyed by a meaningful field instead.

Summary

Pointer comparison in Go is deliberately narrow: == and != only, type-safe, and reliable for identity checks and nil guards. This restriction protects you from the instability of raw address ordering that a moving garbage collector would cause, while still giving you the tool needed to check if two pointers reference the same object or if a pointer is safe to follow. The nil check is the gateway to all safe dereferencing.

With this understanding, pointer receivers in methods let you modify the value a pointer points to — the *T receiver syntax builds directly on the pointer concepts covered here.