AlgoMaster Logo

Nil Pointers

Last Updated: May 22, 2026

High Priority
13 min read

A pointer either holds the address of a real value or it holds nil, which means "no address at all". Reading or writing through a nil pointer crashes the program at runtime, which is a frequent source of runtime panics in Go programs. This lesson covers what nil means for pointers, how to check for it, how nil pointers behave with methods, the typed-nil interface gotcha, and the everyday patterns Go code uses to deal with all of this safely.

Nil Is the Zero Value of Every Pointer Type

When you declare a pointer variable without giving it a value, Go sets it to nil. That's true for every pointer type in the language: *int, *string, *Product, *Order, and so on. There's no other "uninitialized" state for a pointer to be in.

Every one of those declarations produces a pointer with no target. The %v verb prints <nil> for a nil pointer, and comparing the pointer against nil returns true. This is the same zero-value rule as the rest of Go: numbers start at 0, strings start at "", slices and maps start at nil, and pointers start at nil.

The reason this matters is that any pointer variable you haven't explicitly set to point at something is nil. That includes pointer fields inside a struct, pointer elements inside a slice, and pointer return values from a function that didn't construct anything. If you forget about this and try to read through one of those pointers, the program panics.

The Order struct has a Shipping field of type *Address. We never set it, so it's nil. The struct itself is fully usable, and reading o.Shipping gives back a nil pointer rather than crashing. The crash only happens if you then try to follow that pointer, which is what the next section covers.

Dereferencing Nil Panics

The whole point of a pointer is that you can dereference it with *p to read or write the value it points at. If the pointer is nil, there's no value to read, so the program crashes with a runtime panic.

The line p.Code is shorthand for (*p).Code, so Go has to dereference p first. Since p is nil, the runtime catches the bad memory access and turns it into a Go panic instead of a hard crash. The message "invalid memory address or nil pointer dereference" is the one you'll see most often when learning Go.

The same crash happens through any field, even nested ones, and through any operation that follows the pointer:

Here we're trying to write through the nil pointer, not read. Same panic. The runtime doesn't care about the direction; it just can't follow a nil address.

The crucial thing to keep in mind is that the panic happens when the pointer is followed, not when it's compared, copied, or passed around. A nil pointer is a perfectly valid Go value. You can pass it to functions, store it in slices, return it from functions, and compare it to other pointers. The trouble only starts when you try to read or write the value behind it.

Three nil pointers, copied and stored, with no panic in sight. Each one is just a value that happens to be nil.

Comparing Pointers to Nil

The only safe operation on a pointer you might not have set is comparing it against nil. Use p == nil to check if it's unset, and p != nil to check if it points at something.

The if o.Shipping != nil check guards the dereference. When Shipping is unset, the function skips the dereference and prints a fallback message. When it's set, the function follows the pointer safely. This pattern is everywhere in real Go code.

You can also compare two pointers directly. Two pointers compare equal when they point at the same memory address (or when both are nil):

a and b both point at the same book variable, so they're equal. c points at a separate Product value that happens to have the same field contents, but it's a different address, so a == c is false. Pointer equality is about identity, not about field-by-field comparison.

Defensive Nil Checks: When to Bother

A nil check before a dereference is cheap, but adding one to every pointer access clutters the code. The rule of thumb most Go developers follow is to check nil at the boundary, where you might be receiving a pointer you didn't construct yourself.

You should add a nil check when:

  • A function parameter is a pointer and the caller might pass nil.
  • A return value from another function could be nil to signal "not found" or "no value".
  • A struct field is an optional pointer (like *Discount or *Address) that might never be set.
  • You're processing a slice of pointers ([]*Product) where elements might be nil.

You can usually skip the nil check when:

  • You just constructed the pointer in the same function with &, so you know it's not nil.
  • The function's contract explicitly says the pointer must be non-nil, and the caller is local code you control.
  • The pointer comes from a method receiver that the language guarantees is the receiver of a method call.

cartTotal checks c.Discount != nil before reading c.Discount.Percent. If the cart has no discount, the pointer stays nil and the function uses the subtotal as-is. If the cart has a discount, the function follows the pointer and applies the percentage. The check is at the boundary where the function decides whether the optional value is present.

Don't write nil checks "just in case" everywhere. If you just created the pointer two lines above with p := &Product{...}, checking p != nil adds noise without catching anything; & on a struct literal never returns nil. The point of the check is to handle the case where nil is a legitimate value the pointer might hold.

Nil Receivers: Methods on Nil Pointers

A nil pointer can call a method whose receiver is a pointer to that type, and the method body runs normally. The call itself doesn't panic. The panic only happens if the method body tries to dereference the receiver to read a field.

The call c.ItemCount() itself does not panic. Go passes the nil pointer to the method as the receiver. The panic happens inside ItemCount, on the line return len(c.Items), because reading c.Items requires dereferencing c, and c is nil.

That distinction matters because it means you can write methods that handle a nil receiver on purpose. Here's the same type with an ItemCount that doesn't crash:

A nil Cart is treated as a cart with zero items. The method checks for nil first, returns a sensible default, and never tries to read the fields. This is the "nil receiver" pattern.

The diagram shows the two paths a nil-receiver method can take. The call always reaches the method body. From there, an explicit nil check sends the receiver down a safe path that returns a default, while an unchecked field access dereferences nil and panics. Designing the method to take the safe path is what "supporting a nil receiver" means.

A common place this shows up is the String() string method that types define for printing. A nil-safe String returns a sentinel rather than panicking:

fmt.Println calls String() on each value, including the nil one. Because our String checks for nil up front, printing a nil *Address produces <nil> instead of a crash. Many standard library types do exactly this (*url.URL, for example), so it's a pattern worth recognizing.

Not every method should support a nil receiver. Methods that update fields (AddItem, SetPrice) generally shouldn't, because a nil receiver has nowhere to put the new value. Read-only methods that have a meaningful "empty" answer (count, length, list contents) are the natural fit for nil-safe behavior.

Nil Pointers in Struct Fields

A pointer field is the standard way to express "this part of the value is optional". The field is nil when the value isn't present, and points at a real struct when it is.

Both Shipping and Coupon are nil because we didn't set them. Compare that with the same struct where the fields are non-pointer types: every order would carry a zero-valued Address (with empty city and country), and you'd have no clean way to say "no address yet". A nil pointer says "absent" unambiguously.

The cost of this expressiveness is that every place that reads o.Shipping.City has to check for nil first. The check is local and small, but you do have to remember it. Forgetting is one of the most frequent sources of panics in production Go code.

shippingCity is the single place that needs to know how to handle the absent address. Callers don't have to think about it. Packaging the nil check inside a small function is a common way to keep the rest of the program from re-doing the same defensive check at every read site.

A related pattern is using a pointer field to distinguish "set to zero" from "not set". Take a discount percentage: 0 is a legitimate value (no discount), and you may also want to express "the customer hasn't been offered any discount yet". A float64 field can hold 0 for both cases, but a *float64 field can hold nil for "not offered" and &zero for "offered, but worth zero". The price of that extra expressive power is, again, the nil check.

Returning Nil to Signal "Not Found"

A very common Go idiom is for a lookup function to return a pointer plus a bool (or an error), where a nil pointer means the item wasn't found. The caller checks the second return value and avoids dereferencing the pointer in the "not found" case.

The function returns either a pointer to a real product in the catalog plus true, or nil plus false. The caller uses ok to decide whether the pointer is safe to dereference. Notice the loop uses &catalog[i] and not &p from a range variable, which would give a pointer to the loop's local copy. The important detail here is that the returned pointer is either nil or a valid address into the catalog.

Some Go APIs skip the bool and rely on the pointer alone, with the convention that a nil return means "not found". That works too, but the caller still has to check:

The map lookup returns the zero value of the value type when the key is missing. The value type here is *Customer, and its zero value is nil, so a missing key returns nil for free. The caller's job is to check it.

A subtle gotcha lives in that pattern: if the map's value type is a pointer, every missing key returns nil. But if the map contains a real key whose value was explicitly stored as nil, you can't tell those two cases apart from the return value alone. That's when you'd add the second bool return (the value, ok := m[key] form) to distinguish "no such key" from "stored as nil".

The Typed-Nil Interface Gotcha

Here's one of the most famous Go bugs. An interface value compared against nil can be false even when the pointer it carries is nil. The shorthand version is: "a non-nil interface that holds a nil pointer is not equal to nil".

To see why, you need to know how interfaces are stored. An interface value is really two words internally: a type descriptor (which concrete type is in there) and a value (the actual data, often a pointer). An interface is nil only when both words are nil. If the type word is set, even if the value word is a nil pointer, the interface as a whole is not nil.

So far, so good. Both functions explicitly return nil, and the caller's nil check works in both cases. Now watch what happens when a function returns a typed nil pointer wrapped in an interface:

The function returns c, which is a nil *Cart. The return type is Validator, so Go wraps the nil pointer in an interface value. The resulting interface has its type word set to *main.Cart and its value word set to nil. That interface is not equal to nil, because the type word is set. The comparison v == nil returns false, even though the underlying pointer is nil.

The diagram shows the two-word structure of an interface value. Wrapping a nil *Cart in an interface leaves the value word nil but fills in the type word with *main.Cart. The == nil comparison only returns true when both words are nil, so a typed-nil pointer wrapped in an interface compares unequal to nil.

A common version of this bug looks like:

The author meant for getValidator(false) to return "no validator", and the caller's if v == nil to skip validation. But the function returns a typed nil *Cart wrapped in an interface, so v == nil is false, and the program calls v.Validate() on a nil receiver. In this case the method is nil-safe so it happens to work, but the intended early return never fires. With a method that's not nil-safe, the same shape would panic.

The fix is to return an untyped nil whenever you want the interface to compare equal to nil:

By writing return nil directly (instead of returning a typed pointer variable that happens to be nil), the function returns an interface with both words nil, and the caller's check works the way you'd expect. The rule of thumb: when a function returns an interface and you want "absent" to compare equal to nil at the caller, return the untyped nil literal, not a typed nil variable.

Nil Pointer vs Nil Slice vs Nil Map vs Nil Interface

The word nil shows up in several places in Go, and they're related but not identical. A quick reference so you don't mix them up:

TypeZero valueSafe to read?Safe to write?Notes
*T (pointer)nilDereferencing panicsWriting panicsThis is the pointers section
[]T (slice)nillen and range are safe, indexing panicsappend is safe and returns a new sliceSlice semantics
map[K]V (map)nillen and lookup are safe (return zero value)Writing panicsMap semantics
interface{...}nilMethod calls on nil interface panicn/aThe typed-nil gotcha lives here

A nil pointer, a nil slice, and a nil map share the same word nil but differ in what's safe to do with them. You can call len on a nil slice or nil map without trouble; you cannot dereference a nil pointer without panicking. You can append to a nil slice but you cannot write to a nil map. The behavior is part of each type's contract.

Reading a nil slice's length, looking up a key in a nil map, and appending to a nil slice all work. Compare that to:

Writing to a nil map panics. The other operations are safe; this one isn't. Each "nil" has its own rules, which is worth keeping straight because the error messages differ (assignment to entry in nil map vs invalid memory address or nil pointer dereference), and the fixes differ too. For a nil map you need to make it first; for a nil pointer you need to set it to point at a real value.

A nil interface is a fourth case, and it's the one this lesson dwells on because of the typed-nil gotcha. A nil interface (var v Validator) is two nil words. A typed-nil interface (an interface holding a nil pointer of a concrete type) is one set word plus one nil word, and it does not compare equal to nil. That's a trap unique to interfaces.

Common Defensive Patterns in Real Go Code

Real Go code uses a small set of recurring patterns to deal with nil. Recognizing them makes reading other people's code much easier, and using them keeps your own code idiomatic.

The early-return guard. When a function takes a pointer that might be nil, the very first thing it does is check and return:

The guard puts the nil branch at the top, so the rest of the function can assume the pointer is non-nil. It's easier to read than nesting the body inside an if a != nil { ... } block.

The accessor with a sensible default. Wrap a nil-prone field lookup in a small function that returns a safe value:

Callers ask cart.DiscountPercent() and don't have to think about the nil. Wrapping the check in the method keeps the defensive code in one place instead of scattering it across every caller.

Lazy construction. A method that needs to mutate a pointer field can construct the value on first use if it's still nil:

The first call to Add sees a nil Wishlist and creates one. Subsequent calls see a non-nil pointer and just update the existing map. The caller doesn't need to remember to initialize the wishlist before adding items.

Nil-safe methods on pointer receivers. Methods can check if receiver == nil and return a default. This is most useful for read-only methods where the empty answer makes sense (count, length, summary string).

Returning the untyped nil literal. When a function's return type is an interface and you want the caller's == nil check to work, write return nil (the literal) rather than return someTypedNilPointer. This avoids the typed-nil interface trap.

These patterns combine in real code. A typical service function might check a nil parameter at the top, return nil to signal "not found", store optional sub-values in pointer fields, and use accessor methods to read those fields safely. Each piece is small. Together they keep nil from being a runtime surprise.