Georgios Komninos
Georgios Komninos's Blog

Georgios Komninos's Blog

Interfaces and error checking golang

Georgios Komninos's photo
Georgios Komninos
·Nov 19, 2022·

4 min read

Introduction

In this post I would like to discuss a mistake that I have seen while I was reviewing a pull request. We are going to talk a bit about golang interfaces in the context of error handling and checking if an error is nil

Let's see some code

first we define a custom error MyError and we implement the error interface.

we define 2 functions:

  • doSomething1
  • doSomething2

the intention of both functions is to return an error if the input value is false

Download the code and run it via go run main.go Here is the output:

Running doSomething1(true)
        no error: <nil> <nil>
Running doSomething1(false)
        error: error happened main.MyError
Running doSomething2(true)
        error: <nil> *main.MyError
Running doSomething2(false)
        error: error happened *main.MyError

Let's inspect the output of doSomething1:

  • When the input is true then err != nil and the behavior looks correct.
  • When the input is false then err==nil and the behavior looks correct

Let's inspect the output of doSomething2:

  • When the input is true then err == nil and the behavior looks wrong.
  • When the input is false then err!=nil and the behavior looks correct

So something is wrong with the implementation of doSomething2.

Let's investigate a bit

func doSomething2(v bool) error {
        var err *MyError
        if !v {
                err = &MyError{}
                return err
        }
        return err
}

doSomething2 returns type error interface and MyError implements thats.

  • we declare first a pointer to MyError (err variable) when v is false we:
    • then we initialize a MyErr{} and assign it's address to err
    • we return the err when v is true:
    • we return the err

What's the problem here?

Let's inspect the output of the main.go for the executions of the doSomething2 function.

When v == true:

Running doSomething2(true)
        error: <nil> *main.MyError

As you see it prints the path that is for when err != nil. But since we returned an uninitialized pointer of *MyError it should be nil, shouldn't be ?

When v == false

Running doSomething2(false)
        error: error happened *main.MyError

we print the path that is for when err != nil which is correct.

Our code does not work as we expect.

Let's see why doSomething2 is wrong

The function returns an error interface .

In our main function we check if the return value of the function is nil or not. Since the function returns an interface we check if an interface is nil or not .

An interface consists of two parts:

  • it's type
  • it's value

in go source code

there is:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

tab: describes the interface data: it's a pointer to the value that implements the interface

An interface is nil when both type (tab) and data (data) are nil . You can clearly see that this is not the case in our output:

<nil> *main.MyError

As you see the data here are nil but the type is*main.MyError.

Now we have an explanation of why we see this behavior. Let's see how we fix now doSomething2 .

func doSomething2(v bool) error {
        var err error
        if !v {
                err = &MyError{}
                return err
        }
        return err
}

We just need to use a declare a variable of type error and use that.

We can also do something else in our main.go if we don't want to change our doSomething2 function

if err := doSomething2(true); err.(*MyError) != nil {
  ...
}

we convert error to the underlying type (*MyError) and we can check it's value. Do not do this! Instead always use the builtin error type to store the error values.

Summary

An interface consists of it's underlying type and a pointer to the value that implements they interface. An interface is nil only if both the type and the value are nil. Be aware of that and when you return interfaces and check for nil . Especially in error handling use always the built in error type to store your error and return also the builtin error type.

 
Share this