Type embedding in Go
If you really want to use inheritance in Go, then type embedding is as close as you can get. But there are some things to watch out for.
The theory behind embedding is pretty straightforward: by including a type as a nameless parameter within another type, the exported parameters and methods defined on the embedded type are accessible through the embedding type. The compiler decides on this by using a technique called “promotion”: the exported properties and methods of the embedded type are promoted to the embedding type.
The basics
To demonstrate how to use type embedding we’ll start with a basic struct:
type Ball struct {
Radius int
Material string
}
If you want to embed or “inherit” this into a new struct, you can do so as follows:
type Football struct {
Ball
}
So now you have embedded Ball into Football. Now, if you want instantiate a new football, you can do so as follows:
fb := Football{}
fmt.Printf("fb = %+v\n", fb)
Which outputs:
fb = {Ball:{Radius:0 Material:}}
Suppose you want to set the ball parameters of the football, you need to do it like this:
fb := Football{Ball{Radius: 5, Material: "leather"}}
fmt.Printf("fb = %+v\n", fb)
Now if you have this method defined on the embedded type, Ball:
func (b Ball) Bounce() {
fmt.Printf("Bouncing ball %+v\n", b)
}
You can access the method through the embedding type, Football:
fb.Bounce()
Which produces this output:
Bouncing ball {Radius:5 Material:leather}
If you want to, you can also call this method through the embedding type, which is available as a parameter:
fb.Ball.Bounce()
Embedding interfaces
If the embedded type implements a particular interface, then that too is accessible through the embedding type. Here is an interface and a function that accepts the interface as parameter:
type Bouncer interface {
Bounce()
}func BounceIt(b Bouncer) {
b.Bounce()
}
Now you can call the method using the embedding type:
BounceIt(fb)
Embedding pointers
So far we have used type embedding on value structs, but it’s also possible to do it by reference, using pointers. In our example, the embedding type would look like this:
type Football struct {
*Ball
}
Because it’s a pointer, be aware of nil pointer panics. In our case, instantiation an “empty” Football and then calling BounceIt(fb) would throw a panic because the Bounce() method is defined on a value type. That can be fixed by updating it to a pointer type:
func (b *Ball) Bounce() {
fmt.Printf("Bouncing ball %+v\n", b)
}
Embedding interfaces
Besides embedding structs, it is also possible to embed interfaces in structs. You can use this to explicitly state that the embedding struct needs to satisfy the embedded interface and at the same time hide it’s data. In our example, you can do it as follows:
type Football struct {
Bouncer
}
You can can instantiate a new football bounce the same way:
fb := Football{&Ball{Radius: 5, Material: "leather"}}
fb.Bounce()
Which outputs:
Bouncing ball &{Radius:5 Material:leather}
Note that Ball is promoted to a pointer not because of the interface, but because we’ve defined the Bounce method on Ball with a pointer receiver in the previous chapter.
Because Football no longer has a reference to Ball, its Radius and Material properties are not accessible.
Things to watch out for
Now that you know the basics of embedding, there are a couple of gotchas you should be aware of. Pay close attention to these, because if you come from an object-oriented background, you most likely will make these mistakes.
The embedded struct has no access to the embedding struct
Unlike traditional inheritance, it is not possible for the embedded struct (child) to access anything from the embedding struct (parent.) If you duplicate a property, it’s value is never accessible “downstream”. Consider this adjustment to the Football struct and the Bounce method:
type Football struct {
Ball
Radius int
}func (b Ball) Bounce() {
fmt.Printf("Radius = %d\n", b.Radius)
}
And this variable assignment:
fb := Football{Ball{Radius: 5, Material: "leather"}, 7}
Then these two method calls will both print “Radius = 5”:
fb.Bounce()
fb.Ball.Bounce()
It is not possible to typecast to the embedding struct
Remember, embedding is not the same as inheritance. Also, what are you really trying to do?
Conflicting method signatures will not compile if you use them
The compiler will not warn you if you have a conflict in your method signatures. Only when you use the method anywhere in your code the compiler will complain. Let me demonstrate this behavior with the following structs:
type Ball struct {
Radius int
Material string
}type Football struct {
Ball
}type Bouncer interface {
Bounce()
}// compliant to signature of Bouncer interface
func (b Ball) Bounce() {
fmt.Printf("Bouncing ball %+v\n", b)
}// NOT compliant to signature of Bouncer interface
func (fb Football) Bounce(distance int) {
fmt.Printf("Bouncing football %+v for %d meters\n", fb,
distance)
}
And here is how you can call them.
fb := Football{Ball{Radius: 5, Material: "leather"}}fb.Bounce(7) // works
fb.Ball.Bounce() // also worksfb.Bounce() // errors "not enough arguments in call to fb.Bounce"
BounceIt(fb) // errors "Football does not implement Bouncer (wrong type for Bounce method)"
This behavior also applies when you use the same name on the embedding type for a parameter, like this:
type Football struct {
Ball
Bounce int
}
Again, this compiles perfectly fine, but will complain when you try to call fb.Bounce (“Football.Bounce is a field, not a method”) or call BounceIt(fb) (“Football does not implement Bouncer (missing Bounce method)”)
Ambiguous methods will not compile if you use them
Type embedding involving ambiguous methods is not always obvious and is therefore much harder to detect. Take for example this modification:
type Football struct {
Ball
Bouncer
}
Because Ball is already compliant to the Bouncer interface, this should not be a problem, right? Well, not if you want to use it like this:
BounceIt(fb)
The compiler will treat you with the following message:
cannot use fb (type Football) as type Bouncer in argument to BounceIt: Football does not implement Bouncer (missing Bounce method)
Although this doesn’t seem to make much sense to you in the first place, you should look at this from the perspective of the compiler: because the Bounce() method is promoted in both the Ball struct and the Bouncer interface, it doesn’t know which one to use. It simply throws them both out. And because of that, you get the “missing method” error message.
Embedding in interfaces is not possible
Because of the behavioral nature of interfaces, type embedding can not occur on interfaces.
Embedding interfaces by reference is not possible
For the very same reason that you should not define an interface as a pointer parameter: it doesn’t make sense.
Conclusion
Type embedding doesn’t make Go object-oriented, but it can be a helpful tool in relational modeling.
That’s all I have to say about type embedding in Go for now. Here are some suggestions that inspired me to write this article that might quench your thirst for more knowledge on this topic: