Another topic I would like to discuss in these series is the interface to nil comparison problem. This question is frequently asked during many golang interviews and requires an understanding of how types are constructed under the hood.
Question: Which if
statements will be evaluated as true
?
package main
import "fmt"
type SomeType interface {
Get()
}
type SomeImpl struct {...}
func (i SomeImpl) Get() {...}
func main() {
var aType SomeType
if aType == nil {
fmt.Println("nil interface")
}
var aImpl *SomeImpl
if aImpl == nil {
fmt.Println("nil struct")
}
aType = aImpl
if aType == nil {
fmt.Println("nil assignment")
}
}
As we all know (hope so) the empty interface in golang holds a nil
value. The default value of an empty interface is nil. This means the uninitialised variable var aType SomeType
holds a nil
value because it has a type of empty (nil) interface. In this case, the if condition is fulfilled, we will see a print statement in the terminal:
[Running] go run "main.go"
nil interface
Good, let's continue further.
We have another uninitialised variable var aImpl *SomeImpl
which is a pointer to the struct. As you know in golang all memory is initialised (zeroed) this means that the pointers, even if they are uninitialised, will have a default value, which is nil
. So we have one more fulfilled if condition:
[Running] go run "main.go"
nil interface
nil struct
For the last, we see a value assignment (value initialisation) of a struct pointer value to the interface variable aType = aImpl
. Judging by the earlier made statements it is logical to assume that we assign a nil
value to the var aType
and in the result, aType
will remain nil
.
[Running] go run "main.go"
nil interface
nil struct
nil assignment
[Done] exited with code=0 in 0.318 seconds
This sounds logical, so the confident answer is:
The program will output all print statements into the terminal because all if statements will be fulfilled during the execution!
Okay, that sounds good (would say the interviewer). Let's run the program and check the results. The actual result looks like this:
[Running] go run "main.go"
nil interface
nil struct
[Done] exited with code=0 in 0.318 seconds
As you already noticed, the output does not contain the last nil assignment
print statement. So, why this happened?
To answer this question we must dig deeper into the language to understand how interfaces are constructed in golang.
Interface
In golang, an interface is a type that specifies a set of method signatures. When a value is assigned to an interface, golang constructs an interface value that consists of two parts: the dynamic type and the dynamic value. This is commonly referred to as the “interface tuple.”
- Dynamic Type: This is a pointer to a type descriptor that describes the type of the concrete value stored in the interface.
- Dynamic Value: This is a pointer to the actual value that the interface holds.
The interface tuple can be represented as the following structs:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr // variable sized, actually [n]uintptr
}
-
tab
: A pointer to anitab
structure that contains information about the type and the methods that the type implements for the interface. -
data
: A pointer to the actual data held by the interface.
When a value is assigned to an interface, golang finds the type descriptor for the concrete type being assigned to the interface. Then sets up the method table (itab
) that allows method calls through the interface to be dispatched to the correct implementation and finally stores a pointer to the actual value in the data field of the interface.
When aType = aImpl
is executed
-
Determining Interface Implementation: golang first determines that
*SomeImpl
(a pointer toSomeImpl
) implements theSomeType
interface because*SomeImpl
has a methodGet()
with the correct signature. -
Looking Up the Type Descriptor: golang looks up the type descriptor for
*SomeImpl
. -
Creating the
itab
: golang creates anitab
structure -
Assigning the Pointer: golang assigns the pointer to the
SomeImpl
value to the data field of the interface.
interface (aType)
+------------+ +-----------+
| tab |---------->| itab |
| | |-----------|
| data |--+ | inter |
+------------+ | | _type |
| | fun[0] |
| +-----------+
|
v
+---------------+
| *SomeImpl |
+---------------+
| ........ |
+---------------+
Summary
To sum up, what we learned, the previous explanation in short means:
+ uninitialised +-------------+ initialised +
interface (aType) interface (aType)
+------------+ +--------------------------------+
| tab: nil | | tab: type of *SomeImpl |
| data: nil | | data: value of *SomeImpl (nil) |
+------------+ +--------------------------------+
Now the tricky part.
In golang when checking if an interface
is nil
, both the tab
and data
fields must be nil
. If an interface
holds a nil
pointer of a concrete type, the tab
field will not be nil
, so the interface
itself will not be considered nil
.
This is the reason why we don't see the last print statement inside the terminal. After execution of aType = aImpl
the variable aType
is no longer considered as a nil / empty interface.
It's that easy!