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.
types
and Other Thingstype
s function similarly to typedef
s 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 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:
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:
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:
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:
We assign a concrete type/value to an interface variable, and
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.