Golang Interfaces

Using Interfaces in Golang

In golang, the interface type is a collection of method signatures. Any type that implements the methods defined in the interface is said to “implement/satisfy that interface”. Put another way, interfaces allow you to define a set of methods that can be implemented by different types in different ways. This functionality allows you to define a single interface method that can use the underlying method from a number of different types and their differing implementations.

Empty Interfaces

The simplest interface you can create is an uninitialized empty interface:

var myInterface interface{}
fmt.Println(myInterface)
<nil>


Because the interface is empty, it defines no methods. Because it defines no methods, any type can implement/satisfy it. This is because all types implement at least zero methods.

var myInterface interface{}
myInterface = 3.14
fmt.Println(myInterface)
myInterface = "this is my interface"
fmt.Println(myInterface)
3.14
this is my interface


You can also use custom defined types:

type myType struct {
    myName string
}

func main() {
    var myInterface interface{}
    myInterface = myType{"foo"}
    fmt.Println(myInterface)
}
{foo}

Interface Values

Each instance of an interface consists of an underlying concrete value and type. You can use fmt.Printf to see these values:

type myType struct {
    myName string
}

func main() {
    var myInterface interface{}

    myInterface = 3.14
    fmt.Printf("underlying interface value: %v, underlying interface type: %T\n", myInterface, myInterface)

    myInterface = "this is my interface"
    fmt.Printf("underlying interface value: %v, underlying interface type: %T\n", myInterface, myInterface)

    myInterface = myType{"foo"}
    fmt.Printf("underlying interface value: %v, underlying interface type: %T\n", myInterface, myInterface)
}
underlying interface value: 3.14, underlying interface type: float64
underlying interface value: this is my interface, underlying interface type: string
underlying interface value: {foo}, underlying interface type: main.myType


The use of an interface grants a single variable (myInterface) the ability to hold values of different underlying types (float64, string, and the custom myType).

Type Assertions

If you wish to perform operations with these underlying interface values, you will not be able to do so directly:

var numberOne interface{} = 1
var numberTwo int = 1
mySum := numberOne + numberTwo
fmt.Println("mySum:", mySum)
invalid operation: numberOne + numberTwo (mismatched types interface {} and int)


You’ll need to use type assertions in order to access the underlying concrete type:

var numberOne interface{} = 1
var numberTwo int = 1
mySum := numberOne.(int) + numberTwo
fmt.Println("mySum:", mySum)
mySum: 2

Interface Methods

Not only can an interface hold different underlying types, an interface method can also run different underlying methods associated with these different underlying types. The classic example is that of a single shape interface with an area() method that can run the different area formulas for different underlying shapes.

type shape interface {
	area() float64
}

type circle struct {
	radius float64
}

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

type rectangle struct {
	length float64
	width  float64
}

func (r rectangle) area() float64 {
	return r.length * r.width
}

type triangle struct {
	length1 float64
	length2 float64
	length3 float64
}

func (t triangle) area() float64 {
	s := (t.length1 + t.length2 + t.length3) / 2
	return math.Sqrt(s * (s - t.length1) * (s - t.length2) * (s - t.length3))
}

func main() {
	c := circle{2}
	r := rectangle{1, 1}
	t := triangle{3, 4, 5}

	// Run the area method directly on each type
	fmt.Println("circle area:", c.area())
	fmt.Println("rectangle area:", r.area())
	fmt.Println("triangle area:", t.area())

	// Run the area method on the interface
	// The interface method will run the area()
	// method for the underlying type
	var sh shape
	sh = c
	fmt.Printf("shape type: %T  shape area: %v\n", sh, sh.area())
	sh = r
	fmt.Printf("shape type: %T  shape area: %v\n", sh, sh.area())
	sh = t
	fmt.Printf("shape type: %T  shape area: %v\n", sh, sh.area())
}
circle area: 3.141592653589793
rectangle area: 1
triangle area: 6
shape type: main.circle  shape area: 3.141592653589793
shape type: main.rectangle  shape area: 1
shape type: main.triangle  shape area: 6

Implementation Example

Interfaces are useful when you need to work with different objects that do the same thing differently.

Let’s say we have an application that is written to work with a circle object.

type circle struct {
	radius float64
}

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

func (c circle) draw() {
	// logic for drawing a circle goes here
}

The app has business logic that expects circle objects so it can draw() circles and calculate their area().

func businessLogicArea(c circle) {
	fmt.Println("using circle area for business purposes")
	_ = c.area()
}

func businessLogicDraw(c circle) {
	fmt.Println("using circle draw for business purposes")
	c.draw()
}

When we need to use the business logic, we give it circle instances.

func main() {
	ci := circle{radius: 5.0}
	businessLogicArea(ci)
	businessLogicDraw(ci)
}

The application now needs to support a new type, the square. The square does the same thing the circle does: draw() squares and calculate their area().

type square struct {
	length float64
}

func (s square) area() float64 {
	return s.length * s.length
}

func (s square) draw() {
	// logic for drawing a square goes here
}

In order to add support for this new product, we will need to introduce a shape interface. This interface defines the methods the circle and square types need in order to be considered a shape. In this case, it’s the familiar area() and draw().

type shape interface {
    area() float64
    draw()
}

Now our business logic can use this interface and work with any shape, not just the circle.

func businessLogicArea(sh shape) {
	fmt.Println("using shape area for business purposes")
	fmt.Printf("shape type is %T\n\n", sh)
	_ = sh.area()
}

func businessLogicDraw(sh shape) {
	fmt.Println("using shape draw for business purposes")
	fmt.Printf("shape type is %T\n\n", sh)
	sh.draw()
}

func main() {
	ci := circle{radius: 5.0}
	businessLogicArea(ci)
	businessLogicDraw(ci)

    sq := square{length: 5.0}
	businessLogicArea(sq)
	businessLogicDraw(sq)
}
using shape area for business purposes
shape type is main.circle

using shape draw for business purposes
shape type is main.circle

using shape area for business purposes
shape type is main.square

using shape draw for business purposes
shape type is main.square

You can run this code on the Go Playground.

Additional Reading


https://gobyexample.com/interfaces

https://tour.golang.org/methods/9

https://www.alexedwards.net/blog/interfaces-explained

https://yourbasic.org/golang/interfaces-explained/