Skip to main content
  1. My Blog Posts and Stories/

Unexpected behavior of Golang nils

·1374 words·7 mins

Introduction #

In this article, I will be talking about the unexpected behavior of Golang nils, especially with regards to interfaces and structs. This is based on golang version 1.21.

There are some other examples shown in the unexpected-go website and I do encourage you guys to take a look at some other unexpected behaviors of golang.

Not all nils are the same. #

In Golang, not all nils are created equal.

Let us take a look at an example below.

package main

import "fmt"

type TestInterface interface {
    Test()
}

type TestA struct {}
func (t *TestA) Test() {}

type TestB struct {}
func (t *TestB) Test() {}

func main() {
    nilVal := (TestInterface)(nil)
	var nilVar TestInterface
	var testA TestInterface = (*TestA)(nil)
	var testB TestInterface = (*TestB)(nil)
	var testA2 TestInterface = (*TestA)(nil)

	fmt.Println("nilVal == nil is", nilVal == nil)
	fmt.Println("nilVar == nil is", nilVar == nil)
	fmt.Println("testA == nil is", testA == nil)
	fmt.Println("testB == nil is",testB == nil)
    fmt.Println("testA == testB is",testA == testB)
	fmt.Println("testA == testA2 is", testA == testA2)
}

What do you think the output will be? Based on what we see in the code, we would expect all of them to be true as all of them are nil.

However, the output is as follows:

nilVal == nil is true
nilVar == nil is true
testA == nil is false
testB == nil is false
testA == testB is false
testA == testA2 is true

Why are the last 3 results false? #

The reason is because the last 3 nils are not the same. They are all struct pointers which are casted to an integer.

In this case, we are casting the nil pointer to TestA/TestB first before we cast it to TestInterface.

This is also the cause for Nil errors that are non-nil errors in the unexpected-go website.

Explaining the result in the code #

var testB TestInterface /* Casts TestB to TestInterface */ = (*TestB)(nil) // Casts nil to *TestB

Due to this casting, the nil value has a type value attached to it. In this case the nil value store in testB has a type of *TestB which is different from nil which does not contain any typing.

nilVal == nil is true

nilVal is casted from type TestInterface directly to nil. As it is casted from an interface, it does not have any struct type value attached to it. This this statement is true.

nilVar == nil is true

nilVar is declared as a TestInterface type but not initialized. Thus, similar to the above, this statement is true.

testA == nil is false

testA is casted from type *TestA to TestInterface. As it is casted from a struct pointer, it has a type value attached to it. As untyped nil is not the same as a typed nil, this statement is false.

testB == nil is false

testB is casted from type *TestB to TestInterface. Similar to the above statement, this statement is false.

testA == testB is false

testA and testB are both casted from struct pointers to TestInterface. However, they are casted from different struct pointers. Thus, this statement is false as the types are different.

testA == testA2 is true

Last but not least, testA and testA2 are both casted from struct pointers to TestInterface. However, they are casted from the same struct pointer. Thus, this statement is true as the types are the same.

Fixes #

To check for nil correctly we will need to have a different way of checking nil. There are a few ways to do so and I will be listing them below.

A benchmark for each of the solutions can be found in Appendix A

Use reflect package #

Advantages #

  1. It is generic to any possible types.

Disadvantages #

  1. Slow runtime due to reflections.

Example #

package main

type TestInterface interface {
    Test()
}

type Test struct {}
func (t *Test) Test() {}

func IsNil(val any) bool {
    if val == nil {
        return true
    }

    v := reflect.ValueOf(val)
    switch k := v.Kind(); k {
    case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer,
        reflect.UnsafePointer, reflect.Interface, reflect.Slice:
        return v.IsNil()
    }
    return false
}

func main() {
	var test TestInterface = (*Test)(nil)
	fmt.Println("IsNil(test) =", IsNil(test)) // IsNil(test) = true
}

Use a switch statement #

Write a switch statement that loops through all the types that implements the interface to check for nils

Advantages #

  1. Fast runtime
  2. Easy to implement
  3. Warnings can be added during compile tile to check for missing types (If you have an IDE to do so)

Disadvantages #

  1. Need to add methods for all the structs that implements the interface.

Example #

package main

type TestInterface interface {
    Test()
}

type Test struct {}
func (t *Test) Test() {}


func IsTestNil(val TestInterface) bool{
    if val == nil {
        return true
    }
    switch v := val.(type){
        case (*Test):
            return nil == v
        ... // Other types
        default:
            return false
    }
}

func main() {
	var test TestInterface = (*Test)(nil)
	fmt.Println("IsTestNil(test) =", IsTestNil(test)) // IsTestNil(test) = true
}

Implement a isNil check in the interface #

For this solution, we can implement the method as a class method for all the structs that implements the interface.

Advantages #

  1. Fast runtime
  2. Easy to implement

Disadvantages #

  1. Need to implement the method for all the structs that implements the interface.

Example #

package main

import "fmt"

type TestInterface interface {
    Test()
    IsNil() bool
}

type Test struct {}
func (t *Test) Test() {}
func (t *Test) IsNil() bool {
    return t == nil
}

func main() {
	var test TestInterface = (*Test)(nil)
	fmt.Println("test.IsNil() =", test.IsNil()) // test.IsNil() = true
}

Conclusion #

In conclusion, I hope that this blog post has helped you to understand some of the unexpected behaviors of Golang. While programming in golang, we will have to take note of these unexpected behaviors to prevent any bugs from happening.

Note: If there are any mistakes in the blog post, please feel free to contact me to correct it.

  1. Unexpected Go
  2. StackOverflow Discussion

Appendix A: Benchmarks of the solution #

In the benchmark, we will be pitting each of the solutions head to head to see which one is the fastest. This result is used in the Fixes section of the blog post as the advantage/disadvantage of each solution.

Methodology #

We will be using the testing package to benchmark the solutions. For each of the solutions, we will be implementing a minimal interface to check for nil.

A separate interface is used for the class method solution as we require the interface itself to implement the IsNil method.

The test is run on Windows 11 64 bit with an AMD Ryzen 5 7600 6-Core Processor.

Benchmark Code #

package test

import (
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
)

type TestInterface interface{}
type TestStruct struct{}

type TestIsNilInterface interface {
	IsNil() bool
}
type TestIsNilStruct struct{}

func (t *TestIsNilStruct) IsNil() bool {
	return t == nil
}

func IsNilReflect(val any) bool {
	if val == nil {
		return true
	}

	v := reflect.ValueOf(val)
	switch k := v.Kind(); k {
	case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer,
		reflect.UnsafePointer, reflect.Interface, reflect.Slice:
		return v.IsNil()
	}
	return false
}

func IsNilSwitch(val TestInterface) bool {
	if val == nil {
		return true
	}
	switch v := val.(type) {
	case (*TestStruct):
		return nil == v
	default:
		return false
	}
}

func BenchmarkReflectNilCheck(b *testing.B) {
	var val TestInterface
	valid := TestStruct{}
	for i := 0; i < b.N; i++ {
		assert.True(b, IsNilReflect(val))
		assert.False(b, IsNilReflect(valid))
	}
}

func BenchmarkSwitchNilCheck(b *testing.B) {
	var val TestInterface
	valid := TestStruct{}
	for i := 0; i < b.N; i++ {
		assert.True(b, IsNilSwitch(val))
		assert.False(b, IsNilSwitch(valid))
	}
}

func BenchmarkMethodNilCheck(b *testing.B) {
	var val TestIsNilInterface
	valid := TestIsNilStruct{}
	for i := 0; i < b.N; i++ {
		assert.True(b, IsNilSwitch(val))
		assert.False(b, IsNilSwitch(valid))
	}
}

Results #

The results are in line with our expectations.

SolutionBenchmark Result
reflect package5.873 ns/op
switch statement2.745 ns/op
class method2.745 ns/op

As we can see based on the above table, the switch statement and class method solution is the fastest. The reflect package is the slowest due to the overhead of the reflection itself.

Raw Results #

goos: windows
goarch: amd64
cpu: AMD Ryzen 5 7600 6-Core Processor
BenchmarkReflectNilCheck-12    	204747235	         5.873 ns/op	       0 B/op	       0 allocs/op
BenchmarkSwitchNilCheck-12     	438795841	         2.745 ns/op	       0 B/op	       0 allocs/op
BenchmarkMethodNilCheck-12     	438626950	         2.745 ns/op	       0 B/op	       0 allocs/op
PASS