I made this error so you don't need to

I made this error so you don't need to
Golang's Gopher mascot at the bottom of the image shrugging below the text "err.(error)" where "(error)" is underlined with a red error squiggly line.

Go's type system is very simple. And this is not even a bad thing in most circumstances, but it also can be very prone to errors if used incorrectly.

I am a Go developer since 2017 and a professional Go back end developer since 2021 and I made the following mistake multiple times. So I am writing this post so you hopefully don't run into the same error.

A brief intro to Go type assertions

Type assertions are the way you "cast" an any type value into a specifically typed value. It's technically not a type cast, because any stores the actual type of the value and type assertions do checks on them instead of casting the value into another type. That's why they are called type assertions. They look as following.

var a any = 42
i, ok := a.(int)

The first return value i is the value typed as defined–in this case as an int–and the second value ok is true if the type assertion was successful. If the value a is not the type defined in the assertion, ok will be false, like in the following example.

var a any = 42
i, ok := a.(string)

You can also omit the second return value, but this is strongly discouraged because it will result in a panic if the type assertion fails.

The mistake

Based on that knowledge, take a look at the following snippet where a value is checked on two different types (feel free press the blue arrow button to execute the code).

It correctly asserted the type of v as Type1 and not Type2. This is pretty straight forward, now take a look at the next example. It is almost the same setup, but in this case, we are not using structs as types and instead use type aliases.

This also works as expected, both types are actually of type int but the assertion correctly checks for the alias. Now with that in mind, take a look at the next example. In this case, we also use type aliases, but now we are using the error type instead.

And as you can see, both type assertions now return true, which is not expected. But what is the difference to the int type from the example before? error is actually an internal interface defined as following.

type error interface {
    Error() string
}

Let's take look at another example. In this case, we use any instead of the error type.

any in Go is also actually just a type alias for interface{}. This does not play a huge role in this demonstration, but I wanted to mention it for the people who are not that used to the any type in Go.

The result is the same.

Explanation

The reason for this effect is because the type assertion behaves differently on interfaces than on structs or primitive types. There, the exact type is asserted where when asserting an interface type, the implementation is asserted. Looking at the examples above, both Type1 as well as Type2 implement error. And because they are both aliases of the error interface, both successfully assert on each other. As you can see below, you could extend the example by asserting directly on error or using an error type as the assertion value and the result is the same.

The "Solution"

Most of the time, I made this mistake when implementing some sort of error handling. I used these type aliases on error types to try to distinguish between different error types, but this will not yield the expected outcome as demonstrated above.

Therefore, if you want to distinguish errors by type using type assertions, errors.Is or errors.As, you need to create struct types that wrap the given errors. The easiest way I prefer to use is to use some sort of Inner error struct which implements error and can be referenced by custom error types as anonymous field.

type InnerError struct {
    inner error
}

func (t InnerError) Error() string {
    return t.inner.Error()
}

type MyError struct {
   InnerError
}

func NewMyError(inner error) MyError {
    return MyError {
        InnerError: InnerError {
            inner: inner
        }
    }
}

var err error = NewMyError(errors.New("inner error"))

To simplify the creating and handling of custom error types a bit, I have created a little package called elk that provides some structures like InnerError, which implements error and can be used as anonymous field in custom error structures. The package also provides some further functionality to give a richer error experience for more complex applications. Feel free to give it a visit, if you are interested!

GitHub - studio-b12/elk: An extensive error utility package with the focus on ease of use.
An extensive error utility package with the focus on ease of use. - GitHub - studio-b12/elk: An extensive error utility package with the focus on ease of use.