JavaRush /Courses /Go SELF /Interface value: (type, value)

Interface value: (type, value)

Go SELF
Level 24, Lesson 1
Available

1. What is an interface value

Beginners usually want to think of an interface as a “box” where you can put different types, then call methods, and that’s it. That’s a normal desire: the brain already has a hard enough time. But it’s exactly because of this “box” mental model that the most annoying bugs appear—when you checked err == nil, the logic went down the “no error” branch, yet the program still behaves as if there was an error. Or the other way around: err != nil, but you print err and see <nil>, and you start suspecting Go is trolling you.

The problem is not Go and not your luck. The problem is that an interface value consists of two parts. And nil for an interface is not the same as nil for a pointer.

Two parts of an interface: type and value

When we write var x error or var s fmt.Stringer, that’s an interface variable. And at runtime it stores two things: which concrete type is inside and which concrete value of that type is inside.

You can think of it as a pair:

  • dynamic type (for example, *ValidationError)
  • dynamic value (for example, “some address,” or nil)

Important: the interface type (what’s written on the left: error, Stringer, Greeter) is a contract. And the interface value (a variable of that type) is a container that remembers what’s inside.

A handy “picture in your head”:

interface value
+-----------------------------+
| dynamic type  | dynamic val |
+-----------------------------+

If a variable has neither a type nor a value, that’s a “real nil interface.”

Three kinds of nil people constantly confuse

Most confusion starts because the word nil is one word, but there are multiple situations. In Go it’s especially important to distinguish a “nil pointer” from a “nil interface.”

Let’s summarize this in a table:

Situation Example What’s inside the interface == nil
Nil interface (the interface itself is empty)
var err error = nil
type: absent, value: absent
true
Nil pointer (a regular pointer nil)
var p *T = nil
this is not an interface at all true (for p)
Typed nil in an interface (type exists, value is nil)
var p *T = nil; var err error = p
type: *T, value: nil false (for err)

And that third row is our main character. That is typed nil: inside the interface there is nil, but “with a passport” (with a concrete type).

2. Demonstrations in code

Nil interface and %T

Now we’ll write a small piece of code that simply prints “what’s inside.” We already know how to use fmt.Printf to diagnose types and values—this is exactly the case where formatting isn’t decoration, it’s a flashlight in a dark basement.

package main

import "fmt"

type Greeter interface {
	Greet() string
}

func main() {
	var g Greeter
	fmt.Println(g == nil) // true
	fmt.Printf("%T\n", g) // <nil>
}

Here g is a real nil interface: no dynamic type and no value.

Typed nil: the interface is “not nil,” even though nil is inside

Now let’s make the classic example: put a nil pointer inside an interface.

package main

import "fmt"

type Person struct {
	Name string
}

func (p *Person) String() string {
	if p == nil {
		return "<nil Person>"
	}
	return "Person(" + p.Name + ")"
}

func main() {
	var p *Person = nil

	var s fmt.Stringer = p
	fmt.Println(p == nil) // true
	fmt.Println(s == nil) // false

	fmt.Printf("%T %v\n", s, s) // *main.Person <nil Person>
}

Why is s == nil false? Because inside s the dynamic type *Person is stored. The value is nil, but the type exists. And the check iface == nil checks both parts at once: both the type and the value.

Notice the method String(): we made it “nil-safe” so printing is safe. This is not a mandatory language rule, but a deliberate defense: if a method with a pointer receiver is called on nil, you must be careful inside the method.

3. The error != nil trap

The most painful case: returning an error

The most common scenario where people step on typed nil is returning an error. Because we have an unwritten contract: a nil error means success. And a huge part of Go code relies on this: we constantly compare an error with nil.

Now imagine you wrote your own error as a struct and return a pointer to it.

package main

import "fmt"

type ValidationError struct {
	Field string
	Msg   string
}

// Important: make the method nil-safe, otherwise printing/logging may panic.
func (e *ValidationError) Error() string {
	if e == nil {
		return "<nil ValidationError>"
	}
	return e.Field + ": " + e.Msg
}

func validateTitle(title string) error {
	if title == "" {
		return &ValidationError{Field: "title", Msg: "empty"}
	}

	var e *ValidationError = nil
	return e // DANGEROUS: typed nil is packaged into error
}

func main() {
	err := validateTitle("hello")
	fmt.Println(err == nil)        // false (and that's unexpected)
	fmt.Printf("%T %v\n", err, err) // *main.ValidationError <nil ValidationError>
}

If your inner voice is saying “wait, but it’s nil!”, then you’ve caught the essence of the trap.

nil is the value of the pointer e, but what we return is the interface error. When it’s boxed into the interface, a dynamic type *ValidationError appears, and the interface stops being nil.

The correct way: return a “real nil interface”

The fix is usually simple and a bit… annoying: don’t return the variable e if it is a pointer-nil. You must return exactly nil as an error.

package main

type ValidationError struct {
	Field string
	Msg   string
}

func (e *ValidationError) Error() string {
	if e == nil {
		return "<nil ValidationError>"
	}
	return e.Field + ": " + e.Msg
}

func validateTitle(title string) error {
	if title == "" {
		return &ValidationError{Field: "title", Msg: "empty"}
	}
	return nil // IMPORTANT: nil interface, with no dynamic type
}

func main() {}

Here return nil creates an “empty” interface: with no dynamic type and no value. And then err == nil behaves as expected.

4. Example: a task manager and a typed nil bug

So this doesn’t stay abstract, let’s imagine we’re continuing a mini app for tasks (a hypothetical CLI task manager). We have a Task model and a function that creates a task and validates the input.

In previous topics we already got used to keeping validation close to the boundary (where we create the entity). Now it’s important for us to ensure the contract “nil = success” doesn’t break.

A good variant: either an error, or nil

package main

import "fmt"

type Task struct {
	Title string
}

type ValidationError struct {
	Field string
	Msg   string
}

func (e *ValidationError) Error() string {
	if e == nil {
		return "<nil ValidationError>"
	}
	return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}

func NewTask(title string) (*Task, error) {
	if title == "" {
		return nil, &ValidationError{Field: "title", Msg: "empty"}
	}
	return &Task{Title: title}, nil
}

func main() {
	t, err := NewTask("buy milk")
	fmt.Println(t.Title, err == nil) // buy milk true
}

This is a good variant: we either return a concrete error, or we return nil as the error interface.

How easy it is to “break the contract” by introducing an error variable

Now we’ll show how easy it is to ruin everything if you try to make the code “prettier” and introduce a variable for the error:

package main

type ValidationError struct {
	Field string
	Msg   string
}

func (e *ValidationError) Error() string {
	if e == nil {
		return "<nil ValidationError>"
	}
	return e.Field + ": " + e.Msg
}

func NewTask(title string) error {
	var verr *ValidationError

	if title == "" {
		verr = &ValidationError{Field: "title", Msg: "empty"}
	}

	return verr // typed nil if title is not empty
}

func main() {}

This code looks “logical”: if there’s no error, verr stays nil. But what we return is not *ValidationError, but error. And we get typed nil.

If you use it like this:

err := NewTask("ok")
if err != nil {
	// you will unexpectedly end up here
}

…then the app will start behaving as if “there is always an error.”

5. Diagnosing typed nil

A quick test: print (%T, %v)

When you suspect typed nil, there’s a simple test: print (%T, %v).

The idea is: if err != nil, but fmt.Printf("%T", err) shows *SomeType, and at the same time the value looks “like nil,” then you almost certainly boxed a nil pointer into an interface.

A mini helper you can temporarily keep in code:

package main

import "fmt"

func debugInterface(name string, x any) {
	fmt.Printf("%s: type=%T, value=%v\n", name, x, x)
}

func main() {
	var p *int = nil
	debugInterface("p", p) // p: type=*int, value=<nil>
}

Yes, any is also an interface (just “the most general” one), but here we’re using it only as a convenient diagnostic container, not trying to build architecture around it.

Why printing sometimes shows <nil>, but comparison says “not nil”

This is the point where many people think “the compiler is broken.” In reality, these are just different questions.

Printing a value often prints the dynamic value. If it’s nil, you’ll see <nil> or something similar.

But comparing iface == nil answers the question: “is there nothing in the interface at all?” That is, is there neither a type nor a value. With typed nil, there is a type, so the answer is “no, the interface is not empty.”

The difficulty is that to a human, “emptiness” feels the same. For the Go runtime, these are different states.

6. Practical rules

Two rules that actually save a project

In interface topics you often see “abstract advice” that sounds nice but doesn’t help in practice. Here the advice is as practical as possible—things you can apply tomorrow and the trap stops showing up.

The first rule sounds boring, but it works: if a function returns an interface (error, fmt.Stringer, your Store), then in the “nothing there” branch, return the literal nil, not a variable that “should be nil.” This is especially important when the variable has a concrete type *T.

The second rule is like “turn on the flashlight”: when you see strange behavior like x != nil, print "%T". Type is half of an interface. Until you see it, it’s like you’re trying to debug the box without opening the lid.

error is a regular interface

Today’s trap becomes especially nasty because error is not magic, but an ordinary interface:

type error interface {
	Error() string
}

And that’s exactly why any type that has the method Error() string can be an error.
That’s very powerful: you can create rich errors with context, fields, and logic, and code will still work with them as a contract.

But “powerful” almost always means “you can shoot yourself in the foot.” Typed nil is one of those shots: formally everything is correct, but your expectations about control flow break.

7. Common mistakes

Mistake #1: returning (*T)(nil) as an interface and expecting == nil.
This often appears when you store an error in a variable of a concrete pointer type (for example, *ValidationError) and return it as error. If the variable remained nil, the interface can still become “not nil” due to the dynamic type. The cure is the habit of returning return nil on the success path, rather than return somePtr.

Mistake #2: checking the “wrong nil.”
Sometimes code checks if p == nil and then uses iface, or vice versa. It’s like “I checked the door, but entered through the window.” If you work with an interface, check the interface; if you work with a pointer, check the pointer. And if you see weirdness, print %T to understand what you’re actually checking.

Mistake #3: trying to initialize an error “nicely” via a variable without thinking about the return type.
The pattern “declare var err *MyError, then return err” looks neat until the function returns an interface. In the interface world, “neatness” must include the thought “will I box a typed nil?”

Mistake #4: assuming fmt.Println(err) is a diagnostic for nil.
Printing mostly shows the dynamic value, not whether the interface is “empty.” So you can get <nil> on screen and err != nil in logic. If you suspect typed nil, print fmt.Printf("%T", err) and compare err == nil.

Mistake #5: implicitly relying on the contract nil = success, but violating it in your own functions.
Go code around you assumes a nil error means success. If your function returns “not nil” in the successful case, you’ll get a cascade of weird effects: extra early returns, incorrect messages, extra if branches. Better to get return nil right once than to heroically fix the consequences all over the code later.

1
Task
Go SELF, level 24, lesson 1
Locked
The Invisible Employee
The Invisible Employee
1
Task
Go SELF, level 24, lesson 1
Locked
The ghost's business card
The ghost's business card
1
Task
Go SELF, level 24, lesson 1
Locked
Login validation
Login validation
1
Task
Go SELF, level 24, lesson 1
Locked
Resource provisioning
Resource provisioning
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION