Loading...
Loading...
Marco Franssen

In this blog post, I'd like to take a closer look at interfaces and type assertions in Go. Implementing interfaces works a little differently than in languages such as c# and Java. I'll cover some theory and practical use cases. If this is your first time working with Go, check out this post for development environment setup and a small hello-world example.
The empty interface (interface{}) defines zero methods and can hold values of any type. Every type satisfies an interface with no methods. Code uses it to handle values of unknown types; for example, fmt.Print takes any number of arguments of type interface{}. It offers the least help with reusing logic, but can be handy when you don't know the type up front. The trade-off is losing type safety wherever you use it.
Now let's see how to define our own interface. An interface in Go is a set of method signatures. Interfaces are implemented implicitly: you don't have to declare that a type implements a particular interface. This decouples the interface definition from its implementation, which can live in any package without prior coordination. We'll explore that decoupling further later on.
First, let's look at an interface definition and how types can implement it.
package main
import "fmt"
type Animal interface {
Call() string
}
type Cow struct {
name string
}
type Dog struct {
name string
}
func (c *Cow) Call() string {
return fmt.Sprintf("Mooohoooo %s Booo!", c.name)
}
func (d *Dog) Call() string {
return fmt.Sprintf("WooofWaf %s Wuf!", d.name)
}
func main() {
var animals []Animal = []Animal{
&Cow{"Betsy"},
&Dog{"Roover"},
&Cow{"Bull"},
&Dog{"Fifi"},
}
for _, animal := range animals {
fmt.Println(animal.Call())
}
}Running this produces the following output.
$ go run main.go
Mooohoooo Betsy Booo!
WooofWaf Roover Wuf!
Mooohoooo Bull Booo!
WooofWaf Fifi Wuf!On line 5, we define an interface with a single method, Call() string. It's implicitly implemented by the methods on lines 17 and 21. These methods have receivers: a pointer to Cow (*Cow) and a pointer to Dog (*Dog), respectively. If you're more familiar with Javascript, you can loosely compare this to adding a method through Object.prototype.myMethod. Unlike in c# or Java, we don't have to declare which interface a type implements. Since both types implement Animal, we can create a slice of animals, then loop over it and have each animal call out its name.
Type assertions let us access an interface's underlying value. c := a.(Cow) assigns the underlying Cow value from the animal interface to c. If a isn't a Cow, this causes a panic. To avoid that, we'll generally use the following syntax, which checks whether the interface value holds a particular type.
package main
import "fmt"
func main() {
var i interface{} = "Howdy fellow Gophers"
fmt.Println("Type asserting test string:")
s, ok := i.(string)
fmt.Println(s, ok)
fmt.Println()
fmt.Println("Type asserting test float64:")
f, ok := i.(float64)
fmt.Println(f, ok)
fmt.Println()
fmt.Println("Type asserting byte slice:")
b := i.([]byte)
fmt.Println(b)
}This example uses the empty interface. Notice that ok holds a boolean value that you can check in an if statement. If an assertion succeeds, s, f or b holds the underlying value of the asserted type. The last assertion doesn't check for success, so it causes a panic. To learn more about handling panics, see this blog post for examples of recovery.
For a hands-on exercise, try applying type assertions to the first example in this post. Access the underlying animal values in the for loop and print them.
Now that we know a bit about interfaces in Go, I'd like to show a useful application: defining your own error structs. In Go, error is also an interface. Many packages have methods with signatures such as func SomeMethod() error or func SomeMethod() (string, error).
We could implement such a method as follows, then test the error message string.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func GetData(url string) (string, error) {
res, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("could not fetch from %s, %v", url, err)
}
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("did not get 200 response from %s, %v", url, res.StatusCode)
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body) // ignore the error here for brevity
return string(body), nil
}
func TestGetData(t *testing.T) {
body, err := GetData("htp://faulty.url")
if err == nil {
t.Errorf("Expected an error, got '%v'", err)
}
expectedErr := fmt.Sprintf("could not fetch from %s, %v", "htp://faulty.url", "Get htp://faulty.url: unsupported protocol scheme \"htp\"")
if err.Error() != expectedErr {
t.Errorf("Expected error\n%s\nbut got\n%v", expectedErr, err)
}
if body != "" {
t.Errorf("Expected no body, got'%v'", body)
}
}Tests like this are pretty clumsy and take a fair amount of effort to write. I don't really want my tests to depend on the exact error message. I'd rather check whether I got an error of a particular type. Go's error interface looks like this:
type error interface {
Error() string
}To define our own error types, we just need to implement the Error() string method on our structs. Let's rewrite the example to use our own error types, then update the test to use type assertions.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
type FetchError struct {
url string
err error
}
func NewFetchError(u string, e error) FetchError {
return FetchError{u, e}
}
func (e FetchError) Error() string {
return fmt.Sprintf("could not fetch from %s, %v", e.url, e.err)
}
type HttpError struct {
url string
code int
}
func NewHttpError(u string, c int) HttpError {
return HttpError{u, c}
}
func (e HttpError) Error() string {
return fmt.Sprintf("did not get 200 response from %s, %v", e.url, e.code)
}
func GetData(url string) (string, error) {
res, err := http.Get(url)
if err != nil {
return "", NewFetchError(url, err)
}
if res.StatusCode != http.StatusOK {
return "", NewHttpError(url, res.StatusCode)
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body) // ignore the error here for brevity
return string(body), nil
}
func TestGetData(t *testing.T) {
body, err := GetData("htp://faulty.url")
if err == nil {
t.Errorf("Expected an error, got '%v'", err)
}
fetchError, ok := err.(FetchError)
if !ok {
t.Errorf("Expected fetchError but got %v", fetchError)
}
if body != "" {
t.Errorf("Expected no body, got '%v'", body)
}
}This makes the tests much cleaner. On line 59, we use a type assertion to check whether we got a FetchError. You could make similar assertions in production code to handle each error type differently. Imagine making those decisions by comparing error strings everywhere. That would become a disaster.
To learn more about testing Go code, have a look at this blog post, which covers testing and benchmarking in more detail.
Here's a type-assertion solution to the animal exercise from earlier.
package main
import "fmt"
type Animal interface {
Call() string
}
type Cow struct {
name string
}
type Dog struct {
name string
}
func (c *Cow) Call() string {
return fmt.Sprintf("Mooohoooo %s Booo!", c.name)
}
func (d *Dog) Call() string {
return fmt.Sprintf("WooofWaf %s Wuf!", d.name)
}
func main() {
var animals []Animal = []Animal{
&Cow{"Betsy"},
&Dog{"Roover"},
&Cow{"Bull"},
&Dog{"Fifi"},
}
for _, animal := range animals {
fmt.Println(getSpecies(animal))
fmt.Println(animal.Call())
fmt.Println()
}
}
func getSpecies(i interface{}) string {
var m string
switch a := i.(type) {
case Cow:
case *Cow:
m = fmt.Sprintf("I'm a Cow, %#v", a)
break
case Dog:
case *Dog:
m = fmt.Sprintf("I'm a Dog, %#v", a)
break
default:
m = "Don't know what species this is"
}
return m
}Using a type switch lets us extend the behavior of our code. The variable a has the matched type, as you can see when running the example in the playground. If Cow had additional methods, you could call them in its switch case. Notice that getSpecies uses the empty interface (interface{}). We could use our Animal interface here, since we're only passing animals, but the empty interface also lets the function accept other values. In general, I prefer to avoid it because it doesn't provide compile-time checks. Still, there are use cases where you need it: at the time of this 2019 post, Go doesn't have generics yet. Plans for Go 2 include generics and contracts, which would add compile-time checks for cases we currently handle with interfaces and type assertions.
Thanks for reading. I hope this clarified some of the awesomeness of Go interfaces! I really like how loosely coupled they are compared to interfaces in other languages. I'd appreciate it if you shared this post on social media. Comments are very welcome, too.
Marco Franssen
Build a small Go Docker image with a multi-stage build, run it with Docker Compose, and use Traefik 1.7 to route requests and scale your local web service.
Marco Franssen
Build a Go HTTP server that handles interrupt signals, gives active requests time to finish, and coordinates graceful shutdown with goroutines and channels.
Marco Franssen
Learn Go test and benchmark conventions with a JSON example, then explore code coverage, Testify assertions and options for comparing performance.
Marco Franssen
Explore Go concurrency with practical goroutine and channel examples, including buffered channels, GOMAXPROCS, and the difference between concurrency and parallelism.