The other day, when I was working on a few tests using Go mocks and testify, I came across an interesting bug in my code that prevented the tests from passing. The issue had to deal with how nil works with interfaces and it’s types.

This story is free for everyone: Free Link

In Go, nil is a powerful and ubiquitous concept used to represent the zero value for pointers, interfaces, slices, maps, channels, and function types. While nil might seem simple at first glance, its behavior can lead to subtle bugs, especially when working with interfaces.

This post is my explanation of interfaces and nil, explaining common pitfalls and providing examples to clarify its nuances.

nil Basics

Untyped nil

nil in Go is a predeclared identifier that represents the zero value for various types. However, nil itself is untyped; it takes on a type only when assigned to a variable or passed as an argument to a function.

Example:

var p *int = nil  // Typed nil: p is a pointer to int, initialized to nil
var s []int = nil // Typed nil: s is a slice, initialized to nil
var m map[string]int = nil // Typed nil: m is a map, initialized to nil

Here, nil is assigned to variables of specific types (*int, []int, and map[string]int), making it a "typed nil."

nil in Interfaces

Interfaces in Go consist of two parts:

  1. Dynamic Type: The actual type the interface wraps.
  2. Dynamic Value: The value of the wrapped type.

An interface is considered nil only if both its dynamic type and dynamic value are nil.

Example:

var i interface{} = nil  // Both dynamic type and value are nil
fmt.Println(i == nil) // true

var p *int = nil // A nil pointer
var i interface{} = p // Dynamic type: *int, Dynamic value: nil
fmt.Println(i == nil) // false (dynamic type is *int)

Common Pitfalls with nil

Pitfall 1: nil in Interfaces

Assigning a nil pointer (or other nil-capable type) to an interface creates an interface with a non-nil dynamic type but a nil value. This can lead to unexpected results when comparing the interface to nil.

Example:

func checkNil(i interface{}) {
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is not nil")
}
}

var p *int = nil
checkNil(p) // Output: i is not nil (dynamic type: *int)

Solution:

To avoid this, always ensure the dynamic type is nil when expecting a truly nil interface.

var i interface{} = nil
checkNil(i) // Output: i is nil

Pitfall 2: Mocking in Tests

When mocking a function that returns a pointer type, returning nil directly may cause a panic if the code expects a typed nil.

Example:

mockDBClient.On("GetDeviceByID", mock.AnythingOfType("string")).Return(nil, nil)

In this case, the first nil has no type and defaults to interface{}. If the code attempts to use this return value as a *Device, it will result in a panic due to a type mismatch. This was what was happening in my case.

Solution:

Explicitly cast the return value to the expected type:

mockDBClient.On("GetDeviceByID", mock.AnythingOfType("string")).Return((*models.Device)(nil), nil)

This ensures the returned nil is typed correctly as *models.Device.

Practical Examples

Example 1: Checking nil in Interfaces

func isNil(i interface{}) bool {
return i == nil
}

var p *int = nil
fmt.Println(isNil(p)) // false: dynamic type is *int

var i interface{} = nil
fmt.Println(isNil(i)) // true: no dynamic type

Example 2: Handling Typed Nil in Functions

func process(i interface{}) {
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is not nil")
}
}

var p *int = nil
process(p) // Output: i is not nil

Example 3: Mocking in Unit Tests

type MockDB struct {
mock.Mock
}

func (m *MockDB) GetDeviceByID(deviceID string) (*models.Device, error) {
args := m.Called(deviceID)
return args.Get(0).(*models.Device), args.Error(1)
}

func TestGetDeviceByID(t *testing.T) {
mockDB := new(MockDB)
// Correctly mock the return value
mockDB.On("GetDeviceByID", "1234").Return((*models.Device)(nil), nil)
device, err := mockDB.GetDeviceByID("1234")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if device != nil {
t.Fatalf("expected nil, got %v", device)
}
}

Key Takeaways

  1. nil in Go is untyped but must conform to the expected type in assignments or function calls.
  2. Interfaces in Go can have a non-nil dynamic type with a nil value, leading to surprising behaviors.
  3. Always explicitly cast nil to the appropriate type when mocking or testing code that involves interfaces or pointers.
  4. Be mindful of interface behavior in both production code and tests to avoid unexpected panics.