8. Interfaces

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.

Concrete Types versus Interfaces

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.

Explicit Interface Implementation

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

Implicit Interface Implementation

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.

Before Interfaces

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.Writer1 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.

2. https://pkg.go.dev/os#File

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.

Using Interfaces

One of the most well-known interfaces in Go is the io.Writer4 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.Stdout7 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

Implementing 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

Multiple Interfaces

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.Stringer8 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

Asserting Interface Implementation

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.

The Empty Interface

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.

The any Keyword

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.

The Problem with Empty Interfaces

“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).

Using an Empty Interface

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.

Defining Interfaces

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

Defining a Model Interface

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

Implementing the Interface

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

Embedding Interfaces

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 io10 package defines many interfaces, including interfaces that are composed of other interfaces, such as io.ReadWriter11 and io.ReadWriteCloser,12 Listing 8.37.

10. https://pkg.go.dev/io

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.Read13 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

Defining a Validatable Interface

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

Type Assertion

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.Writer14 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

Asserting Assertion

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.

Asserting Concrete Types

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

Assertions through Switch

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

Capturing Switch Type

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.

Beware of Case Order

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.WriteStringer15 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-staticcheck16 tool can be used to check switch statements for poor case clause organization, Listing 8.48.

16. https://staticcheck.io/

Listing 8.48 Checking for Poor switch Statement Organization

Using Assertions

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

Defining the Callback Interfaces

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.

Breaking It Down

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

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset