I made this error so you don't need to
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 forinterface{}
. 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 theany
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!