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 nilHere, 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:
- Dynamic Type: The actual type the interface wraps.
- 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 nilPitfall 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 typeExample 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 nilExample 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
nilin Go is untyped but must conform to the expected type in assignments or function calls.- Interfaces in Go can have a non-nil dynamic type with a nil value, leading to surprising behaviors.
- Always explicitly cast
nilto the appropriate type when mocking or testing code that involves interfaces or pointers. - Be mindful of interface behavior in both production code and tests to avoid unexpected panics.




