Avoiding Go runtime errors with interface compliance and type assertion checks
As I've been brushing up on my Go skills with a side project, one question I had was how errors related to struct
s not implementing an interface manifest in different ways in Go.
So here's a bit of a dive into what I learned on how we can ensure that the struct
s we define actually implement those interface methods so that the users of our software don't run into a runtime error later on.
Go makes a couple of mechanisms available to us for this. Specifically, Go has:
- compile-time interface compliance checks; and
- compile-time type assertions.
Which leads us to the question of what do these checks look like in practice?
Compile-time interface compliance check
This check will verify interface compliance when we attempt to use a struct
where an interface is expected.
For instance, here's some code that would lead to a compile-time error as a result of an interface compliance check:
type SpecialDatabase interface {
DoStuff() error
}
type Store struct {
// Connection to a database
db *pgx.Conn
}
func NewStore(ctx context.Context, connStr string) SpecialDatabase {
db, err := pgx.Connect(ctx, connStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
return &Store{db: db}
}
This example doesn't follow the "accept interfaces, return concrete types" pattern for demonstration purposes
and would look something like the following:
cannot use &Store{…} (value of type *Store) as SpecialDatabase value in return statement: *Store does not implement SpecialDatabase (missing method DoStuff)
If you're writing Go code where your project is using the struct
and methods defined on the interface, then this is likely the type of check that will lead to compile errors raising awareness of the issue.
Now that we know what interface compliance checks look like, let's look at the other mechanism that Go offers us.
Compile-time type assertion
This will verify interface compliance regardless of whether you're actually using the struct
as a particular interface type anywhere in your code.
If the Go code being written is intended to be used as a library by others, then you'll likely want to use this type of assertion as the library may not be using the interface methods on the struct
, yet those methods are expected to be defined on the struct
by those using the library.
An example of some problematic code:
type Foo struct {
Name string
}
type CoolInterface interface {
DoStuff() error
}
func main() {
test := Foo{Name: "Ada"}
fmt.Printf("Hello %s", test.Name)
test.DoStuff()
}
Without the type assertion, this will fail at runtime rather than compile-time with the following error:
test.DoStuff undefined (type Foo has no field or method DoStuff)
To ensure this is caught at compile-time, you'd need to write this type assertion:
var _ CoolInterface = (*Foo)(nil)
Let's break down this line of code a little further so we can understand what's going on:
(*Foo)(nil)
- Creates anil
pointer toFoo
. Thenil
value is used because we don't need an actual instance since we're just checking method signatures.- If
Foo
doesn't implement all methods inCoolInterface
, you'll get a compile-time error like the following:
cannot use (*Foo)(nil) (value of type *Foo) as CoolInterface value in variable declaration: *Foo does not implement CoolInterface (missing method DoStuff)
We also get the added benefit of this serving as documentation that the intention of the Foo
struct is to have the methods specified on CoolInterface
available on it.
Wrapping up
So to recap, if the Go program being written is intended to be used as a library, might want to consider adding type assertion checks to ensure errors are caught at compile time.
If the Go program being written uses the struct
and invokes the methods defined on the interface, then type assertion checks are not entirely necessary as the interface compliance checks will likely catch these issues.
Happy coding!
Like what you've read?
Subscribe to receive the latest updates in your inbox.