A walkthrough of Embedding in go version 1.14
Embedding¹ is a clever way of extending/composing many types into a single type. It represents the O² part of SOLID³ principles — Open closed principle; open to extension, closed for manipulation. What this means is that we can easily extend a type so that it does the original thing, as well as something else without having to amend the original implementation.
Sample Code for this Post
All running sample code can be found here — https://github.com/ankur22/embedding
In the example below, I’ve declared a
logger and a
checker, and each implement a function
validate respectively. I’ve also declared
loggerAndChecker and within it I’ve declared
loggerAndChecker now implements both the
Note how I’ve not named the fields. If I had then
loggerAndChecker would not implement the functions from the other two types implicitly, and I would be required to explicitly call the functions, as in this example:
Also worth noting that
loggerAndChecker can contain its own fields along with the embedded structs.
If either or both the
checker have a field
s, then the
s declared in
loggerAndChecker would override the
s from the embedded structs. Both the
s values from the embedded structs are still accessible explicitly. If
loggerAndChecker did not declare
s but both
checker did, we wouldn’t be able to implicitly call
loggerAndChecker, but we can still retrieve each of the
s values from both embedded structs:
Like above, if the
checker implement the same function (e.g.
validate), and we try and call
loggerAndChecker it will not compile. However, if we call any other function that do not overlap it will compile and run:
I think it’s a good idea to override the overlapping method, otherwise it will be very annoying for someone who tries to use that function. In the example below I use the validate function from the
Interface embedding is similar conceptually to struct embedding — we’re embedding interfaces into interfaces instead of structs into structs.
In the following example we have a few interfaces.
validator each declare functions that should be implemented for a type to qualify to be identified as one or the other.
doerAndValidator doesn’t declare any functions explicitly but instead relies on the declarations of the embedded interfaces.
It’s worth noting that the
doesAndValidator can still declare its own functions too as well as embedding other interfaces:
What if the
doer and the
validator declare the same function? As of go version 1.14, that’s completely fine:
Wait, what about if we two interfaces declare functions with same name, but different signatures? That’s still not ok and it won’t compile since go doesn’t do method overloading⁴ like in other languages such as Java or Python:
Real World Example
In the example below I’ve created a
retryableHTTPClient to perform
GET requests, and if a transient error occurs the request is retried. I’ve built on top of the builtin HTTP client to perform this extra action.
retryableHTTPClient can be passed around anywhere that accepts
http.Client, and the benefits of retry will be reaped there too.
You will see that I’ve used struct embedding of
retryableHTTPClient. We can call all the HTTP client public functions, even if we haven’t implemented them for
retryableHTTPClient as they will fall back to using the HTTP client’s implementation. In this example only the
GET is retryable, so you would need to complete this for the other public methods that also need to be retryable.
Other Articles That Are Also Worth A Read