In the past few months, I have been conducting a
study asking people that it’s hard for them to understand in Go. And I noticed that the answers regularly mentioned the concept of interfaces. Go was the first interface language I used, and I remember that at that time this concept seemed very confusing. And in this guide, I want to do this:
- To explain in human language what interfaces are.
- Explain how they are useful and how you can use them in your code.
- Talk about what
interface{}
(an empty interface). - And walk through several useful interface types that you can find in the standard library.
So what is an interface?
The interface type in Go is a kind of
definition . It defines and describes the specific methods that
some other type should have .
One of the interface types from the standard library is the
fmt.Stringer interface:
type Stringer interface { String() string }
We say that something
satisfies this interface (or
implements this interface ) if this “something” has a method with a specific signature string value
String()
.
For example, the
Book
type satisfies the interface because it has the
String()
string method:
type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) }
It doesn't matter what type the
Book
or what it does. All that matters is that it has a method called
String()
that returns a string value.
Here is another example. The
Count
type also
satisfies the fmt.Stringer
interface because it has a method with the same signature string value
String()
.
type Count int func (c Count) String() string { return strconv.Itoa(int(c)) }
It is important to understand here that we have two different types of
Book
and
Count
, which act differently. But they are united by the fact that they both satisfy the
fmt.Stringer
interface.
You can look at it from the other side. If you know that the object satisfies the
fmt.Stringer
interface, then you can assume that it has a method with the signature string value
String()
that you can call.
And now the most important thing.
When you see a declaration in Go (of a variable, function parameter, or structure field) that has an interface type, you can use an object of any type as long as it satisfies the interface.Let's say we have a function:
func WriteLog(s fmt.Stringer) { log.Println(s.String()) }
Since
WriteLog()
uses the interface type
fmt.Stringer
in the parameter
fmt.Stringer
, we can pass any object that satisfies the
fmt.Stringer
interface. For example, we can pass the
Book
and
Count
types that we created earlier in the
WriteLog()
method, and the code will work fine.
In addition, since the passed object satisfies the
fmt.Stringer
interface, we
know that it has a
String()
method, which can be safely called by the
WriteLog()
function.
Let's put it all together in one example, demonstrating the power of interfaces.
package main import ( "fmt" "strconv" "log" )
That's cool. In the main function, we created different types of
Book
and
Count
, but passed them to the
same WriteLog()
function. And she called the appropriate
String()
functions and wrote the results to the log.
If you
execute the code , you will get a similar result:
2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3
We will not dwell on this in detail. The main thing to remember: using the interface type in the declaration of the
WriteLog()
function, we made the function indifferent (or flexible) to the
type of the received object. What matters is
what methods he has .
What are useful interfaces?
There are a number of reasons why you can start using interfaces in Go. And in my experience, the most important ones are:
- Interfaces help reduce duplication, that is, the amount of boilerplate code.
- They make it easier to use stubs in unit tests instead of real objects.
- Being an architectural tool, interfaces help untie parts of your code base.
Let's take a closer look at these ways of using interfaces.
Reduce the amount of boilerplate code
Suppose we have a
Customer
structure containing some kind of customer data. In one part of the code, we want to write this information to
bytes.Buffer , and in the other part we want to write client data to
os.File on disk. But, in both cases, we want to first serialize the
ustomer
structure to JSON.
In this scenario, we can reduce the amount of boilerplate code using Go interfaces.
Go has an
io.Writer interface type:
type Writer interface { Write(p []byte) (n int, err error) }
And we can take advantage of the fact that
bytes.Buffer and the
os.File type satisfy this interface, because they have the
bytes.Buffer.Write () and
os.File.Write () methods, respectively.
Simple implementation:
package main import ( "encoding/json" "io" "log" "os" )
Of course, this is just a fictitious example (we can structure the code differently to achieve the same result). But it illustrates well the advantages of using interfaces: we can create the
Customer.WriteJSON()
method once and call it every time we need to write to something that satisfies the
io.Writer
interface.
But if you are new to Go, you will have a couple of questions: “
How do I know if the io.Writer interface exists at all? And how do you know in advance that he is satisfied bytes.Buffer
and os.File
? "
I'm afraid there is no simple solution. You just need to gain experience, get acquainted with the interfaces and different types from the standard library. This will help reading the documentation for this library and viewing someone else's code. And for quick reference, I added the most useful types of interface types to the end of the article.
But even if you do not use interfaces from the standard library, nothing prevents you from creating and using
your own interface types . We will talk about this below.
Unit Testing and Stubs
To understand how interfaces help in unit testing, let's look at a more complex example.
Suppose you have a store and store information about sales and the number of customers in PostgreSQL. You want to write a code that calculates the share of sales (specific number of sales per customer) for the last day, rounded to two decimal places.
A minimal implementation would look like this:
Now we want to create a unit test for the
calculateSalesRate()
function to verify that the calculations are correct.
Now this is problematic. We will need to configure a test instance of PostgreSQL, as well as create and delete scripts to populate the database with fake data. We will have to do a lot of work if we really want to test our calculations.
And the interfaces come to the rescue!
We will create our own interface type that describes the
CountSales()
and
CountCustomers()
methods, which the
calculateSalesRate()
function relies on. Then update the signature
calculateSalesRate()
to use this interface type as a parameter instead of the prescribed
*ShopDB
type.
Like this:
After we have done this, it will be easy for us to create a stub that satisfies the
ShopModel
interface. Then you can use it during unit testing of the correct operation of mathematical logic in the function
calculateSalesRate()
. Like this:
Now run the test and everything works fine.
Application architecture
In the previous example, we saw how you can use interfaces to decouple certain parts of the code from using specific types. For example, the
calculateSalesRate()
function does not matter what you pass to it, as long as it satisfies the
ShopModel
interface.
You can expand this idea and create whole “untied” levels in large projects.
Suppose you are creating a web application that interacts with a database. If you make an interface that describes certain methods for interacting with the database, you can refer to it instead of a specific type through HTTP handlers. Since HTTP handlers refer only to the interface, this will help to decouple the HTTP level and the level of interaction with the database from each other. It will be easier to work with levels independently, and in the future you will be able to replace some levels without affecting the work of others.
I wrote about this pattern in
one of the previous posts , there are more details and practical examples.
What is an empty interface?
If you have been programming on Go for some time, then you have probably come across an
empty interface type interface{}
. I’ll try to explain what it is. At the beginning of this article, I wrote:
The interface type in Go is a kind of definition . It defines and describes the specific methods that some other type should have .
An empty interface type
does not describe methods . He has no rules. And so any object satisfies an empty interface.
In essence, the empty interface type
interface{}
is a kind of joker. If you meet it in a declaration (variable, function parameter or structure field), then you can use an object of
any type .
Consider the code:
package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) }
Here we initialize the
person
map, which uses a string type for keys, and an empty interface type
interface{}
for values. We assigned three different types as map values (string, integer and float32), and no problem. Since objects of any type satisfy the empty interface, the code works great.
You can
run this code here , you will see a similar result:
map[age:21 height:167.64 name:Alice]
When it comes to extracting and using values from a map, it’s important to keep this in mind. Suppose you want to get the
age
value and increase it by 1. If you write a similar code, then it will not compile:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) }
You will receive an error message:
invalid operation: person["age"] + 1 (mismatched types interface {} and int)
The reason is that the value stored in map takes the type
interface{}
and loses its original, base int type. And since the value is no longer integer, we cannot add 1 to it.
To get around this, you need to make the value integer again, and only then use it:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) }
If you
run this , everything will work as expected:
2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice]
So when should you use an empty interface type?
Perhaps
not too often . If you come to this, then stop and think about whether it is right to use
interface{}
. As a general advice, I can say that it will be more understandable, safer and more productive to use specific types, that is, non-empty interface types. In the above example, it was better to define a
Person
structure with appropriately typed fields:
type Person struct { Name string Age int Height float32 }
An empty interface, on the other hand, is useful when you need to access and work with unpredictable or user-defined types. For some reason, such interfaces are used in different places in the standard library, for example, in the
gob.Encode ,
fmt.Print, and
template.Execute functions.
Useful Interface Types
Here is a short list of the most requested and useful interface types from the standard library. If you are not already familiar with them, then I recommend reading the relevant documentation.
A longer list of standard libraries is also available
here .