Hacker News new | ask | show | jobs
by avalanche123 2437 days ago
This pattern is called Composition (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
2 comments

Yes! I find embedded structs to be a really elegant way of doing composition.
Exporting structs doesn't seem like composition to me. It seems much more like multiple inheritance.
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.

>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?

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); }
    }
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.
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.