
Decorators in Go using embedded structs - fabianlindfors
https://fabianlindfors.se/blog/decorators-in-go-using-embedded-structs/
======
AYBABTME
Embedding like this will only lead to pain long term. It's preferable to
implement the interface you want to fulfil directly (by having actual method
definitions), despite the extra ceremony. Inheritance with embedding is a
false friend in Go. Stick to composition.

If you want to delegate a call to a "subtype", simply invoke the method on the
subtype by writing it out in long form. It doesn't make you feel as
intelligent but it's way more robust and won't come back to bite you down the
line.

~~~
weberc2
> Inheritance with embedding is a false friend in Go. Stick to composition.

Note that this applies to every language with inheritance. Composition is
_always_ better.

~~~
AYBABTME
I don't disagree. I just think it's particularly bad in Go.

~~~
pm90
Do you say this specifically because of struct embedding?

------
avita1
I find the comparison to java a bit unfair, because this pattern is totally
possible but has been largely deemed a bad pattern. The analog to the pattern
in java is to extend from a non-final class. So in this case "class
HTTPClient" and "class CachedHTTPClient".

There are lots of drawbacks to doing it that way, the worst of which is that
you need to remember to override the method in CachedHTTPClient everytime you
add a method to HTTPClient, and the compiler gives you no hints about it.

~~~
jerf
It's important to deeply understand why patterns are deemed bad in one
language, so you can see if it applies to another language [1]. In the case of
Go, we generally try to keep interfaces small. In many real-world cases of
decoration in Go, probably the vast, vast majority, the interface only has one
method, so there isn't any way to forget to override the other methods.

While there's no particular exact language feature I can point at to say why
this happens, in general, Go interfaces are more fluid since they don't have
to be declared up front, and end up being kept simpler than Java classes and
interfaces, so the concerns about failing to override other methods are
greatly, greatly reduced. They are not technically eliminated, but they're
pushed way, way down my list of priorities.

[1]: This is not special pleading for Go, it goes well beyond that. A good
design in Java is a bad design in Python, a good design in Python is a bad
design in Java, etc. If you had two languages where the exact same patterns
were appropriate in the exact same way, I'd question whether you actually had
two languages.

~~~
jayd16
This doesn't protect you in the slightest. The failure case is clear and
likely for something like an CachedHttpClient where every call should be
cached. You're talking hypothetically but here's a simple common failure case.

~~~
jerf
How do you fail to implement the second method override on a single-method
interface?

Bear in mind that when using the interface, the interface is all there is. It
essentially erases the other methods from consideration. That's why an
interface value in Go is a distinct type; it isn't just "a thing that can
happen to hold all these various concrete values", it is a distinct thing with
its own method set. So discussion underlying structs and their method sets is
a category error. (This is a bit subtle, but important to understand what is
actually going on in Go, or any other language with a similar setup.)

I'm not talking hypothetically. I'm talking about what happens in _real_ Go
code. Discussing what could happen if people wrote interfaces in a way other
than they actually do is what is hypothetical. This is the sort of thing that
matters when deciding whether or not a particular pattern is useful in a
language. It's rarely entirely down to pure syntax concerns or some sort of
Platonic software engineering consideration. In fact, even within the same
language you can encounter situations where a pattern makes sense in one
_framework_ but is a bad idea in another _framework_ ; Javascript is full of
such things. (Whether that's for good or bad reasons is a separate
consideration; the fact is that it is full of them.)

~~~
jayd16
>How do you fail to implement the second method override on a single-method
interface?

Well in the example given, there is an interface Client with two methods. If a
maintainer controls the Client interface and the HTTPClient implementation,
the case can occur where that maintainer updates Client and HttpeClient.
Suddenly, CachedHTTPClient in the downstream project has an unchached method
and as far as my limited Go knowledge goes, no compiler error.

>I'm talking about what happens in real Go code

The case appears in the blog post. Would you say the blog is not idiomatic Go?

~~~
jerf
"Would you say the blog is not idiomatic Go?"

Yes, it's a contrived example to make the point in the blog. Blog samples have
to be taken that way. The vast majority of the time real code decorates in Go,
it's with either A: an interface of 1 method or B: something sufficiently
local in concern that this sort of thing isn't a concern, beyond it just being
a bug (a compiler forcing you to specify an override won't save you from just
sticking the minimal stub in). Part of why this can be a problem in Java is
you tend to get a certain sprawl to your class hierarchy that doesn't occur in
Go. Or most other languages, used well. Java's got some unique weaknesses in
this area that do not generally translate.

~~~
jayd16
I personally think it has more to do with the coders than the code. Seeing as
it's trivial to do it wrong in Go (see blog) I have to assume IBM websphere Go
would be a similar nightmarish hellscape as it is in Java.

------
inlined
This is a bad practice in Go for any interface type. Interfaces which are
wrapped will fail all type assertions against their original type and fail to
assert into additional interfaces that the original also implemented.

For example, imagine you have an io.Reader. It may actually implement
io.ReaderTo which is used in some cases to implement alloc-free copying. If
you wrap it into a new struct, e.g. ioutil.NopCloser you delegate the main
interface but not the bonus interfaces. You can no longer cast to the type of
the original reader interface either (concrete type comparison is more
important with errors, which had a custom fix in Go just for this)

~~~
fabianlindfors
If we have an io.Reader which implements io.ReaderTo then it has a certain set
of exported functions. If this is embedded in ioutil.NopCloser then all those
exported functions will be available on ioutil.NopCloser. Wouldn't structural
typing then ensure that ioutil.NopCloser implements io.ReaderTo as well as
io.Reader?

Sorry if I'm misunderstanding!

~~~
omegabravo
[https://play.golang.org/p/V3eWfbDGhXF](https://play.golang.org/p/V3eWfbDGhXF)

In this example, by wrapping an interface in another interface, you lose the
ability to cast back down to an interface that the original type fulfilled.

~~~
fabianlindfors
Thanks for putting that together, I see your point know. Thought you were
talking about embedding when you said "wrap" before.

Considering that this is not related to embedding specifically, would you say
that decorators in general are bad practice in Go?

------
avalanche123
This pattern is called Composition
([https://en.wikipedia.org/wiki/Composition_over_inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance)).
What makes it much more practical in Go than in say Java is struct embedding
and implicit interface implementations

~~~
jayd16
Exporting structs doesn't seem like composition to me. It seems much more like
multiple inheritance.

~~~
weberc2
It is exactly composition; the compiler just generates the methods to
automatically delegate to the anonymous elements. It's just syntax sugar that
probably creates more confusion than it alleviates. It's not inheritance of
any kind because the outer struct cannot be passed into a function that takes
the inner element type (e.g., if you have `type B int` and `type A struct {B}`
and `func foo(B) {}`, you can't pass an instance of `A` into
`foo()`--`foo(A{B: 0})` is a compiler error.

As it is just composition, it is strictly better than inheritance, but still
probably unnecessarily confusing versus just writing out the delegation
methods yourself.

~~~
jayd16
>It's not inheritance of any kind because the outer struct cannot be passed
into a function that takes the inner element type

My mistake. I was mislead by the blog post.

From the article:

> The new type would be interchangeable with the existing client which would
> minimize the need for changes to existing code.

So with go exported structs it is not fair to say they can be used
interchangeably if at any point that instance is used as a parameter, field,
or variable that defines the type?

In the examples given in the article, the thing that is used as parameters is
the interface Client not the concrete type HTTPClient. Would that not allow
CachedHTTPClient to passed around as if it was a Client and would that not
show the same issues as inheritance?

~~~
weberc2
Both types implement the `Client` interface. So either type can be passed into
a function that accepts the `Client` interface, but a `CachedHTTPClient`
_cannot_ be passed into a function that accepts an `HTTPClient` (you would
have to pass the `CachedHTTPClient.HTTPClient`) and of course not vice versa
either. You can do the same thing in Java (forgive my syntax):

    
    
        public interface Client {
            ArrayList<String> getUsers();
            void createUser(String name);
        }
    
        public class HTTPClient implements Client {
            public ArrayList<String> getUsers() { /* ... */ }
            public void createUser(String name) { /* ... */ }
        }
    
        public class CachedHTTPClient implements Client {
            private HTTPClient httpClient;
    
            public ArrayList<String> getUsers() { /* ... */ }
    
            // In Go via "struct embedding", this method would be generated
            // automatically; in Java, we have to write it out. NBD.
            public void createUser(String name) { this.httpClient.createUser(name); }
        }

~~~
jayd16
Makes sense. I guess I just don't see how exported types save you from any of
the pitfalls of inheritance. In my mind composition is the use of existing
types _without_ being forced into a type contract.

~~~
weberc2
Mind you, I don’t advocate for struct embedding, but it doesn’t force you into
a type contract. The outer struct retains its type—it is not a subtype of the
nested type so it can’t be used in places that take the inner type.

Literally all it does is automatically create methods on the outer struct that
delegate to the anonymous member.

Unlike inheritance, there is no fragile base class problem and methods on the
inner anonymous member can’t dispatch to methods on the outer struct. Also,
the “parent” member is just another field in your struct. You can modify it or
replace it at runtime, unlike the parent in OOP languages.

------
pram
A small critique on your examples: you never actually show using the Client
interface anywhere. Might be useful to someone new to see how you'd switch out
HTTPClient for CachedHTTPClient using it.

~~~
fabianlindfors
Good point, thank you!

------
ljm
There’s probably more but the singular best use-case I’ve found for embedding
a struct is when you want a mutex.

That is elegant.

This example looks nice, but I think the naming of the structure and
interfaces makes it hard to follow and a cache doesn’t necessarily benefit
from it. In fact, it’s worse if your cache isn’t in-memory, and you’re not
sharing an instance or a connection pool. If you are, this is just indirection
and you can live with `Cache.get` just as well.

~~~
sethammons
I'd recommend only embedding a mutex on private structs - you likely don't
want to expose Lock and Unlock.

------
yepthatsreality
I think this is also a good primer into golang inheritance and composition,
which are major concepts in successful golang programming.

~~~
inlined
golang doesn’t believe inheritance is necessary. Be careful trying to put a
square peg in a round hole.

~~~
weberc2
golang is right :) Seriously though, Go doesn't have inheritance and I don't
miss it. The only reason I need it in Java, etc is because so many libraries
expect/require you to subclass.

