📜 ⬆️ ⬇️

How to use interfaces in Go



In his free time, the author of the material advises on Go and parses the code. Naturally, in the course of such activities, he reads a lot of code written by other people. Recently, the author of this article had the impression (it’s an impression, no statistics) that programmers began to work more often with interfaces in the “Java style”.

This post contains recommendations from the author of the material on the optimal use of interfaces in Go, based on his experience in writing code.
')
In the examples of this post, we will use two packages of animal and circus . Many things in this post describe working with code that borders on regular use of packages.

How not to do


A very common phenomenon that I observe:

 package animals type Animal interface { Speaks() string } //  Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animals" func Perform(a animal.Animal) string { return a.Speaks() } 

This is the so-called use of Java-style interfaces. It can be characterized by the following steps:

  1. Define an interface.
  2. Define one type that satisfies the interface behavior.
  3. Define methods that satisfy the interface implementation.

In summary, we are dealing with "writing types that satisfy the interfaces." This code has its own distinct smell , suggesting the following thoughts:


How to do instead


The interfaces in Go encourage a lazy approach, and that's good. Instead of writing types that satisfy interfaces, you should write interfaces that meet real practical requirements.

What is meant: instead of defining Animal in the package of animals , define it at the point of use, that is, the circus * package.

 package animals type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

A more natural way to do this is this:

  1. Define types
  2. Define the interface at the point of use.

This approach reduces dependence on the components of the animals package. Reducing dependencies is the right way to create fault-tolerant software.

Postel law


There is one good principle for writing good software. This is the Postel law , which is often formulated as follows:
“Treat conservatively what you refer and, liberally, what you accept”
In terms of Go, the law is:

“Accept interfaces, return structures”

In general, this is a very good rule for designing fault-tolerant, stable things * . The main unit of code in Go is the function. When designing functions and methods, it is useful to adhere to the following pattern:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

Here we accept everything that implements the interface, which can be anything, including empty. Given the value of a particular type. Of course, the limitation of what can be a has its meaning. As one go-proverb says:

"The empty interface says nothing," - Rob Pike

Therefore, it is highly desirable to prevent the interface{} taking over.

Application Example: Imitation


A striking example of the benefits of applying Postel’s law is testing cases. Suppose you have a function that looks like this:

 func Takes(db Database) error 

If the Database is an interface, then in the test code you can simply provide a simulation of the implementation of the Database without the need to transfer a real DB object.

When the definition of an interface is acceptable in advance


To be honest, programming is a fairly free way to express ideas. There are no immutable rules. Of course, you can always define interfaces in advance, without fear of being arrested by the police code. In the context of multiple packages, if you know your functions and are going to accept a specific interface within a package, then do so.

The definition of an interface usually smacks of over-engineering, but there are situations in which you obviously should do this. In particular, the following examples come to mind:


Next, a brief look at each of them.

Sealed Interfaces


Sealed interfaces can only be discussed in the context of multiple packages. A sealed interface is an interface with unexported methods. This means that users outside this package cannot create types that satisfy this interface. This is useful for emulating a variant type in order to exhaustively search for interface-matching types.

If you define something like this:

 type Fooer interface { Foo() sealed() } 

Only the package that Fooer defined can use it and create something valuable from it. This allows you to create brute force switch operators for types.

The sealed interface also allows analysis tools to easily pick up any non-bulkhead pattern matches. BurntSushi sumtypes package is aimed at solving this problem.

Abstract data types


Another case of defining an interface in advance is related to the creation of abstract data types. They can be either sealed or unsealed.

A good example of this case is the sort package included in the standard library. It defines the collection to be sorted as follows.

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

This code fragment has upset a lot of people, because if you want to use the sort package, you will have to implement methods for the interface. Many people dislike the need to add three extra lines of code.

However, I think this is a very elegant form of generics in Go. Its use should be encouraged more often.

Alternative and at the same time elegant design options will require higher order types. In this post we will not consider them.

Recursive interfaces


This is probably another example of the code with the smell, but there are cases when it is simply impossible to avoid using it. Simple manipulations allow you to get something like

 type Fooer interface { Foo() Fooer } 

The recursive interface pattern will obviously require its definition in advance. The recommendation for defining the interface at the point of use is not applicable here.

This pattern is useful for creating contexts and then working on them. The code loaded by the context usually encloses itself inside a package with exporting only contexts (ala tensor package), so in practice I do not encounter this case as often. I can tell you something else about contextual patterns, but I will leave it for another post.

Conclusion


Despite the fact that one of the headlines of the post reads "How not to do it," I am in no way trying to forbid anything. Rather, I want to make sure that readers often think about border conditions, since it is in such cases that various abnormal situations arise.

I find the ad principle at the point of use extremely useful. As a result of its application in practice, I do not encounter problems that arise in case of neglect of them.

Nevertheless, I also sometimes inadvertently write Java-style interfaces. As a rule, this happens if shortly before this I wrote a lot of Java or Python code. The desire for excessive complexity and the “presentation of everything as classes” is sometimes very strong, especially if you write Go code after writing a lot of object-oriented code.

Thus, this post also serves as a reminder to yourself about what the path to writing code looks like, which will not later cause a headache. Waiting for your comments!

image

Source: https://habr.com/ru/post/411029/


All Articles