Go Secret — Interface{}: Nil is not Nil
We’ll see how it can keep different types of values, look at what’s inside it, and find out how it checks if two things are the same.
In this section, we will delve deeper into the interface{}
type and gain a comprehensive understanding of how it operates.
Secret series:
Go Secret — Defer: What you know about DEFER in Go is not enough!
Go Secret — Interface{}: Let Me Tell You Another Thing (You’re here)
1. Interface actually holds 2 things, not one.
When you assign a variable of a specific type to an interface{} variable in Go, what actually happens is that Go forms a new structure. This new structure holds both the type information and the variable’s value.
Think of it like a simple container (though, in reality, it’s more complicated):
During runtime, this action is known as “boxing” the value, picture an interface{} structure somewhat like this:
type iface struct {
tab *itab
data unsafe.Pointer
}
Here, the ‘tab’ type descriptor is a tiny structure carrying information like the interface’s method set, the underlying type, and the methods of the underlying type that implement the interface.
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
The ‘data
’ pointer just points to where the variable’s actual value lives in memory.
When you want to take a value out of an interface{}
, Go “unboxes” it. It reads the type and pointer information, and then it forms a new variable of the right type based on this data.
All this is managed by the Go runtime.
“How did you find out about these inner workings?”
Well, if you’re curious for more details, you can delve into the Go runtime repository. It’s a treasure trove of functions and structures that the Go runtime system uses to handle the running of Go programs.
2. Nil is not Nil
In Go, an interface{} variable can hold a nil value but still NOT be considered equal to nil.
Let’s see a simple example to make this clear:
func main() {
var x interface{}
var y *int = nil
x = y
if x != nil {
fmt.Println("x != nil") // actual
} else {
fmt.Println("x == nil") // expect
}
fmt.Println(x)
}
// x != nil
// <nil>
Here, I set ‘x
’ to a nil pointer of type int but you’ll see that it prints x != nil
even though x
shows as <nil>
when printed. This happens because the ==
operator only sees an interface{}
as nil if both its type and value parts are nil.
“Seriously, what’s the workaround for this mess?”
A way to tackle this is by using reflection, which doesn’t require you to know the actual type and I often write a small function like this to check for nil values:
func IsAnyNil(x interface{}) bool {
return reflect.ValueOf(x).IsNil()
}
“And don’t tell me that
interface(int64) = 1
andinterface(int32) = 1
are not equal, right?”
Great question! No, they’re not the same. When it comes to interface{}, even if the values look the same, the types matter.
I ran a quick test to confirm:
func main() {
var y int32 = 1
var z int64 = 1
var x1 interface{} = y
var x2 interface{} = z
if x1 == x2 {
fmt.Println("x1 == x2") // expect
} else {
fmt.Println("x1 != x2") // actual
}
}
// x1 != x2
You may encounter this solution for dealing with panic if the type of x is not a channel, function, interface, map, pointer, or slice. Thus, we slightly modify our solution:
And there you go, now you should have a good grasp of how interface{}
behaves in Go. Feel free to use this knowledge as you work on your projects.