Introduction
I see a lot of developers coming to Go from object oriented programing languages such as C# and Java. Because these developers have been trained to use type hierarchies, it makes sense for them to use this same pattern in Go. However, there are aspects of Go that don’t allow type hierarchies to provide the same level of functionality they do in other object oriented programming languages. Specifically, the concepts of base types and subtyping don’t exist in Go so type reuse requires a different way of thinking.
In this post I am going to show why type hierarchies are not always the best pattern to use in Go. I’ll explain why it is better to group concrete types together not by a common state but by a common behavior. I’ll show how to leverage interfaces to group and decouple concrete types, and lastly, I will provide some guidelines around declaring types.
Part I
Let’s start with a program I see way too often from those trying to learn Go. This program uses a traditional type hierarchy pattern that would be commonly seen in an object oriented program.
https://play.golang.org/p/ZNWmyoj55W
Listing 1:
01 package main
02
03 import "fmt"
04
05 // Animal contains all the base fields for animals.
06 type Animal struct {
07 Name string
08 IsMammal bool
09 }
10
11 // Speak provides generic behavior for all animals and
12 // how they speak.
13 func (a Animal) Speak() {
14 fmt.Println("UGH!",
15 "My name is", a.Name,
16 ", it is", a.IsMammal,
17 "I am a mammal")
18 }
In listing 1 we see the beginning of our traditional object oriented program. On line 06 we have the declaration of the concrete type Animal
and it has two fields, Name
and IsMammal
. Then on line 13 we have a method named Speak
that allows an Animal
to talk. Since an Animal
is a base type for all animals, the implementation of the Speak
method is generic and can’t represent any given animal very well beyond this base state.
Listing 2:
20 // Dog contains everything an Animal is but specific
21 // attributes that only a Dog has.
22 type Dog struct {
23 Animal
24 PackFactor int
25 }
26
27 // Speak knows how to speak like a dog.
28 func (d Dog) Speak() {
29 fmt.Println("Woof!",
30 "My name is", d.Name,
31 ", it is", d.IsMammal,
32 "I am a mammal with a pack factor of", d.PackFactor)
33 }
Listing 2 declares a new concrete type named Dog
which embeds a value of type Animal
and has a unique field named PackFactor
. We see the use of composition to reuse the fields and methods of the Animal
type. In this case, composition is providing some of the same benefits of inheritance, with respect to type reuse. The Dog
type also implements its own version of the Speak
method, which allows the Dog
to bark like a dog. This method is overriding the implementation of the Animal
type’s Speak
method.
Listing 3:
35 // Cat contains everything an Animal is but specific
36 // attributes that only a Cat has.
37 type Cat struct {
38 Animal
39 ClimbFactor int
40 }
41
42 // Speak knows how to speak like a cat.
43 func (c Cat) Speak() {
44 fmt.Println("Meow!",
45 "My name is", c.Name,
46 ", it is", c.IsMammal,
47 "I am a mammal with a climb factor of", c.ClimbFactor)
48 }
Next we have a third concrete type named Cat
in listing 3 that also embeds a value of type Animal
and has a field named ClimbFactor
. We see the use of composition again for the same reasons, and Cat
has a method named Speak
that allows the Cat
to meow like a cat. Again, this method is overriding the implementation of the Animal
type’s Speak
method.
Listing 4:
50 func main() {
51
52 // Create a Dog by initializing its Animal parts
53 // and then its specific Dog attributes.
54 d := Dog{
55 Animal: Animal{
56 Name: "Fido",
57 IsMammal: true,
58 },
59 PackFactor: 5,
60 }
61
62 // Create a Cat by initializing its Animal parts
63 // and then its specific Cat attributes.
64 c := Cat{
65 Animal: Animal{
66 Name: "Milo",
67 IsMammal: true,
68 },
69 ClimbFactor: 4,
70 }
71
72 // Have the Dog and Cat speak.
73 d.Speak()
74 c.Speak()
75 }
In listing 4 we have the main function where we put everything together. On line 54, we create a value of type Dog
using a struct literal and initialize the embedded Animal
value and the PackFactor
field. On line 64, we create a value of type Cat
using a struct literal and initialize the embedded Animal
value and the ClimbFactor
field. Then, finally, we call the Speak
method against the Dog
and Cat
values on lines 73 and 74.
This works in Go, and you can see how the use of embedding types provides familiar type hierarchy functionality. However there are some flaws with doing this in Go, and one is that Go does not support the idea of subtyping. This means you can’t use the Animal
type as a base type like you can in other object oriented languages.
What is important to understand is that, in Go, the Dog
and Cat
types can’t be used as a value of type Animal
. What we have is an embedded value of type Animal
inside a value of type Dog
and Cat
. You can’t pass a Dog
or Cat
to any function that accepts values of type Animal
. This also means that there is no way to group a set of Cat
and Dog
values together in the same list via the Animal
type.
Listing 5:
// Attempt to use Animal as a base type.
animals := []Animal{
Dog{},
Cat{},
}
: cannot use Dog literal (type Dog) as type Animal in array or slice literal
: cannot use Cat literal (type Cat) as type Animal in array or slice literal
Listing 5 shows what happens in Go when you try to use the Animal
type as a base type in a traditional object oriented way. The compiler is very clear that the Dog
and Cat
types can’t be used as type Animal
.
The Animal
type and the use of type hierarchies in this case is not providing us any real value. I would argue it is leading us down a path of code that is not readable, simple or adaptable.
Part II
When coding in Go try to avoid these type hierarchies that promote the idea of common state and think about common behavior. We can group a set of Dog
and Cat
values if we think about the common behavior they exhibit. In this case there is a common behavior of Speak
.
Let’s look at another implementation of this code that focuses on behavior.
https://play.golang.org/p/6aLyTOTIj_
Listing 6:
01 package main
02
03 import "fmt"
04
05 // Speaker provide a common behavior for all concrete types
06 // to follow if they want to be a part of this group. This
07 // is a contract for these concrete types to follow.
08 type Speaker interface {
09 Speak()
10 }
The new program starts in listing 6 and we have added a new type called Speaker
on line 08. This is not a concrete type like the struct types we declared before. This is an interface type that declares a contract of behavior that will let us group and work with a set of different concrete types that implement the Speak
method.
Listing 7:
12 // Dog contains everything a Dog needs.
13 type Dog struct {
14 Name string
15 IsMammal bool
16 PackFactor int
17 }
18
19 // Speak knows how to speak like a dog.
20 // This makes a Dog now part of a group of concrete
21 // types that know how to speak.
22 func (d Dog) Speak() {
23 fmt.Println("Woof!",
24 "My name is", d.Name,
25 ", it is", d.IsMammal,
26 "I am a mammal with a pack factor of", d.PackFactor)
27 }
28
29 // Cat contains everything a Cat needs.
30 type Cat struct {
31 Name string
32 IsMammal bool
33 ClimbFactor int
34 }
35
36 // Speak knows how to speak like a cat.
37 // This makes a Cat now part of a group of concrete
38 // types that know how to speak.
39 func (c Cat) Speak() {
40 fmt.Println("Meow!",
41 "My name is", c.Name,
42 ", it is", c.IsMammal,
43 "I am a mammal with a climb factor of", c.ClimbFactor)
44 }
In listing 7 we have the declaration of the concrete types Dog
and Cat
again. This code removes the Animal
type and copies those common fields directly into Dog
and Cat
.
Why did we do that?
- The
Animal
type was providing an abstraction layer of reusable state. - The program never needed to create or solely use a value of type
Animal
. - The implementation of the
Speak
method for the Animal
type was a generalization. - The
Speak
method for the Animal
type was never going to be called.
Here are some guidelines around declaring types:
- Declare types that represent something new or unique.
- Validate that a value of any type is created or used on its own.
- Embed types to reuse existing behaviors you need to satisfy.
- Question types that are an alias or abstraction for an existing type.
- Question types whose sole purpose is to share common state.
Let’s look at the main function now.
Listing 8:
46 func main() {
47
48 // Create a list of Animals that know how to speak.
49 speakers := []Speaker{
50
51 // Create a Dog by initializing its Animal parts
52 // and then its specific Dog attributes.
53 Dog{
54 Name: "Fido",
55 IsMammal: true,
56 PackFactor: 5,
57 },
58
59 // Create a Cat by initializing its Animal parts
60 // and then its specific Cat attributes.
61 Cat{
62 Name: "Milo",
63 IsMammal: true,
64 ClimbFactor: 4,
65 },
66 }
67
68 // Have the Animals speak.
69 for _, spkr := range speakers {
70 spkr.Speak()
71 }
72 }
On line 49 in listing 8, we create a slice of Speaker
interface values to group both Dog
and Cat
values together under their common behavior. We create a value of type Dog
on line 53 and a value of type Cat
on line 61. Finally on lines 69 - 71 we iterate over the slice of Speaker
interface values and have the Dog
and Cat
speak.
Some final thoughts about the changes we made:
- We didn’t need a base type or type hierarchies to group concrete type values together.
- The Interface allowed us to create a slice of different concrete type values and work with these values through their common behavior.
- We removed any type pollution by not declaring a type that was never solely used by the program.
Conclusion
There is a lot more to composition in Go but this is an initial understanding around the problems with using type hierarchies. There are always exceptions to every rule, but try to follow these guidelines until you know enough to understand the tradeoffs for making an exception.
To learn more about composition and other topics this post touches, check out these other blog posts:
Exported/Unexported Identifiers In Go
Methods Interfaces And Embedded Types
Object Oriented Mechanics In Go
Composition With Go
Thanks
Here are some friends from the community I would like to thank for taking the time to review the post and provide feedback.
Daniel Vaughan, Ted Young, Antonio Troina, Adam Straughan, Kaveh Shahbazian, Daniel Whitenack, Todd Rafferty