Interfaces in Go provide a way to specify the behavior of an object; “If something can do this, then it can be used here.” In this chapter, we look at how to use interfaces to abstract behavior. Later in this chapter, we explain how generics can be used to further refine interfaces. Concepts such as the Empty Interface
, satisfying multiple interfaces, value versus pointer receivers, and asserting for behavior also are covered in this chapter.
Interfaces allow you to specify behavior. They are about doing, not being. Interfaces also allow you to abstract code to make it more reusable, extensible, and testable.
To illustrate this, let’s consider the concept of a performance venue. A performance venue should allow a variety of performers to perform at the venue.
An example of this as a function might look like Listing 8.1.
Listing 8.1 The PerformAtVenue
Function
The PerformAtVenue
function takes a Musician
, Listing 8.2, as an argument and calls the Perform
method on the musician. The Musician
type is a concrete type.
Listing 8.2 The Musician
Type
When we pass a Musician
to the PerformAtVenue
function in Listing 8.3, our code compiles, and we get the expected output.
Listing 8.3 A PerformAtVenue
Being Called with a Musician
Because the PerformAtVenue
function takes a Musician
, which is a concrete type, as an argument, we are restricted as to who can perform at the venue. For example, if we were to try to pass a Poet
to the PerformAtVenue
function, we would get a compilation error, as shown in Listing 8.4.
Listing 8.4 Compilation Error When Using the Incorrect Type
Interfaces allow you to solve this problem by specifying a common set of methods that are required by the PerformAtVenue
function.
In Listing 8.5, we introduce a Performer
interface. This interface specifies that a Perform
method is required to implement the Performer
interface.
Listing 8.5 The Performer
Interface
Both the Musician
and Poet
types have the Perform
method. Therefore, we can implement the Performer
interface on both of these types. By updating the PerformAtVenue
function, in Listing 8.6, to take a Performer
as an argument, as shown in Listing 8.6, we are now able to pass a Musician
or a Poet
to the PerformAtVenue
function.
Listing 8.6 Both Poet
and Musician
Implement the Performer
Interface
By using an interface, instead of a concrete type, we are able to abstract the code and make it more flexible and expandable.
In a lot of object-oriented languages, such as C# and Java, you have to explicitly declare that your type is implementing a very specific interface.
In C#, Listing 8.7, for example, we declare that we are using the Performer
interface by using the :
operator after the class name and listing the interfaces we want to use.
Listing 8.7 Implementing the Performer
Interface in C#
In Java, Listing 8.8, we use the implements
keyword after the class name to tell the compiler that our type wants to implement the Performer
interface.
Listing 8.8 Implementing the Performer
Interface in Java
In Go, interfaces are implemented implicitly. This means you don’t have to indicate to Go that you are implementing an interface. Given a Performer
interface, Listing 8.9, a type would need to implement the Perform
method to be considered a performer.
Listing 8.9 The Performer
Interface
By adding a Perform
method that matches the signature of the Performer
interface (Listing 8.10), the Musician
type is now implicitly implementing the Performer
interface.
Listing 8.10 The Musician
Type Implicitly Implements the Performer
Interface
Provided a type implements all behaviors specified in the interface, it can be said to implement that interface. The compiler checks to make sure a type is acceptable and reports an error if it does not. Sometimes this is called duck typing, but since it happens at compile-time in Go, it is called structural typing.
Structural typing has a handful of useful side effects:
The concrete type does not need to know about your interface.
You are able to write interfaces for concrete types that already exist.
You can write interfaces for other people’s types or types that appear in other packages.
Consider Listing 8.11. This function takes a pointer to os.File
, along with a slice of bytes. The function then calls the os.File.Writer
1 function with the data passed in.
1. https://pkg.go.dev/os#File.Writer
Listing 8.11 The WriteData
Function
To call this function, we must have an os.File
,2 which is a concrete type in the system. To call this function, we either need to retrieve, or create, a file on the filesystem, or we can use os.Stdout
,3 which is an os.File
, as we do Listing 8.12.
3. https://pkg.go.dev/os#Stdout
Listing 8.12 Using os.Stdout
As os.File
Testing this function involves significant setup. In Listing 8.13, we need to create a new file, call the WriteData
function, close the file, reopen the file, read the file, and then compare the contents. We need to do all of this work to be able to call one function, Write
, on os.File
.
Listing 8.13 Testing the WriteData
Function
The WriteData
function is a prime candidate to be refactored using interfaces.
One of the most well-known interfaces in Go is the io.Writer
4 interface, Listing 8.14. The io.Writer
interface requires the implementation of a Write
method that matches the signature of Write(p []byte) (n int, err error)
.
4. https://pkg.go.dev/io#Writer
Listing 8.14 The io.Writer
Interface
Implementations of the io.Writer
interface can be found all over the standard library, as well as in third-party packages. A few of the most common implementations of the io.Writer
interface are os.File
, bytes.Buffer
,5 and strings.Builder
.6
5. https://pkg.go.dev/bytes#Buffer
6. https://pkg.go.dev/strings#Builder
Knowing that the only portion of os.File
we are using matches the io.Writer
interface, we can modify the WriteData
to use the io.Writer
, Listing 8.15, and improve the compatibility and testability of the method.
Listing 8.15 The WriteData
Function
The usage of the WriteData
function does not change in Listing 8.16.
Listing 8.16 Using os.Stdout
7 As io.Writer
7. https://pkg.go.dev/os#Stdout
Testing the WriteData
function, Listing 8.17, also becomes easier now that we can substitute the implementation with an easier-to-test implementation.
Listing 8.17 Testing the WriteData
Function
io.Writer
Now that WriteData
uses the io.Writer
, we cannot only use implementations from the standard library like os.File
and bytes.Buffer
, but we can create our own implementation of io.Writer
.
In Listing 8.18, where we implement the Write
function with the proper signature, we don’t have to implicitly declare our type Scribe
as an io.Writer
. The compiler is able to determine whether the type being passed in implements the interface being requested.
Listing 8.18 The Scribe
Type Implements io.Writer
In Listing 8.19, we call the WriteData
function with our implementation of the io.Writer
interface, a pointer to the Scribe
type.
Listing 8.19 The Scribe
Type Implements io.Writer
The *Scribe type can also be used to test WriteData
like we did with bytes.Buffer
, Listing 8.20.
Listing 8.20 Testing the WriteData
Function
Because interfaces are implemented implicitly, it means that types can implement many interfaces at once without explicit declaration. In addition to implementing io.Writer
, the Scribe
type also implements the fmt.Stringer
8 interface, Listing 8.21.
8. https://pkg.go.dev/fmt#Stringer
Listing 8.21 The fmt.Stringer
Interface
You use the fmt.Stringer
interface to convert a value to a string. By implementing a String() string
method on the Scribe
type, the Scribe
now implements both the fmt.Stringer
and io.Writer
interfaces, Listing 8.22.
Listing 8.22 The Scribe
Type Implements Both fmt.Stringer
and io.Writer
Often, especially while implementing an interface, it can be useful to assert that your type conforms to all of the interfaces you are trying to implement. One way to do this is to declare a new variable, whose type is the interface you are implementing, and try to assign your type to it, Listing 8.23. Using the _
character tells the compiler to do the assignment to the variable and then throw away the result. These assertions are usually done at the package level.
Listing 8.23 Asserting that the Scribe
Type Implements the io.Writer
and fmt.Stringer
Interfaces
The compiler keeps failing until the Scribe
type implements both the io.Writer
and fmt.Stringer
interfaces.
All the interfaces you’ve seen so far have declared one or more methods. In Go, there is no minimum method count for an interface. That means it is possible to have what is called an empty interface, Listing 8.24. If you declare an interface with zero methods, then every type in the system is considered to have implemented it.
In Go, you use the empty interface to represent “anything.”
Listing 8.24 The Empty Interface
An int
, for example, has no methods, and because of that, an int
matches an interface with no methods.
In Go1.18
, generics9 were added to the language. As part of this, a new keyword, any
, was added to the language. This keyword is an alias for interface{}
, Listing 8.25.
9. https://go.dev/doc/tutorial/generics
Using any
rather than interface{}
is a good idea because it is more explicit and it is easier to read.
Listing 8.25 Using any
Instead of interface{}
If you’re using Go1.18
or greater, then you can use the any
keyword instead of interface{}
. Using any
instead of interface{}
is considered to be idiomatic.
“interface{} says nothing.”
—Rob Pike
It is considered bad practice in Go to overuse the empty interface. You should always try to accept either a concrete type or a nonempty interface.
While there are valid reasons to use an empty interface, the downsides should be considered first:
No type information.
Runtime panics are very possible.
Difficult code (to test, understand, document, and so on).
Consider we are writing a data store, similar to a database. We might have an Insert
method that takes id and the value we want to store, Listing 8.26. This Insert
method should be able to store our data models. These models might represent users, widgets, orders, and so on.
We can use the empty interface to accept all of our models and insert them into the data store.
Listing 8.26 The Insert
Method
Unfortunately, this means that in addition to our data models, anybody may pass any type to our data store. This is clearly not our desire. We could try an set up an elaborate set of if/else
or switch
statements, but this becomes untenable and unmanageable over time. Interfaces allow us to filter out unwanted types and only allow through types that we want.
You can create a new interface in the Go type system by using the type
keyword, giving the new type a name, and then basing that new type on the interface
type, Listing 8.27.
Listing 8.27 Defining an Interface
Interfaces define behavior; therefore, they are only a collection of methods. Interfaces can have zero, one, or many methods.
“The larger the interface, the weaker the abstraction.”
—Rob Pike
It is considered to be nonidiomatic to have large interfaces. Keep the number of methods per interface as small as possible, Listing 8.28. Small interfaces allow for easier interface implementations, especially when testing. Small interfaces also help you keep your functions and methods small in scope, making them more maintainable and testable.
Listing 8.28 Keep Interfaces Small with No More Than Two or Three Methods
It is important to note that interfaces are a collection of methods, not fields, Listing 8.29. In Go, only structs have fields; however, any type in the system can have methods. This is why interfaces are limited to methods only.
Listing 8.29 Interfaces Are Limited to Methods Only
Consider, again, the Insert
method for our data store, Listing 8.30. The method takes two arguments. The first argument is the ID of the model to be stored.
Listing 8.30 The Insert
Method
The second argument, in Listing 8.30, should be one of our data models. However, because we are using an empty interface, any type from int
to nil
may be passed in.
To prevent types, such as a function definition that isn’t an expected data model, Listing 8.31, we can define an interface to solve this problem. Because the Insert
function needs an ID for insertion, we can use that as the basis for an interface.
Listing 8.31 Passing a Function Type to the Insert
Method
To implement the Model
interface, Listing 8.32, a type must have a ID() int
method. We can clean up the Insert
method’s definition by accepting a single argument, the Model
interface, Listing 8.33.
Listing 8.32 The Model
Interface
Listing 8.33 Changing the Insert
Method to Accept a Model
Interface
Now, the compiler and/or runtime rejects any type, such as string
, []byte
, and func()
, that doesn’t have a ID() int
method, Listing 8.34.
Listing 8.34 Rejecting Types That Implement Model
Finally, let’s create a new type, User
, that implements the Model
interface, Listing 8.35.
Listing 8.35 The User
Type Implements the Model
Interface
When we update the tests, Listing 8.36, to use the User
type, our tests now pass.
Listing 8.36 Using the User
Type
Interfaces in Go can be composed of other interfaces by embedding one or more interfaces in the new interface. This can be used to great effect to combine behaviors into more complex behaviors. The io
10 package defines many interfaces, including interfaces that are composed of other interfaces, such as io.ReadWriter
11 and io.ReadWriteCloser
,12 Listing 8.37.
11. https://pkg.go.dev/io#ReadWriter
12. https://pkg.go.dev/io#ReadWriteCloser
Listing 8.37 The io.ReadWriteCloser
Interface
The alternative to embedding other interfaces is to redeclare those same methods in the combined interface, Listing 8.38.
Listing 8.38 A “Hardcoded” Representation of the io.ReadWriteCloser
Interface
This is, however, the wrong thing to do. If the intention is to implement the io.Read
interface, as it is with io.ReadWriter
, and the io.Read
13 interface changes, then it would no longer implement the correct interface. Embedding the desired interfaces allows us to keep our interfaces cleaner and more resilient.
13. https://pkg.go.dev/io#Read
Since the act of inserting a model is different than the act of updating a model, we can define an interface to ensure that only types that are both a Model
and have a Validate() error
method can be inserted, Listing 8.39.
Listing 8.39 The Validatable
Interface
The Validatable
interface, as shown in Listing 8.40, embeds the Model
interface and introduces a new method, Validate() error
, that must be implemented in addition to the method requirements of the Model
interface. The Validate() error
method allows the data model to validate itself before insertion.
Listing 8.40 The Insert
Method Accepting a Validatable
Interface
With a concrete type, like an int
or a struct
, the Go compiler and/or runtime knows exactly what the capabilities of that type are. Interfaces, however, can be backed by any type that matches that interface. This means it is possible that the concrete type backing a particular interface provides additional functionality beyond the scope of the interface.
Go allows you to test an interface to see if its concrete implementation is of a certain type. In Go, this is called type assertion.
In Listing 8.41, we are asserting that variable i
, of type any
(empty interface), also implements the io.Writer
14 interface. The result of this assertion is assigned to the variable w
. The variable w
is of type io.Writer
and can be used as such.
14. https://pkg.go.dev/io#Writer
Listing 8.41 Asserting That any
Is an io.Writer
What happens, however, when someone passes a type, such as an int
or nil
, that does not implement io.Writer
?
In Listing 8.42, when passing an int
, instead of an io.Writer
, the application panics. These panics can, and will, crash your applications and need to be protected against.
Listing 8.42 Asserting That any
Is an io.Writer
To prevent a runtime panic
when a type assertion fails, we can capture a second argument during the assertion, Listing 8.43. This second variable is of type bool
and is true
if the type assertion succeeded and false if it does not.
Listing 8.43 Validating a Type Assertion Was Successful
You should always check this boolean to prevent panics and to help keep your applications from crashing.
In addition to asserting that one interface implements another interface, you can use type assertion to get the concrete type underneath.
In Listing 8.44, we are trying to assert that variable w
, of type io.Writer
, to the type bytes.Buffer
. If the assertion is successful, ok == true
, then variable bb
is of type bytes.Buffer
, and we can now access any publicly exported fields and methods on bytes.Buffer
.
Listing 8.44 Asserting that io.Writer
Is a bytes.Buffer
When we want to assert an interface for a variety of different types, we can use the switch
statement in lieu of a lot of if
statements, Listing 8.45. Using switch
statements when doing type assertions also prevents the type assertion panics we saw earlier with individual type assertions.
Listing 8.45 Using a switch
Statement to Make Many Type Assertions
While just switching on a type can be useful, it is often much more useful to capture the result of the type assertion to a variable.
In Listing 8.46, the result of the type assertion in the switch is assigned to the variable t := i.(type)
.
Listing 8.46 Capturing the Asserted Type to a Variable
In the case of i
being of type bytes.Buffer
, then the variable t
is also of type bytes.Buffer
, and all publicly exported fields and methods of bytes.Buffer
can now be used.
The case
clauses in a switch
statement are checked in the order that they are listed. A poorly organized switch
statement can lead to incorrect matches.
In Listing 8.47, because both bytes.Buffer
and io.WriteStringer
15 implement io.Writer
, the first case
clause matches against io.Writer
, which will match both of those types and prevent the correct clause from being run.
15. https://pkg.go.dev/io#WriteStringer
Listing 8.47 Incorrect switch
Statement Layout
The go-staticcheck
16 tool can be used to check switch
statements for poor case
clause organization, Listing 8.48.
Listing 8.48 Checking for Poor switch
Statement Organization
Assertion doesn’t just work with empty interfaces. Any interface can be asserted against to see if it implements another interface. We can use this in our data store to add callback hooks: “before insert” and “after insert,” Listing 8.49.
Listing 8.49 Inserting a Record into the Data Store
We can define two new interface types in our system to support before and after insert callbacks, Listing 8.50.
Listing 8.50 Defining the Callback Interfaces
The Insert
function can be updated, Listing 8.51, to check for these new interfaces at the appropriate time in the workflow.
Listing 8.51 Checking for the Callback Interfaces
These new interfaces allow a type that implements Validatable
to opt-in to additional functionality.
Let’s look at how we are using these interfaces in the Insert
method.
In Listing 8.52, if the m
variable, of type Validatable
(interface), can be asserted to the BeforeInsertable
interface, then the bi
variable is of type BeforeInsertable
and ok
is true
. The BeforeInsert
method is called, the error it returns is checked, and the application continues or returns the error. If, however, m
does not implement BeforeInsertable
then, ok
returns false
, and the BeforeInsert
method is never called.
Listing 8.52 Checking for the BeforeInsert
Callback
We check the AfterInsertable
interface in the same way, Listing 8.53.
Listing 8.53 Checking for the AfterInsert
Callback
In this chapter, we discussed interfaces in Go. Interfaces are a collection of method definitions. We explained that if a type implements those methods, it is implicitly implementing that interface. We saw the dangers of using any
or an empty interface. We showed you how to define interfaces, how to embed interfaces, and how to assert for behavior and types in code. Finally, we demonstrated that you can assert one interface for another interface, allowing you to accept a smaller interface and check for “optional” behavior.