Asserting with interface type T

Performing type assertion x.(T) with an interface type T asserts that x implements the T interface. This way you can guarantee an interface value implements an interface and only if it does, you will be able use its methods.

To understand how this can be leveraged, let’s take a quick look in the pkg/errors again. You already know the errors package, so let’s dive into the errors.Cause(err error) error function.

This function gets an error and extracts the most internal error it wraps (that which no longer wraps another error inside it). It might seem simple, but there are plenty of great things you can learn from this implementation:

The function gets an error value and it can’t assume the err argument it receives is a wrapped error (one that supports the Cause method). So before calling the Cause method, it is necessary to check that you’re dealing with an error that implements this method. By performing the type assertion in each iteration of the for loop, you can make sure the cause variable supports the Cause method, and can keep on extracting internal errors from it until you reach an error which does not have a cause.

By creating a lean, local interface containing just the methods you need and performing the assertion on it, your code is decoupled from other dependencies. The argument you received doesn’t have to be a known struct, it just needs to be an error. Any type implementing the Error and Cause methods works here. So, if you implement the Cause method in your custom error type, you can use this function with it instantly.

There’s one small catch you should be aware of, though: interfaces may change, and so you should maintain your code carefully, so your assertions don’t break. Remember to define your interfaces where you use them, keep them lean, and maintain them carefully and you should be fine.

Lastly, If you only care about one method, it’s sometimes more convenient making the assertion on an anonymous interface containing only the method you rely on, i.e. v, ok := x.(interface{ F() (int, error) }) . Using anonymous interfaces can help decoupling your code from possible dependencies, and can help guard your code from possible changes in interfaces.

Asserting with concrete type T & Type Switches

I will preface this section by introducing two similar error handling patterns that suffer from a couple of drawbacks and pitfalls. It doesn’t mean they’re not common, though. Both of them can be handy tools in small projects, but they don’t scale well.

The first one is the second kind of type assertion: Performing type assertion x.(T) with a concrete type T . It asserts the value of x is of type T , or it is convertible to type T .

The other one is the Type Switch pattern. Type switches combine a switch statement with type assertion, using the reserved type keyword. They are especially common in error handling, where knowing the underlying type of an error variable can be very helpful.

The big drawback of both approaches is that they both lead to code coupling with their dependencies. Both examples need to be familiar with the SomeErrorType struct (which needs to be exported, obviously), and need to import the mypkg package.

In both approaches, when handling your errors, you must be familiar with the type and import its package. It gets worse when you are dealing with wrapped errors, where the cause of the error can be an error created in an internal dependency you are not, and should not be, aware of.