Getting started with golang interfaces
In Go , interfaces are custom types that define a set of function signatures. By design , interfaces are considered to be tools that make code clean, short and provide a good API between packages, servers and clients. Go interfaces enable developers to treat other types as one type temporarily as long as these types have the functions defined in the interface. When a type has the functions defined in an interface, we say that that type implements that interface.
When using interfaces , you are not allowed to create an instance of an interface, but rather, you can create a variable of an interface type and this variable can be assigned to a value type that implements the interface.
Therefore Go interfaces are super important for Go developers to have in their tool belt. In this article we will learn about Go interfaces and therefore it's a prerequisite that you have Go runtime installed in your computer to be able to proceed without any problems.
Golang interface syntax
It's important to remember that Go interfaces are simply a collection of named function signatures wrapped inside a custom type(interface).To define an interface use the keyword type
followed by the name of the interface and lastly the keyword interface
and curly braces. Using your preferred code editor, navigate to your working directory and create a folder called interfaces and add a main.go file in your interface directory and add the below code.
package main
type salaryCalculator interface{}
When naming your interface, the name should describe what the interface is doing. It has become a popular thing in the Go community to use the “er” suffix to the interface name where possible.
Empty interface
An empty interface is an interface that does not specify methods in it. This means that an empty interface may hold values of any type. Therefore it's worth noting that in Go , every type implements at least zero methods. Empty interfaces are useful with code that handles values of unknown type. A good example is from the fmt package
, where fmt.Print()
function takes any number of arguments of type interface{}
.The zero value of an empty interface in Go is nil
as shown below.
Example
package main
import "fmt"
type salaryCalculator interface{}
func main() {
var s salaryCalculator
fmt.Println(s)
}
Output
$ go run main.go <nil>
Interface with methods set
To add a named function signature, add the name of the function followed by round parenthesis and a return value if the function has one. Our interface has no function signature in it. To add a named method signature, add the name of the function followed by round parenthesis and a return value if the function has one.
Example
package main
type salaryCalculator interface {
calculateSalary() float64
report()
}
Explanation
In the above example, we have defined a salaryCalculator
interface that has two function signatures called calculateSalary()
and report()
. To use (implement) the interface, we need to define a type that has the two functions. It's common to define type structs in Go to implement interfaces, but other types also like string can also be used with interfaces.
Implementing an interface in golang
Example-1
In the next example, we have defined two structs namely PermanentEmployee
and ContractEmployee
. These two struct types define receiver functions with the same names (calculateSalary & report ) and return type as the once in the salaryCalculator
interface. By defining receiver functions with the same name and return types as the once in the interface, will implements the interface implicitly. This is a unique way in Go that allows developers to implement interfaces without using keywords like "implement" . The receiver functions for PermanentEmployee
and ContractEmployee
have the same signatures but different implementations. Therefore the salary calculation for PermanentEmployee
is different from the ContractEmployee
but they all perform the same task.
package main import "fmt" type salaryCalculator interface { calculateSalary() float64 report() } type PermanentEmployee struct { id int basicSalary float64 commission float64 } type ContractEmployee struct { id int basicSalary float64 } func (p PermanentEmployee) calculateSalary() float64 { return p.basicSalary + (p.commission/100)*p.basicSalary } func (c ContractEmployee) calculateSalary() float64 { return c.basicSalary } func (p PermanentEmployee) report() { fmt.Printf("Employee ID %d earns USD %f per month \n", p.id, p.calculateSalary()) } func (c ContractEmployee) report() { fmt.Printf("Employee ID %d earns USD %f per month \n", c.id, c.calculateSalary()) } func main() { var calculator salaryCalculator calculator = PermanentEmployee{id: 1, basicSalary: 10000, commission: 20} calculator.report() calculator = ContractEmployee{id: 2, basicSalary: 5000} calculator.report() }
Output:
$ go run main.go
Employee ID 1 earns USD 12000.000000 per month
Employee ID 2 earns USD 5000.000000 per month
Explanation:
In our main function, we define a calculator variable of type salaryCalculator
. In the next line, we assign the calculator variable to PermanentEmployee
struct. This is made possible due to the fact that PermanentEmployee
implements the salaryCalculator
interface.The same applies for the ContractEmployee
struct.When working with the salaryCalculator
in our example, we see that PermanentEmployee
and ContractEmployee
are one type because they share same behavior and the salaryCalculator
interface links them up. The output on the terminal prints a message for each employee. But how is this useful? Imagine you have a slice of employees , both permanent and contract , and the company they work for wants to calculate the total salary that will be paid at the end of the month. It's true that it's hectic to loop through all employees and do the math on the fly while in the loop.
Example-2
To solve this , we can create a slice of type salaryCalculator
and add employees in the slice , loop through them and calculate the total salary.
Example
package main
import "fmt"
type salaryCalculator interface {
calculateSalary() float64
report()
}
type PermanentEmployee struct {
id int
basicSalary float64
commission float64
}
type ContractEmployee struct {
id int
basicSalary float64
}
func (p PermanentEmployee) calculateSalary() float64 {
return p.basicSalary + (p.commission/100)*p.basicSalary
}
func (c ContractEmployee) calculateSalary() float64 {
return c.basicSalary
}
func (p PermanentEmployee) report() {
fmt.Printf("Employee ID %d earns USD %f per month \n", p.id, p.calculateSalary())
}
func (c ContractEmployee) report() {
fmt.Printf("Employee ID %d earns USD %f per month \n", c.id, c.calculateSalary())
}
func main() {
p1 := PermanentEmployee{id: 1, basicSalary: 2300, commission: 13}
p2 := PermanentEmployee{id: 2, basicSalary: 1500, commission: 18}
p3 := PermanentEmployee{id: 3, basicSalary: 2300, commission: 10}
c1 := ContractEmployee{id: 4, basicSalary: 500}
c2 := ContractEmployee{id: 5, basicSalary: 1100}
c3 := ContractEmployee{id: 6, basicSalary: 700}
employees := []salaryCalculator{p1, p2, p3, c1, c2, c3}
var totalSalary float64
for _, employee := range employees {
totalSalary += employee.calculateSalary()
}
fmt.Printf("Company total salary is : USD %f", totalSalary)
}
Output
$ go run main.go
Company total salary is USD 9199.000000
Example-3
To make our code shorter and cleaner, we can define a salaryExpense()
function that takes a slice of salaryCalculator
interface as an argument, and return the total salary. Add the below code to the existing code.
Example
func salaryExpense(s []salaryCalculator) float64 {
var totalSalary float64
for _, employee := range s {
totalSalary += employee.calculateSalary()
}
return totalSalary
}
func main() {
p1 := PermanentEmployee{id: 1, basicSalary: 2300, commission: 13}
p2 := PermanentEmployee{id: 2, basicSalary: 1500, commission: 18}
p3 := PermanentEmployee{id: 3, basicSalary: 2300, commission: 10}
c1 := ContractEmployee{id: 4, basicSalary: 500}
c2 := ContractEmployee{id: 5, basicSalary: 1100}
c3 := ContractEmployee{id: 6, basicSalary: 700}
employees := []salaryCalculator{p1, p2, p3, c1, c2, c3}
totalSalary := salaryExpense(employees)
fmt.Printf("Company total salary is USD %f \n", totalSalary)
}
Output
$ go run main.go
Company total salary is USD 9199.000000
Example-4: Create built-in error interface
error
is a built-in interface type in Go. A variable from the error interface represents any value that describes itself as a string.The error type has a single method called Error(). In Go , the error interface is declared as below.
type error interface {
Error() string
}
Go does not have the try catch error handling mechanism, and the way around that is to handle errors as they appear. For example, when working with files in Go, it is good to check if the file can be read from. In this scenario, we will use the built-in error interface to handle the error. In the next example, we will create our custom type that implements the error built-in type.
Example
package main
import (
"fmt"
"os"
)
type ReadFileError struct {
message string
}
func (rfe ReadFileError) Error() string {
return fmt.Sprintf("Custom Read File Error! Error Message : %s", rfe.message)
}
func errorHandler(err error) {
fmt.Println(err.Error())
}
func main() {
_, err := os.ReadFile("logs.txt")
if err != nil {
e := ReadFileError{message: "Error reading file"}
errorHandler(e)
return
}
fmt.Println("Reading data from file")
}
Explanation
In our example, we have a created custom struct type ReadFileError
. ReadFileError
has a receiver function called Error()
and it returns a string, which makes ReadFileError
struct to implement the built-in error interface implicitly. We then define the errorHandler
function that takes the built-in error type as its argument. This function only prints messages on the screen by calling err.Error()
function. In the main function, we try to read a file called “logs.txt”
that does not exist in our root folder. This will automatically return an error. We handle the error by creating our custom message e := ReadFileError{message: "Error reading file"}
then use errorHandler(e)
To print the error in the terminal.
Output
$ go run main.go
Custom Read File Error! Error Message : Error reading file
Summary
Go interface is an important tool to use as Go developer. Interfaces in Go enables developers to group together types as long as these types have the same methods defined in an interface that they are implementing. Many times Go interfaces are used to force encapsulation and allow for a versatile, clean and short code base.
References
https://go.dev/tour/methods/9
https://gobyexample.com/interfaces
Related Keywords: golang interface, go interface, interface golang, interfaces in golang, interface in go, golang interface type, golang interfaces