A quirky blog about Go’s interface system and why your nil checks might be lying to you i.e, nil != nil
Picture this: you’ve built a factory function that returns a pointer to a initialized struct, and you write what seems like perfectly reasonable Go code. You have an interface, a struct that implements it, and a simple nil check. Everything looks correct, but when you run it, your program behaves in a way that defies logic, a panic occurs due to a nil pointer dereference, but the value is nil in VSCode’s debugger.
package main
import "fmt"
type UserIFace interface {
GetName() string
}
type User struct {
Name string
}
func (u *User) GetName() string {
return u.Name
}
func getUser(id string) *User {
if id == "" {
return nil
}
return &User{Name: "Swapnil"}
}
func main() {
var user UserIFace = getUser("")
if user == nil {
fmt.Println("User is nil, as expected")
} else {
fmt.Println("Wait... what? User is NOT nil?")
}
}
Run this code, and you’ll see: “Wait… what? User is NOT nil?”
Your getUser("")
function clearly returns nil
, yet user == nil
evaluates to false
. If you’re scratching your head right now, you’re experiencing one of Go’s most important and misunderstood concepts: how interfaces actually work.
To understand why this happens, we need to dig deep into what Go interfaces actually are. Most developers think of interfaces as simple contracts or abstract types, but in Go, they’re much more sophisticated. I learnt this the hard way and it took me a while to understand why my code was panicking and having to explain to my architect that I didn’t screw up the factory function or skimp on the nil checks.
A Go interface is not just a pointer or a simple value. Under the hood, it’s a data structure that contains two crucial pieces of information:
Interface Structure:
┌─────────────┐
│ _type │ ──> Points to *User type metadata
├─────────────┤
│ data │ ──> Points to nil (no actual User instance)
└─────────────┘
When you declare var user UserIFace = getUser("")
, here’s what happens step by step:
getUser("")
returns (*User)(nil)
- a nil pointer of type *User
UserIFace
interface_type
: pointing to *User
type informationdata
: nil
(since the pointer is nil)The critical insight: An interface is considered nil only when both _type
and data
are nil. This is why the original example prints “User is NOT nil” - the interface contains type information about *User
even though the actual value is nil. You cannot swap the nils, they are not the same. ( Did I sneak in a terrible pun there?, well yes,yes I did)
In Go, there are actually two different kinds of “nil” when it comes to interfaces:
// Type 1: Completely nil interface (both type and data are nil)
var completelyNil UserIFace = nil
fmt.Printf("completelyNil == nil: %v\n", completelyNil == nil) // true
// Type 2: Interface with nil value but concrete type info
var nilUser *User = nil
var interfaceWithNilUser UserIFace = nilUser
fmt.Printf("interfaceWithNilUser == nil: %v\n", interfaceWithNilUser == nil) // false
This is why the original example prints “User is NOT nil” - the interface contains type information about *User
even though the actual value is nil.
Let’s examine what’s happening inside these interfaces using reflection : (PS : if you ever want to dump the internals of a struct / return value, instead of reflect try go-spew instead, I found it to be more readable and easier to use than manually writing a reflect-and-print function)
package main
import (
"fmt"
"reflect"
"unsafe"
)
type UserIFace interface {
GetName() string
}
type User struct {
Name string
}
func (u *User) GetName() string {
return u.Name
}
func main() {
// Scenario 1: Completely nil interface
var nilInterface UserIFace = nil
// Scenario 2: Interface containing a nil *User
var nilUser *User = nil
var interfaceWithNilUser UserIFace = nilUser
// Scenario 3: Interface containing actual User
var realUser *User = &User{Name: "Swapnil"}
var interfaceWithRealUser UserIFace = realUser
fmt.Println("=== Analyzing Interface Contents ===")
fmt.Printf("nilInterface:\n")
fmt.Printf(" Value: %v\n", nilInterface)
fmt.Printf(" == nil: %v\n", nilInterface == nil)
fmt.Printf(" Type: %v\n", reflect.TypeOf(nilInterface))
fmt.Printf(" ValueOf IsValid: %v\n", reflect.ValueOf(nilInterface).IsValid())
fmt.Printf("\ninterfaceWithNilUser:\n")
fmt.Printf(" Value: %v\n", interfaceWithNilUser)
fmt.Printf(" == nil: %v\n", interfaceWithNilUser == nil)
fmt.Printf(" Type: %v\n", reflect.TypeOf(interfaceWithNilUser))
fmt.Printf(" ValueOf IsNil: %v\n", reflect.ValueOf(interfaceWithNilUser).IsNil())
fmt.Printf("\ninterfaceWithRealUser:\n")
fmt.Printf(" Value: %v\n", interfaceWithRealUser)
fmt.Printf(" == nil: %v\n", interfaceWithRealUser == nil)
fmt.Printf(" Type: %v\n", reflect.TypeOf(interfaceWithRealUser))
fmt.Printf(" ValueOf IsNil: %v\n", reflect.ValueOf(interfaceWithRealUser).IsNil())
}
This will output something like:
swapnilnair@Swapnil-Nairs-MacBook-Pro sandbox % go run main.go
=== Analyzing Interface Contents ===
nilInterface:
Value: <nil>
== nil: true
Type: <nil>
ValueOf IsValid: false
interfaceWithNilUser:
Value: <nil>
== nil: false // Notice this false, that's the key.
Type: *main.User
ValueOf IsNil: true
interfaceWithRealUser:
Value: &{Swapnil}
== nil: false
Type: *main.User
ValueOf IsNil: false
Notice how interfaceWithNilUser
has a type (*main.User
) even though its value is nil. Understanding this is key to avoiding panics and bugs that leave you baffled.
This behavior might seem counterintuitive, but it’s actually a powerful feature of Go’s type system. Here’s why:
Even when the underlying value is nil, Go still knows what type the interface contains. This enables:
func processUser(u UserIFace) {
if u == nil {
fmt.Println("No user interface provided")
return
}
// We can still call methods on nil receivers if they're designed for it!
// This is only possible because Go knows the type
if reflect.ValueOf(u).IsNil() {
fmt.Println("User value is nil, but we know it's a *User")
// Could call methods that handle nil receivers
} else {
fmt.Printf("User name: %s\n", u.GetName())
}
}
This distinction is crucial in many scenarios:
func getUserFromCache(id string) UserIFace {
if id == "" {
return nil // No user interface at all
}
user := lookupInCache(id)
if user == nil {
return (*User)(nil) // We know it should be a User, but it's nil
}
return user
}
func handleCacheResult(u UserIFace) {
if u == nil {
fmt.Println("Invalid request - no user type expected")
} else if reflect.ValueOf(u).IsNil() {
fmt.Println("Valid request - user not found in cache")
} else {
fmt.Printf("Found user: %s\n", u.GetName())
}
}
This interface behavior causes significant issues in factory patterns, which is what your’s truly uses a lot of and the entire reason I wrote this blog.
package main
import (
"fmt"
"reflect"
)
type UserIFace interface {
GetName() string
}
type AdminIFace interface {
GetPermissions() []string
}
type User struct {
Name string
}
func (u *User) GetName() string {
if u == nil {
return "Unknown User"
}
return u.Name
}
type Admin struct {
Name string
Permissions []string
}
func (a *Admin) GetName() string {
if a == nil {
return "Unknown Admin"
}
return a.Name
}
func (a *Admin) GetPermissions() []string {
if a == nil {
return []string{}
}
return a.Permissions
}
// Problematic factory function
func CreateUserByRole(role string) UserIFace {
switch role {
case "admin":
var admin *Admin = nil // Simulating admin not found
return admin // Returns interface containing (*Admin)(nil)
case "user":
var user *User = nil // Simulating user not found
return user // Returns interface containing (*User)(nil)
default:
return nil // Returns really nil interface
}
}
func main() {
fmt.Println("=== Factory Pattern Demonstration ===")
roles := []string{"admin", "user", "guest"}
for _, role := range roles {
userInterface := CreateUserByRole(role)
fmt.Printf("\nRole: %s\n", role)
fmt.Printf("userInterface == nil: %v\n", userInterface == nil)
if userInterface != nil {
fmt.Printf("Type: %v\n", reflect.TypeOf(userInterface))
fmt.Printf("Value is nil: %v\n", reflect.ValueOf(userInterface).IsNil())
// This will work even with nil values due to nil-safe methods!
// cool sidenote, this is also why you can call methods on nil receivers if they're designed for it,
// protobuf uses this to great effect in the generated code for getters and setters.
fmt.Printf("GetName(): %s\n", userInterface.GetName())
}
}
// The real problem: downstream code expects nil checks to work
fmt.Println("\n=== The Problem ===")
adminUser := CreateUserByRole("admin")
// Programmer expects this to work:
if adminUser == nil {
fmt.Println("No admin user - using default permissions")
} else {
fmt.Println("Admin user found - loading admin dashboard")
// This code will execute even though admin is actually nil!
// could cause runtime panics if methods aren't nil-safe
}
}
This factory pattern creates a dangerous situation where:
CreateUserByRole("admin")
returns a non-nil interface containing a nil *Admin
adminUser == nil
fails unexpectedlyThe output looks like so:
swapnilnair@Swapnil-Nairs-MacBook-Pro sandbox % go run main.go
=== Factory Pattern Demonstration ===
Role: admin
userInterface == nil: false
Type: *main.Admin
Value is nil: true
GetName(): Unknown Admin
Role: user
userInterface == nil: false
Type: *main.User
Value is nil: true
GetName(): Unknown User
Role: guest
userInterface == nil: true
=== The Problem ===
Admin user found - doing admin things muhahahaha !!
The same issue affects error handling:
type CustomError struct {
Message string
Code int
}
func (e *CustomError) Error() string {
if e == nil {
return "unknown error"
}
return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}
func doSomething() error {
var err *CustomError = nil
// Some logic that might set err
if someCondition {
return err // Returns interface containing (*CustomError)(nil)
}
return nil // Returns truly nil interface
}
func main() {
if err := doSomething(); err != nil {
fmt.Printf("Error occurred: %v\n", err) // This will always execute!
}
}
func CreateUserByRole(role string) UserIFace {
switch role {
case "admin":
admin := getAdminFromDB() // Returns *Admin or nil
if admin == nil {
return nil // Return truly nil interface
}
return admin
case "user":
user := getUserFromDB() // Returns *User or nil
if user == nil {
return nil // Return truly nil interface
}
return user
default:
return nil
}
}
func (u *User) GetName() string {
if u == nil {
return "Guest User" // Graceful handling of nil receiver
}
return u.Name
}
func (u *User) IsValid() bool {
return u != nil && u.Name != ""
}
func ProcessUser(userInterface UserIFace) {
if userInterface == nil {
fmt.Println("No user interface provided")
return
}
// Type assert and check for nil
if user, ok := userInterface.(*User); ok {
if user == nil {
fmt.Println("User interface contains nil *User")
return
}
fmt.Printf("Processing user: %s\n", user.Name)
} else if admin, ok := userInterface.(*Admin); ok {
if admin == nil {
fmt.Println("User interface contains nil *Admin")
return
}
fmt.Printf("Processing admin: %s\n", admin.Name)
}
}
For the truly curious (hmu, I’m always happy to talk to fellow adventurers ), here’s how interfaces work at the assembly level. Go interfaces are implemented as a structure similar to:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
// method table follows
}
When you assign (*User)(nil)
to a UserIFace
:
itab
containing method information for *User
data
field points to niltab
is not nilThis is why interface{} == nil
checks the entire structure, not just the data field.
I plan on writing a blog on the internals of interfaces and how they work in Go, along with weird performance implications. So if you’re interested in that, please let me know. Until then, you can check out this blog.
The key insights to remember:
This isn’t a bug or a design flaw - it’s a sophisticated feature that makes Go’s type system more powerful and expressive. Once you understand it, you’ll write more robust code and debug interface-related issues with confidence.
The next time you see unexpected behavior with nil interfaces, you’ll know exactly what’s happening under the hood ( or at least you will after reading this blog and won’t spend hourse wondering if you’re going insane)
If you liked this blog, please let me know and I’ll write more blogs like this. If you didn’t like it, please let me know that too, and I’ll try to make the next one better.
Always remember, the only way to learn is by doing, and the only way to do is by making mistakes. Or as the go dev blog so aptly puts it:
to, err := human()
Thank you for reading!
Built for AI. Ready for Privacy. Secured at Runtime.
USA
AURVA INC. 1241 Cortez Drive, Sunnyvale, CA, USA - 94086
India
Aurva, 4th Floor, 2316, 16th Cross, 27th Main Road, HSR Layout, Bengaluru – 560102, Karnataka, India
PLATFORM
Solutions
Integrations
USA
AURVA INC. 1241 Cortez Drive, Sunnyvale, CA, USA - 94086
India
Aurva, 4th Floor, 2316, 16th Cross, 27th Main Road, HSR Layout, Bengaluru – 560102, Karnataka, India
PLATFORM
Solutions
Integrations
USA
AURVA INC. 1241 Cortez Drive, Sunnyvale, CA, USA - 94086
India
Aurva, 4th Floor, 2316, 16th Cross, 27th Main Road, HSR Layout, Bengaluru – 560102, Karnataka, India
PLATFORM
Solutions
Integrations