We deal with interfaces in Go







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:



  1. To explain in human language what interfaces are.
  2. Explain how they are useful and how you can use them in your code.
  3. Talk about what interface{}



    (an empty interface).
  4. 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" ) //   Book,    fmt.Stringer. type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } //   Count,    fmt.Stringer. type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } //   WriteLog(),    , //   fmt.Stringer   . func WriteLog(s fmt.Stringer) { log.Println(s.String()) } func main() { //   Book    WriteLog(). book := Book{"Alice in Wonderland", "Lewis Carrol"} WriteLog(book) //   Count    WriteLog(). count := Count(3) WriteLog(count) }
      
      





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:



  1. Interfaces help reduce duplication, that is, the amount of boilerplate code.
  2. They make it easier to use stubs in unit tests instead of real objects.
  3. 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" ) //   Customer. type Customer struct { Name string Age int } //   WriteJSON,   io.Writer   . //    ustomer  JSON,     // ,     Write()  io.Writer. func (c *Customer) WriteJSON(w io.Writer) error { js, err := json.Marshal(c) if err != nil { return err } _, err = w.Write(js) return err } func main() { //   Customer. c := &Customer{Name: "Alice", Age: 21} //    Buffer    WriteJSON var buf bytes.Buffer err := c.WriteJSON(buf) if err != nil { log.Fatal(err) } //   . f, err := os.Create("/tmp/customer") if err != nil { log.Fatal(err) } defer f.Close() err = c.WriteJSON(f) if err != nil { log.Fatal(err) } }
      
      





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:



 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr, err := calculateSalesRate(shopDB) if err != nil { log.Fatal(err) } fmt.Printf(sr) } func calculateSalesRate(sdb *ShopDB) (string, error) { since := time.Now().Sub(24 * time.Hour) sales, err := sdb.CountSales(since) if err != nil { return "", err } customers, err := sdb.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil }
      
      





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:



 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) //    ShopModel.     //     ,     //  -,     . type ShopModel interface { CountCustomers(time.Time) (int, error) CountSales(time.Time) (int, error) } //  ShopDB    ShopModel,   //       -- CountCustomers()  CountSales(). type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr := calculateSalesRate(shopDB) fmt.Printf(sr) } //       ShopModel    //    *ShopDB. func calculateSalesRate(sm ShopModel) string { since := time.Now().Sub(24 * time.Hour) sales, err := sm.CountSales(since) if err != nil { return "", err } customers, err := sm.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil }
      
      





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:



 // : main_test.go package main import ( "testing" ) type MockShopDB struct{} func (m *MockShopDB) CountCustomers() (int, error) { return 1000, nil } func (m *MockShopDB) CountSales() (int, error) { return 333, nil } func TestCalculateSalesRate(t *testing.T) { //  . m := &MockShopDB{} //     calculateSalesRate(). sr := calculateSalesRate(m) // ,        //   . exp := "0.33" if sr != exp { t.Fatalf("got %v; expected %v", sr, exp) } }
      
      





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 .



All Articles