DEV Community

Cover image for Go Course: Interfaces
Karan Pratap Singh
Karan Pratap Singh

Posted on • Originally published at karanpratapsingh.com

Go Course: Interfaces

In this section, let's talk about the interfaces.

What is an interface?

So, an interface in Go is an abstract type that is defined using a set of method signatures. The interface defines the behavior for similar types of objects.

Here, behavior is a key term that we will discuss shortly.

Let's take a look at an example to understand this better.

One of the best real-world examples of interfaces is the power socket. Imagine that we need to connect different devices to the power socket.

no-interface

Let's try to implement this. Here are the device types we will be using.

type mobile struct {
    brand string
}

type laptop struct {
    cpu string
}

type toaster struct {
    amount int
}

type kettle struct {
    quantity string
}

type socket struct{}
Enter fullscreen mode Exit fullscreen mode

Now, let's define a Draw method on a type, let's say mobile. Here we will simply print the properties of the type.

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %d", m, m.brand, power)
}
Enter fullscreen mode Exit fullscreen mode

Great, now we will define the Plug method on the socket type which accepts our mobile type as an argument.

func (socket) Plug(device mobile, power int) {
    device.Draw(power)
}
Enter fullscreen mode Exit fullscreen mode

Let's try to "connect" or "plug in" the mobile type to our socket type in the main function.

package main

import "fmt"

func main() {
    m := mobile{"Apple"}

    s := socket{}
    s.Plug(m, 10)
}
Enter fullscreen mode Exit fullscreen mode

And if we run this we'll see the following.

$ go run main.go
main.mobile -> brand: Apple, power: 10
Enter fullscreen mode Exit fullscreen mode

This is interesting, but let's say now we want to connect our laptop type.

package main

import "fmt"

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50) // Error: cannot use l as mobile value in argument
}
Enter fullscreen mode Exit fullscreen mode

As we can see, this will throw an error.

What should we do now? Define another method? Such as PlugLaptop?

Sure, but then every time we add a new device type we will need to add a new method to the socket type as well and that's not ideal.

This is where the interface comes in. Essentially, we want to define a contract that, in the future, must be implemented.

We can simply define an interface such as PowerDrawer and use it in our Plug function to allow any device that satisfies the criteria, which is that the type must have a Draw method matching the signature that the interface requires.

And anyways, the socket doesn't need to know anything about our device and can simply call the Draw method.

interface

Now let's try to implement our PowerDrawer interface. Here's how it will look.

The convention is to use "-er" as a suffix in the name. And as we discussed earlier, an interface should only describe the expected behavior. Which in our case is the Draw method.

interface-implementation

type PowerDrawer interface {
    Draw(power int)
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to update our Plug method to accept a device that implements the PowerDrawer interface as an argument.

func (socket) Plug(device PowerDrawer, power int) {
    device.Draw(power)
}
Enter fullscreen mode Exit fullscreen mode

And to satisfy the interface, we can simply add Draw methods to all the device types.

type mobile struct {
    brand string
}

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %d\n", m, m.brand, power)
}

type laptop struct {
    cpu string
}

func (l laptop) Draw(power int) {
    fmt.Printf("%T -> cpu: %s, power: %d\n", l, l.cpu, power)
}

type toaster struct {
    amount int
}

func (t toaster) Draw(power int) {
    fmt.Printf("%T -> amount: %d, power: %d\n", t, t.amount, power)
}

type kettle struct {
    quantity string
}

func (k kettle) Draw(power int) {
    fmt.Printf("%T -> quantity: %s, power: %d\n", k, k.quantity, power)
}
Enter fullscreen mode Exit fullscreen mode

Now, we can connect all our devices to the socket with the help of our interface!

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}
    t := toaster{4}
    k := kettle{"50%"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50)
    s.Plug(t, 30)
    s.Plug(k, 25)
}
Enter fullscreen mode Exit fullscreen mode

And just as we expected, it works.

$ go run main.go
main.mobile -> brand: Apple, power: 10
main.laptop -> cpu: Intel i9, power: 50
main.toaster -> amount: 4, power: 30
main.kettle -> quantity: Half Empty, power: 25
Enter fullscreen mode Exit fullscreen mode

But why is this considered such a powerful concept?

Well, an interface can help us decouple our types. For example, because we have the interface, we don't need to update our socket implementation. We can just define a new device type with a Draw method.

Unlike other languages, Go Interfaces are implemented implicitly, so we don't need something like an implements keyword. This means that a type satisfies an interface automatically when it has "all the methods" of the interface.

Empty Interface

Next, let's talk about the empty interface. An empty interface can take on a value of any type.

Here's how we declare it.

var x interface{}
Enter fullscreen mode Exit fullscreen mode

But why do we need it?

Empty interfaces can be used to handle values of unknown types.

Some examples are:

  • Reading heterogeneous data from an API.
  • Variables of an unknown type, like in the fmt.Prinln function.

To use a value of type empty interface{}, we can use type assertion or a type switch to determine the type of the value.

Type Assertion

A type assertion provides access to an interface value's underlying concrete value.

For example:

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode

This statement asserts that the interface value holds a concrete type and assigns the underlying type value to the variable.

We can also test whether an interface value holds a specific type.

A type assertion can return two values:

  • The first one is the underlying value.
  • The second is a boolean value that reports whether the assertion succeeded.
s, ok := i.(string)
fmt.Println(s, ok)
Enter fullscreen mode Exit fullscreen mode

This can help us test whether an interface value holds a specific type or not.

In a way, this is similar to how we read values from a map.

And If this is not the case then, ok will be false and the value will be the zero value of the type, and no panic will occur.

f, ok := i.(float64)
fmt.Println(f, ok)
Enter fullscreen mode Exit fullscreen mode

But if the interface does not hold the type, the statement will trigger a panic.

f = i.(float64)
fmt.Println(f) // Panic!
Enter fullscreen mode Exit fullscreen mode
$ go run main.go
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
Enter fullscreen mode Exit fullscreen mode

Type Switch

Here, a switch statement can be used to determine the type of a variable of type empty interface{}.

var t interface{}
t = "hello"

switch t := t.(type) {
case string:
    fmt.Printf("string: %s\n", t)
case bool:
    fmt.Printf("boolean: %v\n", t)
case int:
    fmt.Printf("integer: %d\n", t)
default:
    fmt.Printf("unexpected: %T\n", t)
}
Enter fullscreen mode Exit fullscreen mode

And if we run this, we can verify that we have a string type.

$ go run main.go
string: hello
Enter fullscreen mode Exit fullscreen mode

Properties

Let's discuss some properties of interfaces.

Zero value

The zero value of an interface is nil.

package main

import "fmt"

type MyInterface interface {
    Method()
}

func main() {
    var i MyInterface

    fmt.Println(i) // Output: <nil>
}
Enter fullscreen mode Exit fullscreen mode

Embedding

We can embed interfaces like structs.

For example

type interface1 interface {
    Method1()
}

type interface2 interface {
    Method2()
}

type interface3 interface {
    interface1
    interface2
}
Enter fullscreen mode Exit fullscreen mode

Values

Interface values are comparable.

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct{}

func (MyType) Method() {}

func main() {
    t := MyType{}
    var i MyInterface = MyType{}

    fmt.Println(t == i)
}
Enter fullscreen mode Exit fullscreen mode

Interface Values

Under the hood, an interface value can be thought of as a tuple consisting of a value and a concrete type.

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct {
    property int
}

func (MyType) Method() {}

func main() {
    var i MyInterface

    i = MyType{10}

    fmt.Printf("(%v, %T)\n", i, i) // Output: ({10}, main.MyType)
}
Enter fullscreen mode Exit fullscreen mode

With that, we covered interfaces in Go.

It's a really powerful feature, but remember, "Bigger the interface, the weaker the abstraction" - Rob Pike.


This article is part of my open source Go Course available on Github.

GitHub logo karanpratapsingh / learn-go

Master the fundamentals and advanced features of the Go programming language

Learn Go

Hey, welcome to the course, and thanks for learning Go. I hope this course provides a great learning experience.

This course is also available on my website and as an ebook on leanpub. Please leave a ⭐ as motivation if this was helpful!

Table of contents

What is Go?

Go (also known as Golang) is a programming language developed at Google in 2007 and open-sourced in 2009.

It focuses on simplicity, reliability, and efficiency. It was designed to combine the efficacy, speed…




Top comments (0)