📜 ⬆️ ⬇️

Crash course on interfaces in go

Interfaces in Go are one of the distinguishing features of a language that form the way to solve problems. When similar to interfaces in other languages, Go interfaces still have important differences and this initially leads to excessive reuse of interfaces and confusion about how and when to use them. This is normal, but let's try to figure out what is so special about the interfaces in Go, how they are arranged, why they are so important, and what the orthogonality of the interface types and structural types in Go means.

In this article you will learn:


Header
( artwork by Svitlana Agudova )

Orthogonality


So, let's start with the first important point, which is quite easy to understand - interfaces define behavior . In this regard, the interface in Go is almost the same as in Java.
')
For example, here is the interface and its implementation in Java:

public interface Speakable { public String greeting = "Hello"; public void sayHello(); } public class Human implements Speakable { public void sayHello() { System.out.println(Speakable.greeting); } } Speakable speaker = new Human(); speaker.sayHello(); 

Go example:

 type Speaker interface { SayHello() } type Human struct { Greeting string } func (h Human) SayHello() { fmt.Println(h.Greeting) } ... var s Speaker s = Human{Greeting: "Hello"} s.SayHello() 

http://play.golang.org/p/yqvDfgnZ78

At first glance, the differences are purely cosmetic:


But some of these differences are key, in particular the last two of them. Let us dwell on them in more detail:

Implicit implementation


If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck.

In Go, a structure with methods will satisfy the interface simply by the fact of the method declaration. This does not seem to be particularly important on small programs or artificial examples, but it turns out to be key in large projects , where you have to think twice before changing a class repeatedly inherited by other classes.

The ability to easily and simply implicitly implement various interfaces allows programs to grow painlessly without needing to think through all possible interfaces in advance and not to drown in multiple inheritance. This is by the way that Go was designed to make life easier in large projects.

An important and not immediately obvious difference of this is how, as a result, you build the architecture of your program - in Java or C ++ you most likely start with the declaration of abstract classes and interfaces, and then proceed to specific implementations. In Go, on the contrary, you first write a specific type, define the data and methods, and only if you really need to abstract the behavior do you create a separate interface. Again, the scale of this difference is more pronounced on large projects.

Data vs behavior


If in Java both classes and interfaces describe both data and behavior, then in Go these concepts are fundamentally delimited.

The structure stores data, but not behavior. The interface stores the behavior, but not the data.

If you want to add a variable to the interface like Hello string - know that you are doing something wrong. If you want to embed an interface into a structure, you confuse behavior and data. In the Java world, this is normal, but in Go, this is an important distinction, since it forms the clarity of abstractions.

In the example above, Speaker describes the behavior, but does not say what the one who implements this interface should say. Again, Speaker as an interface in Go code emerged from the practical need to be implemented by some other type , and not as a "base class", to which a specific implementation of Human was written.

It is important to understand that as soon as you begin to clearly separate the abstractions of "behavior" and "data", you will begin to more clearly understand the purpose and proper way of using interfaces in Go. Human and Speaker are orthogonal . Human can easily satisfy 10 more interfaces (Walker, Listener, Player, Programmer, etc), and Speaker can be satisfied with dozens of types, even from other packages (Robot, Animal, Computer, etc). And all this, with minimal syntax overhead, which, again, is important in large code bases.

Interface device


If you do not really understand how Human can simultaneously be Speaker and a dozen more interfaces, and still be orthogonal, let's dig deeper and see how the interfaces under the hood are arranged. In Go 1.5 (which is itself written in Go), the interface type looks like this:

src / runtime / runtime2.go

 type iface struct { tab *itab data unsafe.Pointer } 

Where tab is a pointer to an Interface Table or itable is a structure that stores some type metadata and a list of methods used to satisfy an interface.
data - indicates the actual variable with a specific (static) type,

For clarity, we slightly modify our code as follows:

 h := Human{Greeting: "Hello"} s := Speaker(h) s.SayHello() 

http://play.golang.org/p/AB0ExdGN0W

Itable

The figure shows that s consists of two pointers, the first indicating itable for a particular pair (static type Human, Speaker interface), and the other on a copy of the original Human value.

 h := Human{Greeting: "Hello"} s := Speaker(h) h.Greeting = "Meow" s.SayHello() //  "hello" 

itable


Now a few words about itable. Since this table will be unique for each pair of interface-static type, it will be irrational and inefficient to calculate it at the compilation stage (early binding).

Instead, the compiler generates metadata for each static type, which, among other things, stores a list of methods implemented for that type. Similarly, metadata is generated with a list of methods for each interface. Now, during program execution, runtime Go can calculate itable on the fly (late binding) for each specific pair. This itable is cached, so the miscalculation occurs only once.

Knowing this, it becomes obvious why Go catches type mismatches at the compilation stage, but casting to the interface during execution. Do not forget that it is in order to safely catch errors of casting to interface types that there is a construction comma-ok - if s, ok := h.(Speaker); !ok { ... } if s, ok := h.(Speaker); !ok { ... } .

 var s Speaker = string("test") // compile-time error var s Speaker = io.Reader // compile time error var h string = Human{} // compile time error var s interface{}; h = s.(Human) // runtime error 

Empty interface {}


Now let us recall the so-called empty interface (interface) - interface{} , which is generally satisfied with any type. Since the empty interface has no methods, it’s not even necessary to calculate and store itable - just enough meta-information about the static type.

Therefore, in memory, an empty interface looks like this:
Empty Interface

Now, every time you want to use an empty interface - remember. that he means nothing. No abstraction. This is an invisible cloak over your particular type, which hides specifics from you, and does not give any understanding about the behavior. That is why it is necessary to use empty interfaces in the most extreme cases.

Interfaces and generics


As you know, in Go, generic containers are limited only to those in the language - slices, maps. Interfaces can be used to write generic algorithms in Go. A classic example here is such a Binary Tree implementation .

 type Item interface { // Less tests whether the current item is less than the given argument. // // This must provide a strict weak ordering. // If !a.Less(b) && !b.Less(a), we treat this to mean a == b (ie we can only // hold one of either a or b in the tree). Less(than Item) bool } 

The btree.Item type is an interface in which the only Less method is defined that allows you to compare values. Under the hood of the algorithm, a slice from Item is used, and the algorithm doesn’t care much what static type is there - the only thing it needs is to be able to compare values, and this is what the Less () method gives us.

 type MyInt int func (m MyInt) Less(than MyInt) bool { return m < than } b := btree.New(10) b.ReplaceOrInsert(MyInt(5)) 

A similar approach can be seen in the standard library in the sort package — any type that satisfies the interface sort.Interface can be passed as a parameter to the sort.Sort function, which will sort it:

 type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) } 

For example:

 type Person struct { Name string Age int } // ByAge implements sort.Interface for []Person based on // the Age field. type ByAge []Person func (a ByAge) Len() int { return len(a) } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } ... people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26}, } sort.Sort(ByAge(people)) 

How to stop abusing interfaces and start living


Many newcomers to Go, especially those who have switched from languages ​​with dynamic typing, see in interface types a way to not work with specific types. “I’ll wrap everything up in interface {},” the developer thinks and pollutes his program with interfaces that are mostly empty.

But the golden rule here sounds like this - always work with specific types, and use the interface only where it is needed, and the empty interface is generally in the most extreme cases when there is no other way.

For example, you are writing a dashboard on which you output some data, and this data comes from one source as float64 values, and from another as strings ("failed", "success", etc.). How do you implement a function that receives values ​​by channel and displays them on the screen?

Most beginners will say - easy, let's make a channel of empty interfaces ( chan interface{} ) and pass on it, and then do the castes to the type:

 func Display(ch chan interface{}) { for v := range ch { switch x := v.(type) { case float64: RenderFloat64(x) case string: RenderString(x) } } } 

And, although this code also has the right to exist, we can make it more beautiful. Let's think what is common in our case for float64 and string? The fact that both of them should be rendered is already a candidate for creating an interface with the Render method. Let's try:

 type Renderer interface { Render() } 

Further, since we cannot hang methods on standard types (it will already be a different type), we will create our own MyFloat and MyString:

 type ( MyFloat float64 MyString string ) 

And we implement the Render methods for everyone, automatically satisfying the Renderer interface:

 func (f MyFloat) Render() { ... } func (s MyString) Render() { ... } 

And now our Display function will look like this:

 func Display(ch chan Renderer) { for v := range ch { v.Render() } } 

Much more beautiful and concise, is not it? Now, if we add another type that we need to be able to render in Display, we simply add the Render method and we don’t have to change anything else.

And, importantly, this code displays the real state of affairs - combining the types under the umbrella of the interface according to their general behavior. This behavior is orthogonal to the data itself, and now it is reflected in the code.

Interface sizes


In Go Proverbs there is such a postulate - "The larger the interface, the weaker the abstraction . " In the example above, a small interface with just one method helped to very clearly describe the abstraction of the “values ​​to be rendered”. If we created an interface with a bunch of methods that are specific to, say, string - we would not be able to use it for float64, and we would have to invent something new.

In Go, most interfaces contain 1-2 methods, no more. But this, of course, is not a ban - if you absolutely need an interface with hundreds of methods (for example, for some mocks) - this is also ok.

Who and when should create the interface?


I had an interesting discussion, during which the following statement was made - "every library in Go must export an interface . " Like, if I want to lock (mock) the functionality of the library, then it will be enough for me to simply implement this interface in my stub and test against it.

This is not true. Each library should not export an interface, and the general rule for determining who should create an interface can be described as follows:

The interface is created by the consumer (consumer), and not by the producer (producer).

If your library implements StaticType1, there is no need to invent an interface for it. If you, as a library consumer, want to abstract the behavior of a type, lock it and create StaticType2, which in your code should be interchangeable with StaticType1 - you will implement the Interface yourself, and use it yourself. This is your task - you solve it by means of the language.
In the sort library already mentioned above, the sort.Interface interface is needed for the sort.Sort function to work — that is, the library itself and is its consumer.



Summary


For all its simplicity, the type system in Go still creates some difficulties when switching from other languages. Someone is trying to squeeze it into the SOLID principle, someone is trying to make analogies with classes, someone considers specific types to be evil, and the others are good, and this creates certain difficulties for beginners. But this is normal, almost everything goes through this, and I hope that this article has clarified a bit the essence, purpose and purpose of interfaces in Go.

Summarizing, three theses:


Links


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


All Articles