Interfaces in Go

20 August 2023, Updated 20 August 2023

This is a text that I wrote back when I was teaching myself Go. I cleaned it up a bit and added some more info.

Interfaces in Go are similar to Java (and other conventional) interfaces in the sense that they define a set of functions on an object that are used to operate on some data irrespective of what the underlying data of the object actually is. That is, they define behaviour. However that's where the similarities end.

Go takes this approach to a large extent in exchange for object oriented programming principles by using it for polymorphism, abstraction and rudimentary (runtime) generics. Interfaces are a major source of where Go's flexibility come's from.

Notes on types and Other Things

types function similarly to typedefs in the C language, that is, it simply aliases a type with some identifier. The statement below aliases int to Number.

1type Number int

Struct and interface types in Go are anonymous in nature. That is, they don't have any identifier assigned to them by default. We instead use type to assign them one when we define a formal type.

1type Vector2D struct {
2	x int
3	y int
4}

As a consequence of their anonymity, we can also assign anonymous structs and interfaces directly to a variable, and use them as desired.

1var b struct{ x, y int }
2
3b.x = 4
4b.y = 5
5fmt.Println(b)
6
7b = struct{ x, y int }{ 6, 7 }
8fmt.Println(b)

Receiver functions are functions that can be used almost analogously to member functions in Object Oriented Languages. Receiver functions can be defined on any type except for interfaces and types which are pointers (like type B *A, pointer receivers like func (*A) f() {...} are fine).

1type Stars int
2
3func (s Stars) String() string {
4	return strings.Repeat("*", int(s))
5}

I would recommend that you look up how receiver functions work in Go in further detail before proceeding further.

Interfaces as Interfaces

Interfaces in Go work similar to how they work in Java. In Java, they act as reference types that only expose the functions that are defined in the interface:

 1interface B {
 2	void doSomething();
 3};
 4
 5class K implements B {
 6	public void doSomething() {
 7		System.out.println("Hi");
 8	}
 9};
10
11class A {
12	public static void main(String[] args) {
13		B b = new K();
14		System.out.println("Hello");
15		b.doSomething();
16	}
17}

Note how the interface is being used as a variable here.

Here is an analogous program in Go:

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type B interface {
 8	doSomething()
 9};
10
11type K struct {
12
13}
14
15func (_ K) doSomething() {
16	fmt.Println("Hi")
17}
18
19func main() {
20	var value K;
21	var iface B;
22	iface = value;
23	fmt.Println("Hello")
24	iface.doSomething()
25}

Let's look at another example showing the most well known approach for interfaces. The following interface defines a function for getting the magnitude from a given n-dimensional vector, and scalar values. We implement the interface for upto 3D vectors here:

 1import (
 2	"fmt"
 3	"math"
 4)
 5
 6type Magnituder interface {
 7	Magnitude() float64
 8}
 9
10type Scalar float64
11
12func (v Scalar) Magnitude() float64 {
13	return float64(v)
14}
15
16type Vector2D struct {
17	x float64
18	y float64
19}
20
21func (v Vector2D) Magnitude() float64 {
22	return math.Sqrt(v.x * v.x + v.y * v.y)
23}
24
25type Vector3D struct {
26	x float64
27	y float64
28	z float64
29}
30
31func (v Vector3D) Magnitude() float64 {
32	return math.Sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
33}
34
35func printMagnitude(v Magnituder) {
36	fmt.Println(v, v.Magnitude())
37}
38
39func main() {
40	var v1 Scalar = Scalar{2}
41	var v2 Vector2D = Vector2D{2, 2}
42	var v3 Vector3D = Vector3D{2, 2, 2}
43
44	printMagnitude(v1)
45	printMagnitude(v2)
46	printMagnitude(v3)
47}

You will notice that there is nothing in the grammar in both the examples that "says" that the receiver functions are actually implementing the interface. This thus brings us to our first takeaway:

  1. Interfaces are not coupled to their implementations, thus these receiver functions exist independently from the interface.

The compiler resolves the argument of printMagnitude and verifies that all of the functions present in Magnituder are implemented. Otherwise it throws an error.

Thus another takeaway from this is that:

  1. Interfaces are only important to the users of that interface, not the implementers. See this for more details on explicit implementation workarounds.

Inheritance and Abstraction using Interfaces

Go does not have a concept of inheritance. Struct members are instead added in by composition. Simply dropping in a struct identifier in another struct definition will make the identifiers members subscriptable with the struct being composed. This is merely syntactic sugar.

Receiver functions for the "parent" struct will also work on the composed struct without having to change the subscript. Interfaces, as inferrable from the previous section, will work as expected as well:

 1type Adder interface {
 2	add() int
 3}
 4
 5type A struct {
 6	a int
 7	b int
 8}
 9
10func (v A) add() int {
11	return v.a + v.b
12}
13
14type B struct {
15	A
16	c int
17}
18
19func (v B) add() int {
20	return v.a + v.b + v.c
21}
22
23func printAddition(a Adder) {
24	fmt.Println(a.add())
25}
26
27func main() {
28	var q A
29	q.a = 1
30	q.b = 2
31
32	fmt.Println(q, q.add())
33	printAddition(q)
34
35	var r B
36	r.a = 1
37	r.b = 2
38	r.c = 3
39
40	fmt.Println(r, r.add())
41	printAddition(r)
42}

Note how there is no relation defined betwween A and B above.

This brings us to our third takeaway:

  1. This combination of composable structs and decoupled interfaces allow us to completely discard the traditional method of creating classes, inheritance of classes and the interfaces on them. We can therefore create more generalised and flexible way of sharing functionality and schemes for structured data without having to account heavily for the relationships between them.

Rudimentary Generics using Interfaces

Until very recently, Go did not have any dedicated system for Generics that is similar to C++ or Java. However functionality similar to Generics was still possible. This should be evident if one has given thought over how fmt.Printf and other fmt package functions work.

This is achieved using an anonymous interface, interface{}, or the empty interface.

We have known that a given type is determined to implement a given interface when:

  1. We assign a concrete type/value to an interface variable, and

  2. The given concrete type implements all the functions in the interface.

interface{} is empty, as is observable. This means that all the concrete types available in Go satisfy the conditions of implementing this interface. This also means that any concrete type can be assigned to a variable of type interface{}.

The underlying concrete type can be extracted by using a type switch, documented over here.

Iterating over the types, then performing an action according to that type is how a level of generics is achieved here.

This is similar to how Generics is implemented in C11, using the _Generic keyword.

Even though Go has not had Generics for most of its history, it is still a very useful statically typed programming language and toolchain in the current era, and a lot of the common uses of Generics is covered by the above system. A nice writeup on the shortcomings is available here.