Golang: Interfaces

Golang: Interfaces

Understanding the basics of interfaces in Golang. Exploring the concept of polymorphism in golang with the help of interfaces and structs.

Introduction

In the 19th post of the series, we will be taking a look into interfaces in golang. Interfaces allow us to create function signatures common to different structs or types. So, we can allow multiple structs to have a common interface(method) that can have different implementations.

What are Interfaces

Interface as the name suggests is a way to create methods that are common to different structures or types but can have different implementations. It's an interface to define the method or function signatures but not the implementation. Let's take an example of Laptop and Phone having the functionality of wifi. We can connect to wifi more or the less in a similar way on both devices. The implementation behind the functionality might be different but they share the same operation. The WiFi can act as an interface for both devices to connect to the internet.

Define an Interface

To declare an interface in golang, we can use the interface keyword in golang. An interface is created with the type keyword, providing the name of the interface and defining the function declaration. Inside the interface which acts as a struct of general method signatures. The method signatures usually consist of the name of the function with its parameters if any and the return type of the function.


package main

import "fmt"


type Player struct {
    name   string
    health int
}

type Mob struct {
    name     string
    health   int
    is_passive bool
}

type Creature interface {
    intro() string
    //attack() int
    //heal() int
}

func main() {
    player := Player{name: "Steve"}
    mob := Mob{name: "Zombie"}
    fmt.Println(player)
    fmt.Println(mob)
}
go run main.go

{Steve 0}
{Zombie 0 false}

In this above example, we have created an interface called Creature. There are a few structs that we have to define like Player and Mob, these two methods have a few attributes like name as string and health as int which are common in both structs but the Mob struct has an additional attribute is_passive as a boolean value. The Creature is an interface that will define certain function signatures, here we have declared intro, attack, and heal as the methods bound to the Creature interface. This means, that any object which satisfies the Creature interface, can define the methods associated with the interface.

Defining Interfaces

Once we have declared the interface method signatures, we can move into defining the functionality of these methods depending on the struct. If we want a different working method for different types of struct objects passed we can define those for each type of struct that we want. Here, we have two types of structs namely Creature and Mob, based on the struct we can define the intro method for them individually.

package main

import "fmt"

type Creature interface {
    intro() string
    attack(*int) int
}

type Player struct {
    name   string
    health int
}

type Mob struct {
    name     string
    health   int
    category bool
}

func (p Player) intro() string {
    fmt.Println("Player has spawned")
    return p.name
}

func (p Player) attack(m_health *int) int {
    fmt.Println("Player has attacked!")
    *m_health = *m_health - 50
    return *m_health
}

func (m Mob) intro() string {
    fmt.Printf("A wild %s has appeared!\n", m.name)
    return m.name
}
func (m Mob) attack(p_health *int) int {
    fmt.Printf("%s has attacked you! -%d\n", m.name, 30)
    *p_health = *p_health - 30
    return *p_health
}

func main() {
    player := Player{name: "Steve", health: 100}
    mob := Mob{name: "Zombie", health: 140}
    fmt.Println(player.intro())
    fmt.Println(mob.intro())
    fmt.Println(mob)
    fmt.Println(player)
    fmt.Println(player.attack(&mob.health))
    fmt.Println(mob.attack(&player.health))
    fmt.Println(mob)
    fmt.Println(player)
}
go run main.go

Player has spawned
Steve
A wild Zombie has appeared!
Zombie
{Zombie 140 false}
{Steve 100}
Player has attacked!
90
Zombie has attacked you! -30
70
{Zombie 90 false}
{Steve 70}

As we can see, the method intro() is bound to both the struct depending on what struct signature is associated with the method. The method intro takes in the object struct associated as per the call and returns string as defined in the method signature.

The attack method in the Creature interface is also implemented separately for the two structs. For the Player method, we simply take in a pointer to an integer and return an int. The parameter is the pointer to the mob health, and it returns the modified health. We take in a pointer to the mob or player health so as to parse in the actual value and not the copy of the value. If we modify the value, we want to reflect those changes in the actual object. So that is how we can use interfaces to construct dynamic operations on objects as well as different types of structs.

We have seen a simple example of how to declare and define interfaces for given type structs. Also, we can pass by value as well as by pointers so as to define the behavior of the method whether to dynamically modify or change the values of the object associated with it.

Examples of Interfaces

There are quite some use cases of interfaces, in object-oriented programming, the above example fits the polymorphism feature quite well. The ability to reuse certain method signatures and define the functions as per requirement brings flexibility to the code structure.

We will see a few examples for understanding how we can use interfaces in various ways.

Type Switch Interface

We can use an empty interface to check for the type of variable we have parsed. Using this empty interface we can create a kind of dynamic parameter to a function.

package main

import (
    "fmt"
    "strconv"
)

func parse_int(n interface{}) int {
    switch n.(type) {
    case int:
        return (n).(int) * (n).(int)
    case string:
        s, _ := strconv.Atoi(n.(string))
        return s
    case float64:
        return int(n.(float64))
    default:
        return n.(int)
    }
}

func main() {
    num := parse_int(4)
    fmt.Println(num)
    num = parse_int("4")
    fmt.Println(num)
    num = parse_int(4.1243)
    fmt.Println(num)
}
go run main.go

16
4
4

Here, we can see we have an interface as a parameter to the function parse_int, the return type is int, so the incoming parameter can be any valid type. But if we don't convert the given type into an appropriate int, it will result in an error as we are returning the int value of the parsed parameter. We are taking the parameter as interface{} which is an empty interface, this will contain the parameter parsed as an interface type. That's why we need to convert the interface object into an int or the parsed type of the interface.

Interface Slice

We can even create a slice of interfaces, which means we can initialize or group together various objects of different structs in a single slice. This might be helpful for calling functions associated with different objects via the interface very easily and in a much cleaner way.

package main

import "fmt"

type Creature interface {
    intro() string
}

type Player struct {
    name   string
    health int
}

type Mob struct {
    name     string
    health   int
    category bool
}

func (p Player) intro() string {
    fmt.Println("Player has spawned")
    return p.name
}

func (m Mob) intro() string {
    var name string
    if m.name != "" {
        name = m.name
    } else {
        name = "Mob"
    }
    fmt.Printf("A wild %s has appeared!\n", name)
    return m.name
}


func main() {
    entity := []Creature{Player{}, Mob{}, Mob{}, Player{}}

    for _, obj := range entity {
        fmt.Println(obj.intro())
    }
}
Player has spawned
A wild Zombie has appeared!
A wild Zombie has appeared!
Player has spawned

In the above example, we can see that the entity variable is created as a slice of interfaces Creature, i.e. various objects associated with the Creature interface can be contained in a single slice. There are 2 instances of Player and Mob each in the slice. We can further iterate over the slice as a range-based loop and thereby the functions associated with the interfaces can be called. Here, we have called the intro function.

So, there are a lot of things that can be done with interfaces, we can create multiple interfaces for a single object struct and nest interfaces. Based on the use case of the program, interfaces can be used to reduce the boilerplate code as well as improve the readability of the code.

That's it from this part. Reference for all the code examples and commands can be found in the 100 days of Golang GitHub repository.

Conclusion

From this part of the series, we were able to understand the basics of interfaces using a few examples. We explored how interfaces can be used to bring in polymorphism in golang, also we can improve the readability of the code. The boilerplate code can be considerably reduced by using interfaces when dealing with structs and types. Hopefully, you found this post helpful and understood even the basics of interfaces in golang. Thank you for reading, if you have any queries, questions, or feedback, you can ping me on my social handles or in the comments. Happy Coding :)series: "['100-days-of-golang']"