Skip to content

Go Traps - nil interfaces

August 30, 2022Ewen Quimerc'h5 min read

Learn good practices about Go interfaces to never get a runtime panic

Go is one of the programming languages I love the most. It allows a very fast execution time (unlike Python) without affecting negatively the developer experience (unlike C), and is strongly typed (unlike TS where you can lie to the compiler). But it has some flaws, and nil interfaces are a tricky one I have to address.

After reading this article, you'll never experience runtime errors regarding interfaces

If you already know a little about interfaces, you can skip to this section.

What is an interface in Go?

Definition

In Go, the interface type defines an object that should have a special behaviour.

type Speaker interface {
	Say(word string) string
}

This struct Person is a Speaker.

type Person struct {
	Name string
}

func (p Person) Say(word string) string {
	return "Hi, " + word
}

And so is this Dog.

type Dog struct {
	Age string
}

func (d Dog) Say(word string) string {
	return "Waf, " + word
}

It is very useful because you can use either type (Dog or Person) in a function that accepts a Speaker.

func greetMichael(s Speaker) {
	fmt.Println(s.Say("Michael"))
}

func main() {
	h := Person{name:"Laurent"}
	greetMichael(h) // Hi, Michael

	d := Dog{age:8}
	greetMichael(d) // Waf, Michael
}

Unlike concrete types, you don't have to pass a Speaker type to the greetMichael function. Any concrete type that satisfies Speaker will work! This is a very elegant form of polymorphism.

In fact, we reverse the dependency : the function does not care about the type, and can be applied to any type that satisfies Speaker (which means having a Say method).

For example, you can parse different data types with only one function providing you defined a parsing method for each type.

Compile-time checks 🔒

You can't assign something of type T to an interface I if the type T doesn't know the methods defined in I. It is a safe way to make sure nothing breaks at runtime.

type Cat struct {
	Age string
}

func (c Cat) DrinkMilk() string {
	return "Slurp"
}

func main() {
	c := Cat{}
	greetMichael(c)
	// cannot use c (variable of type Cat) as type Speaker in argument to greetMichael: Cat does not implement Speaker (missing Say method)
}

Interface as a type on its own

Passing a concrete type to a function that expects an interface isn't the only way to do. You can also directly pass an interface !

// A concrete type is used to initialize the interface,
// but `speakerVar` is of interface type Speaker!
var speakerVar Speaker = Person{name:"This field doesn't matter"}
// speakerVar.Name isn't available anymore, speakerVar is only a Speaker
greetMichael(speakerVar)

But what if I forget to initialize my interface ?

// I only declare my variable without initializing it explicitely
var speakerVar Speaker
greetMichael(speakerVar) // ???

The problem with Go interfaces

Zero values

In Go we have a special thing about initialization: every type is initialized at declaration, with "zero values". The empty string "" for type string, 0 for numeral types, and structs are initialized recursively.

nil interface

Everything is safe and all, but a question emerges:

What is the zero value of an interface?

The answer is nil. And I love Go, but that thing, it scares me.

It scares me

Because you can't nil.Say() something. Nil doesn't have any method: it's a panic! The code will crash at runtime! What's so scary is that any interface can be nil, so you have to trust libraries that give interfaces.

From the outside, the problem looks like this:

// ✅ a) OK: A concrete type is passed
p := Person{name: "Laurent"}
greetMichael(p) // Hi, Michael

// ⚠️ b.1) Risky: An initialized interface is passed
// (a concrete type is used to initialize the interface but `i` is of type Speaker)
var i Speaker = Dog{age: 5}
greetMichael(i) // Waf, Michael

// ❌  b.2) KO: A nil interface is passed
var nilInterface Speaker // nilInterface is equal to `nil`
greetMichael(nilInterface)
// panic: runtime error: invalid memory address or nil pointer dereference

But I'll never pass a nil interface on purpose, right?

You'll do, involuntarily. It's a common mistake, and it's not your fault. It's because of the way Go initializes variables at declaration. If you don't initialize an interface, it will be nil by default.

type structIForgotToInitialize struct {
	name string
	speaker Speaker
}

func main() {
	m := structIForgotToInitialize{name: "Ewen"}
  // Same as  m := structIForgotToInitialize{name: "Ewen", speaker: nil}
  // I forgot to initialize speaker!
	m.speaker.Say(m.name) // panic, nil dereferencing
}

In this example, m.speaker is of interface type Speaker. So because it is not initialized in the struct declaration, its default value is nil. There is no error at compilation time, but a runtime error when you call m.speaker.Say(m.name).

To sum up, an interface is either:

  • Something that has the desired behavior (either a concrete type or a non-nil interface)
  • nil (and this can be a serious issue)

Solutions

Nil-check before use (if the zero value is useful)

func greetMichael(s Speaker) {
	if s != nil {
		fmt.Println(s.Say("Michael"))
	}
}

This assumes the interface can be nil. This is a way to do that I don't like because it would induce that all interfaces need a nil check before use. Also, this can become very heavy to maintain in real-world code. Except if the zero value is useful!

Make the zero value useful

... is one of the official Go Proverbs.

For example, in Go, an error is simply an interface!

type error interface {
	Error() string
}

So the nil value shows the absence of an error somewhere an error could have occurred. This is a case where nil-checks are acceptable. But you'll still need to be sure that the error exists before using its Error() method.

Accept Interfaces, pass Concrete Types

From inside the function, you'll never know if the function was called with an interface or a concrete type. So you can act on the function calls.

You shouldn't pass an interface to a function, but rather always pass a concrete type that satisfies the interface! This way, the interface cannot be nil, because the concrete value always implements the interface.

With this , you never really use interfaces except in the scope of the function it is needed. A linter has been created to enforce a similar behavior. This linter verifies that your functions never return an interface. This way, you always get concrete types when calling functions. By only manipulating concrete types, you can be sure that you'll never have a nil dereferencing.

Thanks for reading!

I hope you learned some ways to deal with nil interfaces, and pick your strategy according to your needs!

To sum up:

  • if the zero value is useful, nil checks are acceptable
  • otherwise, use the linter to make sure you'll only pass concrete types to functions that accept interfaces
Ewen Quimerc'h

Ewen Quimerc'h

Web Developer at Theodo