Interfaces in Go - 1

In this first post about Interfaces, we will learn about interfaces in Go from scratch and understand why we need them by seeing a problem that they solve. In future posts, we will see more advanced use cases of Interface, like how they help in making our Go code well-designed, flexible and extensible. We will also cover its benefits in making our code easy to test. Let's consider an example of a sports arena to understand interfaces. This arena should allow different types of fighters to compete with each other. Translating this concept into a function, the LetsFight function is our arena where the fights happen:

func LetsFight(w Wrestler) {
	w.Fight()
}

The LetsFight function takes a concrete type(Wrestler) as an argument and calls the Fight method on the wrestler.

type Wrestler struct{
	Name string
}
 
func(w Wrestler) Fight(){
	fmt.Println(w.Name, "is fighting")
}

When we pass a Wrestler to LetsFight function, our code compiles and we get the expected result.

func main(){
	w := Wrestler{Name: "Ian"}
	LetsFight(w)
}

At this point, our LetsFight function is limited as it allows only a Wrestler to fight in the arena. What if other types of athletes like a Boxer or a Kickboxer also wants to fight in the same arena? If we try to pass a Boxer to our LetsFight function, we would get a compilation error because the LetsFight function is only taking a Wrestler as an argument.

How can we then make our LetsFight function more flexible and extensible to let fighters of different fighting styles compete in the same arena? Because in future there might be more fighters of different fighting styles interested to compete in our arena. We straightaway realize that if we could somehow abstract all these fighters as a single type and if we can pass that abstracted type to our LetsFight function, our problem can be solved! But to span the abstraction across different kinds of fighters, there needs to be at least one common aspect in between them. Only then we can take advantage of the commonality across our concrete types(Wrestler, Kickboxer, Boxer) and make a separate type which completes our abstraction.

type Boxer struct{
	Name string
}
func(b Boxer) Fight(){
	fmt.Println(b.Name, "is fighting")
}
 
type Kickboxer struct{
	Name string
}
func(k Kickboxer) Fight(){
	fmt.Println(k.Name, "is fighting")
}

If we look closely at different types of fighters, we see that there is one thing common in between them, they all fight. All the three concrete types in this case have a common method Fight . We can make use of this commonality for the purpose of our abstraction. But should we? Let's say if we take account of a different thing which is common in these concrete types, then we can see that all these structs have the same definition(they all consist a Name ). So why don't we consider making the abstraction across these types on the basis of the structure of the type itself? Before answering this, lets reflect back on what our LetsFight function is concerned with. That function only cares about the behavior of these types, not what these types look like. So, if we make our abstraction on the basis of behavior of these types, we can swap different types along with their structure as long as they still have the common behavior with which our LetsFight function is concerned about(Fight in this case). This makes sense, as in the future, our concrete types can change their structure to meet different requirements. So, by focusing on the behavior as our abstraction point, we can make our code more extensible and flexible. To implement this logic, we make use of Interfaces, which are another genre of types in Go. They have different methods in their method set, this method set collectively is the common behavior based on which we want to do abstraction of different concrete types.

type Fighter interface{
	Fight()
}

The types which have the method/s defined in the method set of Fighter interface are said to be implementing the Fighter interface. All the three types discussed above have a Fight method and thus they all implement the Fighter interface. In this way, we have successfully abstracted a different type(interface) based on the behavior of other types. Next, we can edit our LetsFight function to take an argument of this interface type instead of a particular concrete type(as it was doing earlier).

func LetsFight(f Fighter){
	f.Perform()
}

Now, we are able to pass a Wrestler, Boxer, and a Kickboxer to the LetsFight function as they meet the type requirement here(they all implement the Fighter interface) . Also, our Fighter instance doesn't care about all the methods of the concrete types. It just cares about the methods in its method set which are implemented by types. It makes sense, because the concrete types can have different methods(and thus different behavior). In other words, an interface specifies a particular behavior set which can be implemented by different concrete types which might have very different behavior other than the common behavior as specified by the interface which they implement.

Implicit Implementation of Interface

In Go, we don't have to specify that a concrete type is implementing an interface. If the concrete type has all the methods(with same signature) as defined in the method set of an interface, that type is considered to implement the interface implicitly. This check happens at compile time in Go where the compiler checks if a type implements all behaviors specified in the interface. Our concrete types do not need to know about the interface, and we can write interfaces for the types which already exist.

Note: Please zoom-in to view the image clearly.