2.8. Interfaces

Basics

Interfaces are used to express general behaviour across multiple types. Interface types are defined as a set of method signatures. All types which implement this set of methods implement the defined interface.

As an example we define the interface Stringer:

type Stringer interface {
	String() string
}

All types that implement the method String() and return a string implement the Stringer interface. Types implicitly implement an interface if they implement the required methods. This is different to many other language where we have to explicitly specify that a certain type implements an inteface (e.g. in Java with implements).

In the following example User and Group implement the Stringer interface. The PrintAll function expects a slice of Stringers. This way we can pass users, groups and every other type which implements the Stringer interface to the PrintAll function. In the main function we then initialize a user and a group and put them both into a slice of Stringers which we then pass to the PrintAll function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

// interface type Stringer
type Stringer interface {
	String() string
}

// concrete type User
type User struct {
	Username string
}

func (u *User) String() string {
	return u.Username
}

// concrete type Group
type Group struct {
	Groupname string
}

func (g *Group) String() string {
	return g.Groupname
}

// a function which takes a list of Stringers
func PrintAll(items []Stringer) {
	for _, item := range items {
		// the only thing we know about the items is that they have a method String()
		text := item.String()
		fmt.Println(text)
	}
}

func main() {
	user1 := &User{
		Username: "andrea",
	}

	group1 := &Group{
		Groupname: "admins",
	}

	myItems := []Stringer{user1, group1}

	PrintAll(myItems)
}
Output:
andrea
admins

Any (Empty Interface)

The interface any is an alias for the empty interface interface{}. This alias got introduced in Go 1.18. Before we just wrote interface{} instead.

As the name says the empty interface does not contain any method signatures. Hence all types implement the empty interface.

Any is used by functions which can handle any or at least multiple types. An example for this is the Marshal function from the encoding/json package which serializes a type into its JSON representation.

Its function signature looks as follows:

func Marshal(v any) ([]byte, error)

It takes any type and returns the serialized data ([]byte) or an error (error) if something went wrong during the serialization.

Type Assertion 🌶️

With a type assertion we can obtain the underlying concrete type of an interface type during runtime. In the following example we have a variable of type Stringer. Stringer is an interface type. With type assertion we can check if the variable is a User. If the variable is a User then ok is true and the User is returned.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

type Stringer interface {
	String() string
}

type User struct {
	Username string
}

func (u *User) String() string {
	return u.Username
}

func main() {
	var myStringer Stringer = &User{
		Username: "simone",
	}

	// type assertion
	user, ok := myStringer.(*User)

	if ok {
		fmt.Println(user.Username)
	} else {
		fmt.Println("myStringer is not of type User")
	}
}
Output:
simone

Type Switch 🌶️

With a type switch we can check of which type a certain interface type is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func printType(item any) {
	switch val := item.(type) {
	case int:
		fmt.Println("type is int")
	case []int:
		fmt.Printf("int list with length %d\n", len(val))
	default:
		fmt.Println("unexpected type")
	}
}

func main() {
	printType(1)
	printType([]int{1, 2, 3, 4})
	printType(true)
}
Output:
type is int
int list with length 4
unexpected type