Right, let’s get our hands dirty with what an interface value actually is under the hood. This is where the magic happens, and where most of the confusion comes from. It’s also where you’ll stop being afraid of them and start wielding them like a pro.

Think of an interface variable not as a thing itself, but as a container, a pair of glasses. It has two components, and you must understand both to see the whole picture:

  1. The Dynamic Type: This is the concrete type of the value that’s currently stored inside the interface. It’s the “what is it really?” part.
  2. The Dynamic Value: This is the actual value of that concrete type, currently living inside the interface. It’s the “what are its actual bits?” part.

When you declare a variable of an interface type (e.g., var w io.Writer), both parts are initially nil. It’s an empty container. The type is unknown and the value is nothing.

var w io.Writer // Both dynamic type AND dynamic value are nil
fmt.Println(w == nil) // true

The moment you assign a concrete value to it, both parts get populated. The container now holds something specific.

w = os.Stdout          // Dynamic type: *os.File, Dynamic value: the os.Stdout value
w = new(bytes.Buffer)  // Dynamic type: *bytes.Buffer, Dynamic value: a pointer to a new buffer

This duality is why an interface value can only be nil if both parts are nil. This is the source of a classic Go gotcha. Let’s say you have a function that returns an error interface.

func returnsANilError() error {
    var p *MyErrorType = nil // p is a nil pointer to MyErrorType
    return p                 // We're returning a nil pointer... right?
}

err := returnsANilError()
fmt.Println(err == nil) // false. Wait, what?!

Why is this false? Because the interface value err now has a dynamic type of *MyErrorType and a dynamic value of nil. The container knows what type of nil it’s holding. Since its type component is populated, the interface itself is not nil. This will break your standard if err != nil check. It’s infuriating until you understand the mechanics, and then it’s just mildly annoying. The fix is almost always to return nil directly instead of a typed nil value.

Interrogating the Dynamic Duo with Type Assertions

So how do you peek inside the container to see what’s really there? You use a type assertion. It’s your way of saying, “Hey, interface, I think you’re holding a *os.File. Let me see it, and if you’re not, we’re going to have a problem.”

There are two forms. The safe form:

var myInterface io.Writer = os.Stdout

f, ok := myInterface.(*os.File) // f is now the *os.File, ok is a bool
if ok {
    fmt.Printf("Yep, it's a file: %v\n", f)
} else {
    fmt.Println("Nope, not a file.")
}

And the “panic if you’re wrong” form:

f := myInterface.(*os.File) // if myInterface is not a *os.File, this will panic
fmt.Printf("It's definitely a file: %v\n", f)

Use the first one 99% of the time. The second one is for when you are absolutely, cosmically certain, and even then, think twice.

The Empty Interface: interface{} (or any)

This is a special case worth calling out. The empty interface has no methods. This means every single type in Go satisfies it, because every type has at least zero methods. It’s the ultimate container.

var anything any // any is a built-in alias for interface{}
anything = 42
anything = "hello"
anything = map[string]int{"answer": 42}

When you have an any, the dynamic type and value work exactly the same way. To get your useful data back out, you must use a type assertion to a concrete type (or a type switch, which is just a fancy way of doing multiple type assertions). An any holding a value is very rarely useful on its own; its power is in being able to store anything before you later retrieve it with a type assertion.

The One Rule to Rule Them All

Here’s the core principle that governs all of this: You can only call methods on an interface value that are defined in its interface type, but the method that actually gets executed is the method of the dynamic value. This is the essence of polymorphism.

The interface io.Writer only lets you call Write(). You can’t call Close() on it, even if the dynamic value is an *os.File which has a Close() method. The interface container is hiding everything except the methods it promises. To access those other methods, you must first use a type assertion to get the concrete value back out.

This system is why Go’s interfaces are so powerful and implicit. It’s all about what you do, not what you are. If your concrete type has the right methods, it fits. The interface value is just the well-defined, predictable holder that makes this safe and organized.