In this section, let’s talk about interfaces.
What is an interface?
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.
Behavior is a key term - interfaces describe what types can do, not what they are.
Let’s take a look at an example to understand this better.
Power Socket Example
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.
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{}
Now, let’s define a Draw method on the mobile type:
func (m mobile) Draw(power int) {
fmt.Printf("%T -> brand: %s, power: %d", m, m.brand, power)
}
We can 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)
}
Let’s try to “connect” or “plug in” the mobile type to our socket type:
package main
import "fmt"
func main() {
m := mobile{"Apple"}
s := socket{}
s.Plug(m, 10)
}
And if we run this:
$ go run main.go
main.mobile -> brand: Apple, power: 10
This works, but what if 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
}
This will throw an error because the Plug method only accepts mobile type.
Using the PowerDrawer Interface
This is where the interface comes in. We want to define a contract that must be implemented.
We can define an interface such as PowerDrawer and use it in our Plug function to allow any device that satisfies the criteria - the type must have a Draw method matching the signature that the interface requires.
The socket doesn’t need to know anything about our device and can simply call the Draw method.
Here’s our PowerDrawer interface:
The convention is to use “-er” as a suffix in the interface name. An interface should only describe the expected behavior.
type PowerDrawer interface {
Draw(power int)
}
Now, we update our Plug method to accept a device that implements the PowerDrawer interface:
func (socket) Plug(device PowerDrawer, power int) {
device.Draw(power)
}
To satisfy the interface, we add Draw methods to all 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)
}
Now, we can connect all our devices to the socket:
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)
}
And it works as expected:
$ 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
Why are interfaces powerful?
An interface helps us decouple our types. 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. We don’t need an implements keyword. A type satisfies an interface automatically when it has all the methods of the interface.
Empty Interface
An empty interface can take on a value of any type.
Here’s how we declare it:
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.Println 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)
}
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)
This helps us test whether an interface value holds a specific type or not (similar to reading values from a map).
If the assertion fails, ok will be false and the value will be the zero value of the type, with no panic:
f, ok := i.(float64)
fmt.Println(f, ok)
If the interface does not hold the type and we don’t use the two-value form, the statement will trigger a panic:f = i.(float64)
fmt.Println(f) // Panic!
$ go run main.go
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
Type Switch
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)
}
Running this verifies we have a string type:
$ go run main.go
string: hello
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>
}
Embedding
We can embed interfaces like structs:
type interface1 interface {
Method1()
}
type interface2 interface {
Method2()
}
type interface3 interface {
interface1
interface2
}
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)
}
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)
}
“Bigger the interface, the weaker the abstraction” - Rob Pike