Understanding Interfaces in Go

Arun Kant Pant
Level Up Coding
Published in
6 min readJun 24, 2020

--

Interfaces might seem like a strange concept to newcomers in programming, and to programmers who have not used interfaces because they were either never too confident about them, or maybe because the tools they have used so far did not rely on using interfaces much.

Well, interfaces should be making your life easier instead of making you stress out! And I am writing this blog to show you exactly why.

We will understand the concept of interfaces, using one of my favourite games: Witcher 3, as the base.

In the game, consider the following two types of characters who often run into each other and end up fighting:
Witcher
Monsters

Both of these types of characters share some common traits:
Name
Level
Attacks
Vitality (or health)

However, they also have unique characteristics of their own. For example, a witcher may belong to a certain school and a monster will belong to a certain class of monsters.

Considering these factors, let us create our structs for a Witcher and a monster:

package mainimport "fmt"type character struct {
name string
level int
attacks map[string]int
vitality int
}
type witcher struct {
character
school string
}
type monster struct {
character
monsterType string
}
func main() {

}

Great, so we see the common traits in the character struct, with the other witcher and monster structs also having unique fields of their own besides the ones we are reusing from character.

Now, let us actually create a witcher and a monster:

...func main() { geralt := witcher{
character: character{
name: "Geralt",
level: 112,
attacks: map[string]int{"swordHeavy": 1250, "swordQuick": 750},
vitality: 3000,
},
school: "Wolf",
}
ekkimmara := monster{
character: character{
name: "Ekkimmara",
level: 114,
attacks: map[string]int{"heavySlash": 1440, "rush": 800},
vitality: 6500,
},
monsterType: "Vampire",
}
}

So we have our witcher Geralt, who has two attacks: a heavy sword attack and a quick sword attack. Both these attacks are keys in a map which correspond to the damage they will inflict. The Ekkimmara also has its own attacks and damage they will inflict.

Cool, we have attacks listed for both these characters and the damage these attacks will do.

If we think over it now, this is a common characteristic to both these characters. If they meet in combat, they can both deal damage via attacks to each other, and they can receive damage after falling victim to any attack.

This is where we might begin to think about implementing an interface for a common behaviour!

An interface will help us put both: the witcher and the ekkimmara, into a common type: combatCharacters which we can then use to share common behaviour. This can be made clearer via code:

type combatCharacters interface {

attack(attackName string) int
takeDamage(damage int)
getName() string
getVitality() int
}

An interface will hold a list of method signatures, and any struct (or type to be more precise) implementing any of these methods will also be of type combatCharacters.

In Go, interfaces are implemented implicitly. All you have to do is attach the methods defined in an interface, to a struct, for that struct to also qualify as the type mentioned by that interface.

So, if we attach the methods mentioned above to both our witcher and monster structs, any instances obtained for these structs will also be of type combatCharacters.

This will be useful in creating a generic function for inflicting attacks and receiving damage. The function can take a witcher as an attacker for example, and a monster as a victim.

A glimpse of the function we are about to create :

...
// our interface for reference
type combatCharacters interface {

attack(attackName string) int
takeDamage(damage int)
getName() string
getVitality() int
}...func doDamage(
attacker combatCharacters,
victim combatCharacters,
attackName string
) {
// since the attack method was part of our interface
// we will get the damage inflicted by an attacker
// attackName is a string and an int is returned
damageInflicted := attacker.attack(attackName) // since the takeDamage method was part of our interface
// we can use it to record damage inflicted on a victim


victim.takeDamage(damageInflicted)
}...

The function above can take any attacker having type combatCharacters and victim having type combatCharacters as arguments.

As mentioned before, this can be done by simply implementing the methods defined in our interface.

We will proceed to do this for our witcher first:

...type witcher struct {
character
school string
}
func (w witcher) attack(attackName string) int {
return w.attacks[attackName]
}
// pointer to witcher taken
// to reflect actual damage to a witcher's vitality

func (w *witcher) takeDamage(damage int) {
w.vitality = w.vitality - damage
}
func (w witcher) getName() string {
return w.name
}
func (w witcher) getVitality() int {
return w.vitality
}
...

Notice above, that we have added all the required method signatures (as per our interface) to our witcher struct, for it to qualify as the type combatCharacters.

In the takeDamage method, please note that we have used a pointer to a value of type witcher struct. This is done to actually mutate the vitality of the witcher, in case he takes damage from an attack.

Doing the same for monsters:

...type monster struct {
character
monsterType string
}
func (m monster) attack(attackName string) int {
return m.attacks[attackName]
}
func (m *monster) takeDamage(damage int) {
m.vitality = m.vitality - damage
}
func (m monster) getName() string {
return m.name
}
func (m monster) getVitality() int {
return m.vitality
}
...

Not much difference except the type of structs used. This also qualifies as the type combatCharacters now.

Now, all that’s left to add is our function to depict a clash between foes:

...func doDamage(
attacker combatCharacters,
victim combatCharacters,
attackName string
) {
damageInflicted := attacker.attack(attackName)
victim.takeDamage(damageInflicted)

fmt.Println(
attacker.getName(),
"attacked",
victim.getName(),
"did",
damageInflicted,
"damage",
)

fmt.Println(
victim.getName(),
"now has",
victim.getVitality(),
"health remaining",
)
}
...

Highlighted in bold are the methods we mentioned in our interface.

This would mean, we can now execute something like:

...doDamage(geralt, ekkimmara, "swordHeavy")...

geralt and ekkimmara, both qualifying as the combatCharacters type, will be passed into the function. geralt will be the attacker in this case, while the ekkimmara will be the victim of a “swordHeavy” attack.

Overall, our code looks like this:

package mainimport (
"fmt"
)
type combatCharacters interface {
attack(attackName string) int
takeDamage(damage int)
getName() string
getVitality() int
}
type character struct {
name string
level int
attacks map[string]int
vitality int
}
type witcher struct {
character
school string
}
func (w witcher) attack(attackName string) int {
return w.attacks[attackName]
}
func (w *witcher) takeDamage(damage int) {
w.vitality = w.vitality - damage
}
func (w witcher) getName() string {
return w.name
}
func (w witcher) getVitality() int {
return w.vitality
}
type monster struct {
character
monsterType string
}
func (m monster) attack(attackName string) int {
return m.attacks[attackName]
}
func (m *monster) takeDamage(damage int) {
m.vitality = m.vitality - damage
}
func (m monster) getName() string {
return m.name
}
func (m monster) getVitality() int {
return m.vitality
}
func doDamage(
attacker combatCharacters,
victim combatCharacters,
attackName string,
) {
damageInflicted := attacker.attack(attackName)
victim.takeDamage(damageInflicted)

fmt.Println(
attacker.getName(),
"attacked",
victim.getName(),
"did",
damageInflicted,
"damage",
)

fmt.Println(
victim.getName(),
"now has",
victim.getVitality(),
"health remaining",
)
}
func main() {
geralt := witcher{
character: character{
name: "Geralt",
level: 112,
attacks: map[string]int{"swordHeavy": 1250, "swordQuick": 750},
vitality: 3000,
},
school: "Wolf",
}
ekkimmara := monster{
character: character{
name: "Ekkimmara",
level: 114,
attacks: map[string]int{"heavySlash": 1440, "rush": 800},
vitality: 6500,
},
monsterType: "Vampire",
}
doDamage(&geralt, &ekkimmara, "swordHeavy")}

Note that we are passing the addresses of geralt and ekkimmara as arguments, as per our takeDamage method definition where we actually mutate vitality:

func (w *witcher) takeDamage(damage int) {
w.vitality = w.vitality - damage
}
...func (m *monster) takeDamage(damage int) {
m.vitality = m.vitality - damage
}

On running the complete code, you should get the following output in your terminal:

Geralt attacked Ekkimmara did 1250 damage
Ekkimmara now has 5250 health remaining

So there you have it, interfaces are great to club common behaviours and re use generic functions!

Here is the Ekkimmara attacking our Witcher:

doDamage(&ekkimmara, &geralt, "heavySlash")

And the output:

Ekkimmara attacked Geralt did 1440 damage
Geralt now has 1560 health remaining

A side note, every type will implement the empty interface: interface{}, since every type will have at least no method! So every type can qualify as the empty interface — interface{} type. This again, is because interfaces are implemented implicitly in Go. You do not explicitly tell that something uses a certain interface, by using an implements keyword or anything of the sort.

Hence, in Go’s documentations you may often find things like:

func Println(a ...interface{}) (n int, err error)

Above is the method signature for the very common fmt.Println( ).

As you know it can take a variable number of arguments of any type, the definition therefore has an empty interface as a part of it! This is done so that values of any type can be passed as arguments.

Also remember, at runtime, a value can have only one type. The Go runtime will convert a value of a different type into the interface{} type, wherever required. More on this here.

--

--

Building things, breaking things, learning things. Frontend Engineer at The Economist Intelligence Unit.