This page looks best with JavaScript enabled

Go interfaces

 ·  🎃 kr0m

In Go, an interface is a set of methods that a type must implement.

Unlike some languages that require a class to explicitly declare that it implements an interface, in Go, the implementation of an interface is implicit. If a type implements the methods required by an interface, it is considered to implement that interface automatically.

Interfaces enable the implementation of polymorphism, which means that the same interface function can be called using different types, providing different behavior depending on the type.

They also provide a form of abstraction, hiding the implementation details and focusing on the essential functionality that an object must provide.

This article is quite extensive, so I’ve organized it as follows:


If you’re new to the world of Go, I recommend the following previous articles:


Example with Interface:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Same Code without Interface:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

func (r rectangle) measure_r() {
    fmt.Println(r)
    fmt.Println(r.area())
    fmt.Println(r.perimeter())
}

func (c circle) measure_c() {
    fmt.Println(c)
    fmt.Println(c.area())
    fmt.Println(c.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    r.measure_r()
    fmt.Println("-----")
    c.measure_c()
}

Code Execution:

With interface:

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Without interface:

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Explanation:

Any object that has the area() and perimeter() functions is considered a geometry interface.

type geometry interface {
    area() float64
    perimeter() float64
}

In this case, the rectangle and circle structures have these functions due to Go methods .

This makes the code more generic, as you only need to call the measure() function without worrying about whether it’s a rectangle (r.measure_r()) or a circle (c.measure_c()).

From main, you always call the same function, only needing to implement the functions required by the interface (area()/perimeter()) for each shape.

Additionally, the measure() function can only be used when an object that fulfills the interface is passed, as it requires that as an input parameter:

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

Interface-Type Objects:

Another way to implement the same code with interfaces but without using a function is to create interface-type objects. This way, the assignment is only possible if the source object fulfills the interface. Let’s look at the previous example.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    var g geometry = r
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    fmt.Println("-----")
    g = c
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}
{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Be careful with null objects, as Go allows them to be defined, but if you run any of their methods, the software will crash with a panic.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func main() {
    // Empty geometry object
    var g geometry
    fmt.Println(g.area())
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47a210]

goroutine 1 [running]:
main.main()
	/home/kr0m/test/test.go:36 +0x10
exit status 2

Interface Array as Function Input:

Now, suppose we want to pass a slice of geometries to sum their areas and perimeters.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func sumMeasures(geometryArray []geometry) {
    totalArea := 0.0
    totalPerimeter := 0.0

    for _, geometryObject := range geometryArray {
        totalArea += geometryObject.area()
        totalPerimeter += geometryObject.perimeter()
    }
    fmt.Println("totalArea: ", totalArea)
    fmt.Println("totalPerimeter: ", totalPerimeter)
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    // geometryArray is a slice of geometry interface type
    geometryArray := []geometry{r, c}
    sumMeasures(geometryArray)
}

We can see that the function now requires a slice of geometry:

func sumMeasures(geometryArray []geometry) {

Therefore, before calling

the function, we create that slice:

    geometryArray := []geometry{r, c}

When running the code, we get the following output:

totalArea:  90.53981633974483
totalPerimeter:  45.41592653589793

Including Interfaces within Another Interface:

Interfaces can include other interfaces; for example, we could have defined an area interface, another perimeter, and finally, a geometry that includes the previous two:

type area interface {
    area() float64
}
type perimeter interface {
    perimeter() float64
}
type geometry interface {
    area
    perimeter
}

The final code would look like this:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type area interface {
    area() float64
}

type perimeter interface {
    perimeter() float64
}

type geometry interface {
    area
    perimeter
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Run the code.

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Restricting Methods to Conform to an Interface:

Continuing with the previous example, let’s implement a new method to calculate the diameter of the circle.

func (c circle) diameter() float64 {
    return c.radius * 2
}

The final code would look like this:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}
// EXTRA METHOD
func (c circle) diameter() float64 {
    return c.radius * 2
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Now the circle implements three methods: area(), perimeter(), and diameter(). But the measure function still requires the geometry interface, which only requires the area() and perimeter() functions, which will cause the diameter() method to be excluded within the measure function.

If we modify the measure() function to use the diameter() method, it will fail with the following error:

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    fmt.Println(g.diameter())
}
./04.go:43:19: g.diameter undefined (type geometry has no field or method diameter)

Go Assertion:

Continuing with the previous example, if we need to access that method, we can use what Go calls assertions. This involves creating a new object and invoking original_object.(type), the assert will return an object with all the methods associated with the type and an ok that will indicate if the object is of that type.

With an example, it will be much easier to understand.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}
// EXTRA METHOD
func (c circle) diameter() float64 {
    return c.radius * 2
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    // NEW CODE
    if circle, ok := g.(circle); ok {
        fmt.Println(circle.diameter())
    }
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Now, when running the code, when it reaches the measure function, it will check if the geometric shape is of type circle, and if so, it will also print the diameter.

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793
10

Pointer as Receiver in an Interface Method:

When a method required by an interface has a pointer as a receiver, we must create a variable of the type that fulfills the interface method(s) and then assign the memory address of the first variable to the interface-type variable.

package main

import "fmt"

type toggleable interface {
    toggle()
}

type switchControl string
func (s *switchControl) toggle() {
    if *s == "on" {
        *s = "off"
    } else {
        *s = "on"
    }
    fmt.Println(*s)
}

func main() {
    var t toggleable
    s := switchControl("off")
    t = &s
    t.toggle()
    t.toggle()
}

Run the code and get:

on
off

Empty Interfaces:

There are times when a function may require an empty interface, which implies that it will accept any type of input. If this input is also variadic , it will allow any number of inputs of any type; a real case of this type of function is fmt.Println.

Remember that by requiring the interface to be fulfilled by the function’s input parameters, only the methods indicated in the interface will be available unless we use assertions to retrieve it. Since this is an empty interface, there will be no methods available.

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    fmt.Println(thing)
}

func main() {
    acceptAnything(3.1415)
    acceptAnything("Whatsahh")
}

If we run the code, we get:

3.1415
Whatsahh

However, if we try to call the method of the object passed to the acceptAnything() function, it will give an error:

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    thing.printMethod()
}

type testType string
func (t testType) printMethod() {
    fmt.Println(t)
}

func main() {
    testVar := testType("AA")
    acceptAnything(testVar)
}
./08.go:7:11: thing.printMethod undefined (type interface{} has no field or method printMethod)

If we use the assertion, we can execute the method without problems.

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    if testVar, ok := thing.(testType); ok {
        testVar.printMethod()
    } else {
        fmt.Println(thing)
    }
}

type testType string
func (t testType) printMethod() {
    fmt.Println(t)
}

func main() {
    testVar := testType("AA")
    acceptAnything(testVar)
    acceptAnything("Whatsahh")
}

If we run the code, we get:

AA
Whatsahh

Type Switch:

It is possible to use the type switch to compare types; we just need to use the assertion . In the following example, we create a function with an empty interface to accept any type of data input and, using the assertion in the switch,

determine the variable type.

package main

import (
    "fmt"
)

func findType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {
    findType("Naveen")
    findType(77)
    findType(89.98)
}

When running the code, we get:

I am a string and my value is Naveen
I am an int and my value is 77
Unknown type

The types we can compare are not limited to primitive types; we can also do it against interfaces.

package main

import (
    "fmt"
)

type Describer interface {
    Describe()
}

type Person struct {
    name string
    age  int
}

func (p Person) Describe() {
    fmt.Printf("%s is %d years old.\n", p.name, p.age)
}

func findType(i interface{}) {
    switch v := i.(type) {
    // case comparison against primitive type: string
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
        fmt.Printf("I am a string and my value is %s\n", v)
    // case comparison against primitive type: int
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
        fmt.Printf("I am an int and my value is %d\n", v)
    // case comparison against interface type: Describer
    case Describer:
        fmt.Printf("I am a Describer and my value is %v\n", i.(Describer))
        fmt.Printf("I am a Describer and my value is %v\n", v)
        v.Describe()
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    findType("Naveen")
    findType(77)
    findType(89.98)
    person := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(person)
}

When running the code, we get:

I am a string and my value is Naveen
I am a string and my value is Naveen
I am an int and my value is 77
I am an int and my value is 77
Unknown type
I am a Describer and my value is {Naveen R 25}
I am a Describer and my value is {Naveen R 25}
Naveen R is 25 years old.

We slightly modified the switch so that in each case, the value of the received variable is available directly in the v variable.

If you liked the article, you can treat me to a RedBull here