13 minutes read

Occasionally, you don't want to think about the particular type of value you use in your program. You just need to know that it will be able to do certain things; for example, that you'll be able to call specific methods on it. It doesn't matter whether you have a Printer or a Plotter, you just need an object that has a PrintImage() method. That's what Go interfaces accomplish they define a set of methods that certain types are expected to have.

In this topic, we will learn about the interface type in Go. We'll begin with the most basic way to implement an interface, and wrap up the topic with a practical example of how to use interfaces when working with similar struct types.

Interfaces in Go

An interface is a collection of methods that describes the behavior of an object in object-oriented programming languages or of a certain type — usually a struct in Go.

A Go interface is a type that defines a collection of methods. However, it's not a value itself, but a box for a value. To use interfaces in the code, we need to have some other type like struct or an alias to a primitive type in order to implement all the methods from the interface.

By the official Go docs convention, one-method interfaces are named by the method name plus an -er suffix, or a similar modification to construct an agent noun: for example, Scanner, Writer, Formatter.

Even though it is not obligatory to add the -er suffix to every interface we create, we should still use meaningful names that appropriately describe the interfaces we create.

Now let's take a look at how we can create a simple one-method interface in Go:

package main

import "fmt"

type Speaker struct{}

func (s Speaker) MakeSound() {
    fmt.Println("Boom-Boom! 🔊")
}

type Bell string

func (b Bell) MakeSound() {
    fmt.Println("Ting-Ting! 🔔")
}

type SoundMaker interface {
    MakeSound()
}

func main() {
    var s SoundMaker

    s = Bell("Acme Bell")
    s.MakeSound() // Ting-Ting! 🔔

    s = Speaker{}
    s.MakeSound() // Boom-Boom! 🔊
}

In the above example, we have the Bell and the Speaker types, each having a MakeSound() method. Since both of these types have the same method signature, we can create the SoundMaker interface and implement it on both of them.

To do this, we declare s as a SoundMaker interface type and assign it to the Bell and the Speaker types. Finally, we call the MakeSound() method on s and see the different “sound” outputs of each type.

Take notice that to implement an interface properly, the types we create should have all the methods from the interface, with the same signature for each method. Any type that has all the methods listed in an interface definition is said to "satisfy the interface". If the signature for one of the methods differs from the interface, we can't use the type as the given interface.

Empty interfaces

An interface that does not specify any methods is an empty interface, and it is represented by the interface{} syntax. Two important details regarding empty interfaces are that they can hold values of any type, and their default value is nil.

To further explain this, let's take a look at an example of an empty interface:

...

func describe(i interface{}) {
    fmt.Printf("Value -> %v | Type -> %T\n", i, i)
}

func main() {
    var i interface{}
    describe(i) // Value -> <nil> | Type -> <nil>

    i = 42
    describe(i) // Value -> 42 | Type -> int

    i = "hello"
    describe(i) // Value -> hello | Type -> string
}

Another example of a use case of empty interfaces is within the fmt.Print() family of functions:

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

As you can see, the Print() function takes any number of arguments of the type interface{}.

Use cases of interfaces

So far, we've seen very simple interface implementations. However, we often need to create more complex interfaces in real-world scenarios.

For example, we might want to create an interface that helps us calculate the monthly salary expense generated by different types of employees in a company:

type EmployeeContract struct {
    EmpID         int
    Salary        float64
    TaxPercentage float64
}

type FreelancerContract struct {
    EmpID       int
    HourlyRate  float64
    HoursWorked int
}

// 'EmployeeContract' Salary is the base salary - calculated taxes
func (e EmployeeContract) CalculateSalary() float64 {
    return e.Salary - (e.TaxPercentage * e.Salary)
}

// 'FreelancerContract' salary is the hourly rate * hours worked -- they declare taxes themselves
func (f FreelancerContract) CalculateSalary() float64 {
    return f.HourlyRate * float64(f.HoursWorked)
}

type SalaryCalculator interface {
    CalculateSalary() float64
}

After creating the two different types of employee contracts, we can use the SalaryCalculator interface within the main() function of our program:

...

func main() {
    homer := EmployeeContract{EmpID: 1, Salary: 479.60, TaxPercentage: 0.245}
    fmt.Printf("Homer's salary: $%.2f\n", homer.CalculateSalary()) // Homer's salary: $362.10

    deadpool := FreelancerContract{EmpID: 2, HourlyRate: 50_000.00, HoursWorked: 10}
    fmt.Printf("Deadpool's salary: $%.2f\n", deadpool.CalculateSalary()) // Deadpool's salary: $500000.00

    employees := []SalaryCalculator{homer, deadpool}
    fmt.Printf("Monthly sal. expense: $%.2f\n", salaryExpense(employees)) // Monthly sal. expense: "$500362.10
}

// salaryExpense() takes a slice of []SalaryCalculator and returns the sum of all salaries
func salaryExpense(salaries []SalaryCalculator) float64 {
    totalExpense := 0.0
    for _, v := range salaries {
        totalExpense += v.CalculateSalary()
    }
    return totalExpense
}

Let's examine the above code. Since the salaryExpense() function takes the slice employees of the SalaryCalculator interface as an argument; we can pass in any type of employee contract as long as they implement the SalaryCalculator interface!

The main advantage of being able to pass in any of the employee contract types is that we can extend salaryExpense() to any new employee type we want to create without changing any of its code.

For example, we could create a new employee contract type InternContract that could have a salary or not (depending on their contract). We can just add this new InternContract type to the employees slice argument of salaryExpense(). This way, the program will calculate the monthly salary expenses without any difficulties since InternContract will also implement the SalaryCalculator interface.

Conclusion

In this topic, we have learned about the interface type in Go and how to implement it. The key details of using the interface type in a Go program are the following:

  • To start using interfaces in a Go program, we first need to define a specific type like a struct or an alias to a primitive type to implement all the methods from the interface;

  • The types we create must have all the methods from the interface, with the same signature for each method.

Apart from the standard interfaces, we've also learned about empty interfaces (interfaces that do not specify any methods) — we can create them via the interface{} syntax. Remember that empty interfaces can hold values of any type, and their default value is nil!

Enough reading for now! It's time to test our knowledge and solve some theory and coding tasks to make sure we've learned how to properly use the interface type in our upcoming Go projects.

39 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo