New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: remove embedded struct #22013

Open
henryas opened this Issue Sep 25, 2017 · 74 comments

Comments

Projects
None yet
@henryas

henryas commented Sep 25, 2017

I would like to propose the removal of embedded struct in Go 2.x, because it does not solve inheritance problems, and the alternatives may actually be better.

First, let us examine the pros and cons of inheritance (more specifically, implementation inheritance). Inheritance groups common functionalities into the base class. It allows us to make a change in one place (instead of being scattered everywhere), and it will be automatically propagated to the derived classes. However, it also means the derived classes will be highly coupled to the base classes. The base class implementor, who does not need to know anything about the derived classes, may inadvertently break the derived classes when making changes to the base class. In addition, inheritance encourages a type hierarchy. In order to understand a derived class, one need to know its base classes (which includes its immediate parents, its grandparents, its great grandparents, etc.). It gives us the classic OOP problem where in order to get a banana, you need the monkey and the whole jungle. Modern OOP proponents now encourage "composition over inheritance".

Go's embedded struct is unique. At a glance, it appears to be a composition. However, it behaves exactly like the classic inheritance, including the pros and cons of inheritance. The base struct implementor may inadvertently break the derived structs. In order to understand a derived struct, one must look up the definition of its base struct, creating a hierarchy of types. Thus, Go's embedded struct is actually inheritance (although it is a crippled one).

I say it is a crippled inheritance because Go's embedded struct does not provide the same flexibility as the actual inheritance. One such example is this:

driver.Drive(s60.Volvo.Car.Vehicle)

Even though s60 is a vehicle, in Go, you need to get to the actual base class in order to use it as an argument.

If the Go's Team considers keeping this inheritance property, I say they should either go all the way with inheritance, or scrap it altogether. My proposal is to scrap it.

Here are my reasons for scrapping it:

  1. It offers no benefits. Although it looks like a composition, it is not. It behaves like inheritance with all the flaws of inheritance. It offers less flexibility than the real inheritance for no apparent benefits.
  2. It does not take much effort to write forwarding methods. In fact, it may be better to do an actual composition without the embedded struct feature. The slight extra effort pays off in the long run.
  3. If you truly do not want to write forwarding methods, you may refactor common methods into functions that accept a common interface. You can achieve the same effect as inheritance without having to couple the types. For instance, instead of
type BaseDriver struct{}
func (b BaseDriver) Drive(vehicle Vehicle){}

type TructDriver struct{
   BaseDriver
} 

type TaxiDriver struct{
   BaseDriver
}

You can do this

func Drive(driver Driver, vehicle Vehicle) {}

Let me know what you think.

Thanks.

Henry

@griesemer

This comment has been minimized.

Contributor

griesemer commented Sep 25, 2017

(I assume you mean removing embedded types in general, rather than just embedded structs, which are simply a common type to embed.)

Without casting any judgement on this proposal, I note that embedding is also a significant cause of complexity in the language and implementation.

@twitchyliquid64

This comment has been minimized.

twitchyliquid64 commented Sep 25, 2017

I agree with your presentation of the limitations but disagree with your conclusion to remove it from the language. Embeddings are useful (and the limitations negligible) when you have lots of little types 'embedding' off another little one. It is far less cognitive load to think of subwoofer as speaker plus wubdubdub. I agree this is the wrong approach when you have really long recievers and complex composition though, then interfaces are a much better option.

Looking at your alternative, which you wrote as:

func Drive(driver Driver, vehicle Vehicle) {}

Imagine the realistic case where Driver or Vehicle have obscure interface methods, and there are a lot of implementations of Driver and/or Vehicle. Even having forwarder methods as you propose is a lot of duplication to deal with for function spec changes to the forwarded method (less of a problem nowadays with smart search-and-replace with IDEs).

Lastly, how often have we seen embedding used horrifically? I've seldom seen it (feel free to prove me wrong with links to stuff I'll need to follow up with eye-bleach), and I say if it aint broke dont fix it (especially considering it represents an additional break from Go1 compatibility).

@cznic

This comment has been minimized.

Contributor

cznic commented Sep 25, 2017

I would like to propose the removal of embedded struct in Go 2.x, because it does not solve inheritance problems, and the alternatives may actually be better.

Yes, it does not solve inheritance problems, because embedding is not intended to solve inheritance problems. Because it does not solve what it was not intended to solve, the above rationale about why to remove it makes no sense to me.

@as

This comment has been minimized.

Contributor

as commented Sep 25, 2017

I agree with neither the technical accuracy of the premise nor the final conclusion. Inheritance locks in functional dependencies in the base class. Embedding allows those dependencies to be overridden or propagated at a per-method granularity.

@valyala

This comment has been minimized.

Contributor

valyala commented Sep 25, 2017

I frequently use interface embedding when only a few interface methods must be overridden:

// StatsConn is a `net.Conn` that counts the number of bytes read
// from the underlying connection.
type StatsConn struct {
    net.Conn

    BytesRead uint64
}

func (sc *StatsConn) Read(p []byte) (int, error) {
    n, err := sc.Conn.Read(p)
    sc.BytesRead += uint64(n)
    return n, err
}

or

// AuthListener is a `net.Listener` that rejects unauthorized connections
type AuthListener struct {
    net.Listener
}

func (al *AuthListener) Accept() (net.Conn, error) {
    for {
        c, err := al.Listener.Accept()
        if err != nil {
            return c, err
        }
        if authorizeConn(c) {
            return c, nil
        }
        c.Close()
    }
}

There is no need in implementing other net.Conn and net.Listener methods thanks to method forwarding in these cases.

@ianlancetaylor ianlancetaylor changed the title from Go 2.0: Proposal to Remove Embedded Struct to proposal: Go 2: remove embedded struct Sep 25, 2017

@gopherbot gopherbot added this to the Proposal milestone Sep 25, 2017

@henryas

This comment has been minimized.

henryas commented Sep 25, 2017

@griesemer Yes. Embedded types is the more accurate term. I used embedded struct in the example because it is more evil compared to embedded interface. I would think that it shouldn't be that difficult to type out the full interface signature than relying on interface embedding.

@twitchyliquid64 Sorry if I didn't give a clear example earlier. What I meant to say was instead of doing the following:

type BaseDriver struct{}
func (b BaseDriver) Drive(vehicle Vehicle){}

type TruckDriver struct{
    BaseDriver
}

type CabDriver struct{
    BaseDriver
}

You have two alternatives. First, you can use the real composition and employ method forwarding, which would be as follows:

type TruckDriver struct{
    parent BaseDriver
}
func (t TruckDriver) Drive(vehicle Vehicle){
    t.parent.Drive(vehicle) //method forwarding
   //...or you may implement custom functionalities.
}

It is true that it takes a little duplication (which requires little to no additional effort), but the given the benefit of having the TruckDriver decoupled from the BaseDriver and easier maintenance in the future, I say it's worth it.

Alternatively, you may refactor the common method into function. So the above code snippet will now look like this:

type Driver interface {} //which may be implemented by TruckDriver, CabDriver, etc.

func Drive(driver Driver, vehicle Vehicle){} //Drive is usable by any kind of driver.

The benefit of this approach is that each driver exists independently, and the Drive functionality is decoupled from any specific driver. You can change one or the other without worrying about unforeseen domino effect.

I prefer having my types flat rather than deep. The problem with type hierarchy is that it is difficult to tell what other things you may break when you make a slight alteration to your code.

Given that embedded types is a major feature in Go, I am actually surprised to see that it isn't used as much as I anticipate it to be. It may be due to its certain characteristics, or simply people having no particular need for it. At least, gophers don't go crazy with constructing the different families of apes (or may be not yet). Regardless, I don't agree with the "if it ain't broke, then don't fix it". I am more of a "broken windows theory" guy. Go 2.x is the opportunity to take lessons learned from Go 1.x and makes the necessary improvements. I would think that the resulting simplicity from the removal of unnecessary features may bring about other future opportunities (eg. other potentially good ideas that may be difficult to implement due to the complexity of the current implementation).

@cznic It behaves exactly like inheritance. Changes to the base type is automatically inherited by the derived types. If it does not solve the problems that inheritance is meant to solve, then I don't know what does.

@as I have no idea what you are talking about. Here is an illustration. The last I checked my Parents were normal human beings. Since I am meant to function as a normal human being, I inherit from them so that I too can be a human. However, one day, my Parents abruptly decide to grow scales and a pair of wings, and breath fire. Since I inherit from them, I am too automatically turned into a dragon (without my permission!). Since I am expected to function as a human, it breaks my intended functionality. I could insist in being human by overriding the inherited behavior from them. However, no one can tell what other crazy things they may do in the future that will again break my functionalities. That is doable in both inheritance and in Go's embedded types.

@cznic

This comment has been minimized.

Contributor

cznic commented Sep 25, 2017

It behaves exactly like inheritance. Changes to the base type is automatically inherited by the derived types.

It does not behaves like inheritance at all. Given

type T int

func (T) foo()

type U struct {
        T
}

var u U

u.foo() behaves exactly like u.T.foo() when there's no foo defined within U - that's composition, not inheritance. It's only syntactic sugar with zero semantic change. When (T).foo executes it doesn't have the slightest idea its receiver is emebded in some "parent" type. Nothing from T gets inherited by U. The syntactic sugar only saves typing the selector(s), nothing else is happenning.

@henryas

This comment has been minimized.

henryas commented Sep 25, 2017

@cznic I agree with you that T is embedded in U and that is composition. However, the real issue is with the automatic propagation of T characteristics (even if those characteristics are still technically belong to T). Now, in order to learn what I can do with U (as a consumer of U), I have to learn what T does as well. If T changes, I as the consumer of U, have to be aware of those changes as well. This is the inheritance-like behavior I was talking about. It is simple if it involves only T and U. Imagine if T embeds X, and X embeds Y, and Y embeds Z. All of a sudden, you get the monkey and the banana problem.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Sep 25, 2017

In order for any proposal along these lines to be adopted, you'll need to do some analysis of where and how current Go programs use embedding, and how difficult it would be for them to adapt. You could start with just the standard library.

@as

This comment has been minimized.

Contributor

as commented Sep 25, 2017

@henryas

Since I am expected to function as a human, it breaks my intended functionality.

No, it enhances your functionality. If your intended behavior is to be Human, there is nothing the embedded type can do to change that or take it away. Sure, for some reason if the original type gained a method

BreatheFire(at complex128)

You could say you're say you're a dragon, but you're not. You're a fire breathing human who appreciates complex numbers. Your concerns seem to be about the general concept of functional dependency than about the inheritance problem. These exists in packages, interfaces, function parameter lists, and the list goes on.

@bcmills

This comment has been minimized.

Member

bcmills commented Sep 26, 2017

The base struct implementor may inadvertently break the derived structs.

That claim is not obvious to me. Could you give some concrete examples?

@henryas

This comment has been minimized.

henryas commented Sep 26, 2017

@ianlancetaylor I'll see what I can do.

@as It's actually pretty cool to be a fire-breathing human once in a while. But how much I will stay as a human depends on my earlier expectation of my Parents. If I thought that my Parents were already humans and therefore I didn't implement much humanity in me (because it was already implemented by my Parents), when my Parents turn into a giant lizard, I too will turn. Basically, I don't become what I expect myself to be, but I am actually the enhanced version of my Parents (whatever my Parents want to be). You may argue that this is technically the correct definition of inheritance (and it is), but it breaks the original assumption about me. The assumption about me depends on my assumption about my Parents, whereas my Parents don't necessarily know (and don't need to!) my assumption about them.

The main issue is that I don't encapsulate my Parents trait in me. When my client deals with me, they also have to deal with my Parents, and possibly my Grandparents as well. It creates unnecessary coupling and breaks encapsulation. Changes in one will have a domino effect on the others. With proper encapsulation, the effect of a change should be contained like ship compartmentation.

Again, it depends on whether this is the intended behavior that the Go's Team wants in Go language. If it is then I say they should not hesitate and support the proper inheritance all the way. After all, despite its criticism, inheritance is still a major feature in many popular programming languages today. If they do not want it, then they should scrap it and I vote for scrapping it.

@bcmills

This comment has been minimized.

Member

bcmills commented Sep 26, 2017

It does not take much effort to write forwarding methods.

That is only true at small scales: at larger scales, the advantage of embedding is that you do not need to update forwarding for each newly-added method.

If you embed a BaseDriver today and someone adds some new method to it (e.g. PayToll(amount Money) error), all of the embedders will now implement the expanded method set too. On the other hand, with explicit forwarding methods you must locate and update all of the embedders before you can (say) add that method to a corresponding interface type.

@henryas

This comment has been minimized.

henryas commented Sep 26, 2017

@bcmills The problem with automatic method implementation is that the people who write BaseDriver may not know the context in which BaseDriver are used by their derived drivers, and may inadvertently break the original assumption about the specific drivers. For instance, in your example, I may have certain drivers whom I may want them to pay toll using cash card that the company can control, rather than with cash. I may already have EnterTollRoad(card *CashCard) method in my driver. The changes you make to the BaseDriver just provided a way to circumvent the security measure I have in place for my specific driver.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Sep 26, 2017

@henryas I don't personally find that to be a particularly convincing argument. The fact that it is possible for a program to misuse some feature of the language does not imply that that language feature should be removed. You need to a longer argument; for example, you might argue that the misuse is a common error that people routinely make.

@henryas

This comment has been minimized.

henryas commented Sep 26, 2017

@ianlancetaylor if you read my earlier posts, I did point out that ... well, I will summarize it again here:

  1. The base type, that is supposed to not need to know anything about the derived types, has the ability to alter the derived types and break the original assumption about them.
  2. The consumer of the derived types needs to know not only the derived types themselves, but also the base type. It introduces unnecessary coupling. The consumer of the derived types is now coupled to both derived types and the base types (and the base types' base types if the base types are also derived from other types). It makes the code fragile because a change to any of the base types will have a cascading effect to other parts of the code.

By the way, those are common arguments against inheritance and none of them is my invention. So I really don't need to prove their validity and how they apply in practice. They have been around for years, expounded by experts and practitioners alike. They are relevant to Go's embedded type because despite it not being technically an inheritance, it exhibits an inheritance-like behavior that causes it to share the same pros and cons of an inheritance.

As mentioned before, despite arguments against inheritance, inheritance is still widely used in many programming languages today. So it's really up to the Go's Team to decide which direction they want to take Go language to. In my opinion, embedded type is not a needed feature and can be removed from the language.

However, should the Go team decide to keep this feature, I would say there is no need to complicate it by making it look like an inheritance in disguise. After all, you are still getting all the side effects of a proper inheritance anyway. Just take the common road to inheritance, and boldly claim this is inheritance. It is simpler that way and you most likely have all the kinks already ironed out by other languages who share the same road.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Sep 26, 2017

@henryas Thanks, but I'm not asking for a list of possible errors. I'm asking for an argument that these are errors that people commonly make.

To put it another way, every reasonably powerful language has features that can be misused. But those features also have a benefit. The question, as with all language questions, is how to weigh the benefits against the drawbacks. The fact that a feature can be misused is a minor drawback. The fact that a feature frequently is misused is a major drawback. You've demonstrated a minor drawback. I'm asking whether you can demonstrate a major one.

We don't call embedded types inheritance because it is not inheritance as most people use the term in languages like C++ and Java. For us to call it inheritance would be confusing. See also https://golang.org/doc/faq#Is_Go_an_object-oriented_language and https://golang.org/doc/faq#inheritance .

@henryas

This comment has been minimized.

henryas commented Sep 27, 2017

@ianlancetaylor What you are asking is difficult to quantify. How do you weigh one feature against another? How do you measure the benefits and the drawbacks, and whether they are major or minor? It isn't as simple as counting loc. The best that I can think of is by drawing opinions from everyone's experience with real-life projects whether they would rather have the feature or its absence.

In the Go projects that I have been involved with so far, there have been very little use of embedded types. If I recall correctly, we might have used embedded types for some small, simple types. We can live without the feature with no problem. It shouldn't take much effort to refactor the code. In fact, thinking back, it may be better to not use embedded types. However, considering the types involved are minor, small, and very unlikely to change, I would say, in our projects so far, it makes little difference between using the embedded types and not. Our team is also small. The longest distance between one person to another is just one table away. So the ease of communication may help in a way. In addition, the base and the derived types were usually written by the same person. So I would say it is quite difficult to judge the merits (or their absence) of embedded types from my own projects. However, I would think that embedded types is a superfluous feature.

About why I mention inheritance when the subject is about embedded types, I briefly mentioned this in the earlier post:

They are relevant to Go's embedded type because despite it not being technically an inheritance, it exhibits an inheritance-like behavior that causes it to share the same pros and cons of an inheritance.

I will elaborate further, and attempt to compare Go's embedded types to the common notions of inheritance and composition. I will use Parent and Child rather than Base and Derived.

Inheritance

What is inheritance? Inheritance is described as an "is-a" relationship between two types. The Parent defines the identity, and the Child is essentially the Parent with additional features. You can think of the Child as the enhanced version of the Parent. If the Parent is a fish, then the Child is a super fish. If the Parent is a bird, then the Child is a super bird.

How is this relationship implemented in practice? It is implemented by having the Parent's characteristics automatically exposed by the Child. Now let's compare this to Go's embedded types.

type Parent struct{}
func (p Parent) GrowFinsAndSwim(){} //Parent is a fish

//The child is automatically a fish
type Child struct{
    Parent
}
func (c Child) TalkTo(person Person){} 

//A super fish that can talk to human
child.GrowFinsAndSwim()
child.TalkTo(person)

Now, what if we changed the Parent to be a bird.

type Parent{}
func (p Parent) GrowWingsAndFly(){} //Parent is now a bird

//The child now becomes a bird
type Child struct{
    Parent
}
func (c Child) TalkTo(person Person){} 

//A super bird that can talk to human
child.GrowWingsAndFly()
child.TalkTo(person)

As seen from the above illustration, Go's embedded types behaves like inheritance. The Parent defines the identity, and the Child is the Parent with some enhancements.

Another aspect of inheritance is the Child's ability to override the Parent's behavior to provide more specialized behavior. We'll see how we can do that with Go's embedded type.

type Parent{}
func (p Parent) GrowWingsAndFly(){} //Parent is a bird

//The super bird
type Child struct{
    Parent
    gps GPS
}
func (c Child) GrowWingsAndFly(){} 

//Unlike the Parent, this super bird uses GPS to fly.
child.GrowWingsAndFly()

Again, we see inheritance-like behavior in Go's embedded type.

Now, how do you implement inheritance? Different languages may have different implementations, but I would think that the Child would need to hold some kind of reference to the Parent in order for it to work. If you describe it in Go, it would look something like this:

type Child struct{
    *Parent
}

Voila! It is embedded type! Is Go's embedded type actually an inheritance? You decide.

However, there is an important difference between Go's embedded type and the normal inheritance. The normal inheritance exposes only the Parent's behavior. On the other hand, Go's exposes both the Parent's behavior and the Parent itself. The question to ask is whether there is any benefit to doing so?

Composition

Now, we will compare Go's embedded type to composition. Composition is defined as a "has-a" relationship between two types. A Person has a Pencil, but the Person is definitely not a Pencil. The Child controls its own identity and the Parent has no control whatsoever over the Child. In Go, proper composition should look like this:

type Person struct{
    pencil Pencil
}
func (p Person) Write (book Book) {
    //do something with pencil and book
} 

The important thing to note here is that Pencil is well encapsulated by Person. The definition of Pencil does not automatically apply to Person. If you try to use embedded type to describe this "has a" relationship:

type Person struct{
    Pencil
}

type Pencil struct{}
func (p Pencil) Brand() string{}
func (p Pencil) Manufacturer() string{}

//Okay
person.Pencil.Brand()

//But ... Ouch!
person.Manufacturer()
person.Brand()

that very definition of "has a" (composition) will break, as embedded type resembles more of an "is a" (inheritance) relationship than a "has a" relationship.

Conclusion

Let's start by calling a spade a spade. Go's embedded type is inheritance. It describes an "is a" rather than a "has a" relationship. Technically, it is inheritance disguised as composition. The thing is why do it in a queer roundabout way if what you need is plain inheritance. Is there any benefit to doing so? I think Go 2.x is a great opportunity to fix it or ditch it.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Sep 27, 2017

Let's start by calling a spade a spade. Go's embedded type is inheritance

It is not.

The thing is why do it in a roundabout way if what you need is plain inheritance.

Go does not have inheritance, so embedding is not a round about way of providing inheritance as Go does not provide that feature. In Go value types satisfy the "is-a" relationship, and interface types satisfy the "has-a" relationship.

@cznic

This comment has been minimized.

Contributor

cznic commented Sep 27, 2017

@davecheney Is the last sentence saying what you want to wrote? It seems to me it's the other way around. Or I'm really confused before the first coffee of the day.

@henryas

This comment has been minimized.

henryas commented Sep 27, 2017

@davecheney I am puzzled.

In Go value types satisfy the "is-a" relationship

If Go value types satisfy the "is-a" relationship, how do you describe "A Carpenter is a Person that build furniture. A Singer is a Person that sings" in Go value types?

and interface types satisfy the "has-a" relationship.

type Pencil struct{}
type Person struct{
    pencil Pencil
}
func (p *Person) SetPencil(pencil Pencil){}
func (p *Person) Pencil() Pencil{}

The above code has no interface whatsoever, and yet the Person "has a" Pencil.

Where does that leave embedded types? What is the relationship between the embedded and the embeddee?

@davecheney

This comment has been minimized.

Contributor

davecheney commented Sep 27, 2017

@cznic yup, that's about as clear as I can make it

@henryas

A Carpenter is a Person that build furniture. A Singer is a Person that sings" in Go value types?

A type Carpenters struct{ ... } is-a Carpenter nothing more, nothing less. A Carpenter cannot be assigned to any other named type in Go.

A type Person struct{ ... } fulfils the type Singer interface{ ... } because it has-a func (p *Person) Sing() { ... } method.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Sep 27, 2017

@henryas

Where does that leave embedded types? What is the relationship between the embedded and the embeddee?

There is no relationship. type Pencil struct { ... } has no knowledge that it has been embedded inside another type, and that embedding has no effect on its behaviour.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Sep 27, 2017

@henryas

The above code has no interface whatsoever, and yet the Person "has a" Pencil.

This is not correct, type Person struct { ... } has a method called Pencil() and a field called pencil. As pencil is not embedded into Person, this is neither inheritance, nor embedding.

@as

This comment has been minimized.

Contributor

as commented Sep 27, 2017

In Go, proper composition should look like this:

type Person struct{
    pencil Pencil
}
func (p Person) Write (book Book) {
    //do something with pencil and book
} 

Go supports this already, at the risk of sounding sarcastic, these are just struct fields. The value of anonymous structs is that they propagate structure and function at the field level whereas inheritance propagates function at the type level. Inheritance is a crippled construct such that the structure can not be altered down the creek and the user has to deal with the contract defined in the base class.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Nov 11, 2017

@owais

This comment has been minimized.

owais commented Nov 11, 2017

Not your fault. It is a badly written sentence :)

@metakeule

This comment has been minimized.

metakeule commented Nov 20, 2017

@henryas

To avoid the "base class extension breaks the derived class" issue:
Maybe it would be enough to forbid embedding of types that are defined inside other packages?
Combine this with a godoc change to show methods of embedded types as special marked methods of the derived type for visibility.

@henryas

This comment has been minimized.

henryas commented Nov 20, 2017

Well, first of all, the original intention of this proposal is to re-examine Go's embedded type feature and whether it is superior to existing approaches and alternatives used by other languages, and ultimately to determine whether the feature is necessary. The specific behavior being questioned is the automatic method promotion.

What is the relationship between the embedded and the embeddee that it warrants the automatic method fallover? Wikipedia states there are two types of relationships, the "is-a" and the "has-a". The automatic method promotion seems to satisfy the "is-a" relationship, but some people claim that it isn't an "is-a". However, at the same time you don't need method promotion for the "has-a". While "Man.Dog.WiggleTail()" makes sense, with automatic method fallover, "Man.WiggleTail()" suddenly becomes very wrong. While "Mike.Speak()" is okay, "Mike.Human.Speak()" is somewhat weird. Perhaps Go's embedded type behavior is satisfying another type of relationship that we are not familiar with?

Is Go's embedded type feature superior to existing approaches (eg. inheritance)? There is actually nothing wrong with inheritance. It is a feature used by many modern programming languages today. Like all features, inheritance has its uses and can also be abused. It allows the centralization of common behavior, while at the same time it couples the derived to the base class. Does the embedded type feature adds values over the conventional inheritance approach? Why not use the conventional inheritance? It is a path well-trodden by many other languages. All its warts are known. It is easier to implement and more people are familiar with it and its pitfalls.

If inheritance is not an option, how about scrapping them off? Some modern languages such as Rust choose to not have inheritance at all. It means there is no automatic method promotion, and you actually have to rewrite the methods one by one. Despite the seemingly tedious endeavor, in practice, it is actually quite pleasant to write in Rust. Does having embedded type provide significant values over not having one?

Finally, there is this question whether Go 2.x needs to retain compatibility with Go 1.x? If you want to retain compatibility with Go 1.x, then there is really no point in continuing this discussion. There is no way you can get rid of the embedded type while retaining compatibility with Go 1.x. Again, this highlights the complexity of the embedded type feature. Once it logs into your code, it is difficult to refactor it out. You can't just write tools to do it. You have to do it manually one by one, judging the case for each one.

@bcmills

This comment has been minimized.

Member

bcmills commented Nov 20, 2017

Perhaps Go's embedded type behavior is satisfying another type of relationship that we are not familiar with?

How about incorporates-a?

You wouldn't embed a Dog in a Human struct, because the dog is not a part of the human.

On the other hand, you might embed a Horn in a Car struct: a car has-a horn, but because the horn is part of the car it is still sensible to say “there are a lot of cars honking on this street”.

Why not use the conventional inheritance? It is a path well-trodden by many other languages. All its warts are known.

You've answered your own question here. 🙂

Its warts are known, and we think (hope?) that Go's approach will lead to better code. (Not problem-free, but perhaps with less severe problems.)

Finally, there is this question whether Go 2.x needs to retain compatibility with Go 1.x?

That is an open question. (See https://blog.golang.org/toward-go2.)

@DeedleFake

This comment has been minimized.

DeedleFake commented Nov 21, 2017

Some modern languages such as Rust choose to not have inheritance at all. It means there is no automatic method promotion, and you actually have to rewrite the methods one by one.

But Rust does still offer an alternative. You can use traits to grant methods to types as long as those traits are in scope. For example:

trait Example {
  fn do_something(&self);
}

impl<T: SomeOtherTrait> Example for T {
  fn do_something(&self) {
  }
}

And voila. All types that implement SomeOtherTrait now also automatically implement Example as long as Example's definition is in scope. I've also put together a more full example in the Rust Playground.

I'm not advocating Rust's way of doing it, athough I do think it can be pretty neat sometimes. I'm just saying that Rust also has ways to give methods to multiple types at a time. You may also be able to do something similar using enums, but I'm not exactly sure.

@henryas

This comment has been minimized.

henryas commented Nov 22, 2017

Thanks for the Rust example. You actually don't need two traits. You can merge them into one. The feature at play is the default method. Rust allows you to define default methods for your interface. It allows "method promotion"-like behaviour. The difference between Rust's and Go's approaches is that, in Rust, "Derived.Action()" always mean the derived object doing the action, whereas in Go, "Derived.Action()" is actually "Derived.Base.Action()". It is the base object doing the action, but you can write it as "Derived.Action()" which, depending on the context, may be misleading. The question is whether this behaviour has any merit. Hence, this is the main issue that the proposal is trying to address.

@KamyarM

This comment has been minimized.

KamyarM commented Nov 23, 2017

Well, I have a better idea for you. Let's remove struct totally. Who needs a struct or OOP at all!! let's write codes like 40 years ago.

@dsnet

This comment has been minimized.

Member

dsnet commented Dec 5, 2017

I have no judgements regarding whether embedding should be added or removed, but I do find the current specification of them somewhat unsatisfactory. Here are few experience reports.

Embedding can lead to forward incompatible code

I have seen cases where a user embeds a protobuf struct into their own struct because they wanted to have easy access to the struct fields in the protobuf message. However, doing so inadvertently causes their type to also satisfy the proto.Message interface because all of the protobuf methods are being forwarded. This is wrong because their newly defined type is not a proto message.

As a general principle, using embedding of an external type in a forwards compatible way requires the user to think about all of the possible methods the embedded type may end up adding. My observation is that many programmers lack the discipline to see far ahead about the implications.

For example, suppose you embed a type that currently has methods M1, M2, ..., Mx that you want to forward. In the future, if the embedded type adds a String() string method, then your type now forwards the String method, when you most likely did not want it to be forwarded.

Embedding interacts poorly with pointers and unexported types

The fields of an embedded struct are forwarded even if the embedded type is unexported. This can have unexpected behavior when combined with pointers. For example:

type s struct { X int }
type S struct { *s }

x := S{new(s)}
x.X = 5 // This is actually accessible from external packages

However, this pattern is strange in that while S.X is accessible from external packages, it is not settable if the s struct is not first allocated. However, an external package is not able to allocate S.s since it an unexported field. Thus, the following panics:

x := S{}
x.X = 5 // panic since s is not allocated

This is the root of an inconsistency in the json package. See #21357.

Embedding concrete types to achieve extensible interfaces

This is not what embedding is designed for, but something it is being used for.

In Go, interfaces cannot be extended without breaking all concrete implementation (since they do not implement the newly added method). Thus, the following pattern can be used to achieve that:

// Users implementing Interface must embed the DefaultInterface struct type.
type Interface interface{
    Method1(...)
    Method2(...)
    // We may need to add methods here in the future.
    embedDefaultInterface() // unexported method prevents accidental satisfication
}

type DefaultInterface struct { ... }
func (d *DefaultInterface) Method1(...) {}
func (d *DefaultInterface) Method2(...) {}
func (d *DefaultInterface) embedDefaultInterface() {}

At a later point, Interface.Method3 can be added without fear of breaking all implementations, so long as we add a default implementation as DefaultInterface.Method3. However, this is still inferior to something like default methods in Java interfaces because the methods of the embedded type can only call DefaultInterface methods and not the version defined by custom concrete implementations.

While this does achieve some degree of interface extensibility, it is still unsatisfactory.

\cc @neild

@pciet

This comment has been minimized.

Contributor

pciet commented Dec 10, 2017

Struct embedding is one of my favorite Go features but if there was a better solution I’d be happy with that. The simplicity it provides is great:

type ReduceCase struct {
	Set
	Out Set
}
type Piece struct {
	Kind
	Orientation
	Base  Kind
	Moved bool `json:"-"`
}
type Point struct {
	*Piece // nil for no piece
	AbsPoint
}
@pciet

This comment has been minimized.

Contributor

pciet commented Feb 6, 2018

driver.Drive(s60.Volvo.Car.Vehicle) can be reduced to driver.Drive(s60): https://play.golang.org/p/tILZH-UUJGZ

@DeedleFake

This comment has been minimized.

DeedleFake commented Feb 6, 2018

@pciet This seems like one of those super-contrived examples (class Car {}; class Volvo extends Car {}) that only comes up as an example and, if you ever run into it in practice, is a pretty good indicator that the entire API needs heavy redesigning. I'm generally in favor of keeping struct embedding, but I don't think this is a good example to demonstrate why with.

For this particular example, with no other context, it would probably be a better idea to make Vehicle an interface, since it's there to provide a guarantee of functionality, not a specific layout of data. You could embed Vehicle in a Car interface, and then make Volvo an implementation of Car, and thus one of Vehicle at the same time. Then just make Drive() take a Vehicle argument.

@as

This comment has been minimized.

Contributor

as commented Feb 6, 2018

@DeedleFake

Most of those suggestions are already in the example... too bad there is no diff for comments <-> examples.

it would probably be a better idea to make Vehicle an interface

It already is an interface

You could embed Vehicle in a Car interface

Ah, so here's where the idea is.

Then just make Drive() take a Vehicle argument.

It does.. sort of. There are two Drive functions, one is a method. This is where I agree that the API gets weird.

@pciet

This comment has been minimized.

Contributor

pciet commented Feb 6, 2018

@DeedleFake I agree that this is a contrived example, but my point was that Go doesn’t require something like s60.Volvo.Car.Vehicle.

Changing Car to interface would make every instance of a car a type which doesn’t make sense to me.

@NDari

This comment has been minimized.

NDari commented Feb 11, 2018

Reading through this discussion, I wanted to see for myself what everyone is talking about. So I wrote this code.

type Foo struct { V int }

func (f Foo) Say() {
	f.V += 1
	f.MakeNoise()
}

func (f Foo) MakeNoise() { fmt.Println("I am a foo!") }

type Bar struct { Foo }

func (b Bar) MakeNoise() { fmt.Println("I am a bar!") }

type Baz struct { Foo }

func (b Baz) Say() {
	b.V += 1
	b.MakeNoise()
}

func main() {
	var b Bar
	b.MakeNoise()
	fmt.Println(b.V)
	b.Say()
	fmt.Println(b.V)
	
	b.V += 1
	fmt.Println(b.V)
	b.Say()
	fmt.Println(b.V)
	
	var t Baz
	fmt.Println(t.V)
	t.Say()
	fmt.Println(t.V)
	t.V += 1
	fmt.Println(t.V)
	t.Say()
	fmt.Println(t.V)
}

link to playground

I was not expecting the results of this. The fact the Bar.V is distinct from Bar.Foo.V was not clear to me: For example, calling Bar.Say() does not change the Bar.V. I was also surprised that the value of Bar.Foo.V was also unchanged after calling Bar.Say(). This does not make any sense to me.

Being confused, I changed all the function receivers to be pointers to the respective types, link to playground and now the value of *.V changes in the way that I would expect. I hope that I am not the only one confused by the output of at least one of the print statements in the above two programs.

@henryas

This comment has been minimized.

henryas commented Feb 21, 2018

Another thing that bothers me about embedded struct is the lack of encapsulation. Let's say we have the following data type:

type Mercedes struct {
   Vehicle
}

You can invoke the method with both mercedes.MoveForward() and mercedes.Vehicle.MoveForward(). Mercedes does not need to expose access to Vehicle. mercedes.MoveForward() is enough. If one day Mercedes decides to change its implementation to derive its MoveForward method from Sedan, it's going to break everybody that uses mercedes.Vehicle.MoveForward(). There is no reason why you need access to Vehicle.

@cznic

This comment has been minimized.

Contributor

cznic commented Feb 21, 2018

@bcmills

This comment has been minimized.

Member

bcmills commented Feb 21, 2018

@NDari I think the source of your confusion — pointer vs. value receivers — is mostly independent of embedding. (See https://golang.org/doc/faq#methods_on_values_or_pointers.)

It may be that embedding exacerbates the problem (for example, by encouraging people to think in misleadingly object-oriented terms), but I don't think removing embedding would significantly reduce confusion about value receivers in general.

@NDari

This comment has been minimized.

NDari commented Feb 21, 2018

@bcmills You are correct. I revisited the example and my confusion is orthogonal to the issue of embedded as you said.

@dsnet

This comment has been minimized.

Member

dsnet commented Feb 28, 2018

Here's another example of the poor interaction of embedding and the visibility rules: #24153.

@dsnet

This comment has been minimized.

Member

dsnet commented Mar 21, 2018

#24466 is an example of a user who seems to be using embedding to forward the fields of a struct, but does not intend to forward the methods.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Mar 21, 2018

There is some good discussion above about problems using embedded fields. What I don't see is any discussion of alternatives other than manually repeating all the methods and renaming all field references. Embedded fields are used today and they do serve a purpose. If we are going to eliminate them, we need to have something to replace them with.

Leaving open for now for further discussion. This proposal can't be accepted without some idea of what to replace embedded fields with. Simply eliminating them from the language, although it would simplify matters, can't work with the large existing base of Go code.

@henryas

This comment has been minimized.

henryas commented Mar 22, 2018

If the automatic method fallover is deemed necessary, I would like to propose the reduction of embedded struct inner object's visibility as an alternative solution:

Given the following:

type Car struct {
   FuelLevel int
}

func (c *Car) Drive(distance int, consumptionRate int){
   ///implementation..
}


type Mercedes struct {
    Car
}

mercedes.FuelLevel = 30 //OK!
mercedes.Drive(100, 1) //OK!

//from within the same package
mercedes.Car.FuelLevel = 30 //still okay
mercedes.Car.Drive(100, 1) //still okay

//from external package
mercedes.Car.FuelLevel = 30 //no longer legal
mercedes.Car.Drive(100, 1) //no longer legal

//you need Mercedes to do all the work if you are accessing from external package
mercedes.FuelLevel = 30 //OK!
mercedes.Drive(100, 1) //OK!

//Of course, method override still work as normal. Mercedes may override the Drive method.
mercedes.Drive(100, 1) //if overridden, it calls Mercedes' overridden version.
mercedes.Car.Drive(100, 1) //not legal when accessed from external package.

//if Mercedes needs to allow access to its Car object, it must explicitly allow it to happen.
func (m Mercedes) Car() Car {
   return m.Car
}

//then, from external package we can do this if necessary.
mercedes.Car().Drive(100, 1)

//the key here is that Mercedes has complete control over access to its inner object. 

The problem with the current implementation of embedded struct is that it exhibits both an "is-a" and an "has-a" relations. What the above solution does is to make a distinct separation of the "is-a" from the "has-a". Embedded struct is an "is-a". Mercedes is a car and you can perform car-related activities directly on the Mercedes. There is no need to access the Mercedes' Car field.

If you need a "has-a", you can do the following:

type Person struct {
    MyCar Car
}

person.MyCar.FuelLevel = 30 //OK
person.MyCar.Drive(100, 1) //OK

Now, this Person has a Car.

Another advantage of this approach is that it should be relatively easy to fix existing code using gofix.

@as

This comment has been minimized.

Contributor

as commented Mar 22, 2018

The problem with the current implementation of embedded struct is that it exhibits both an "is-a" and an "has-a" relations.

Can you explain why? Where's the logic in that conclusion that supports this assertion? "is-a" and "has-a" are just alphabet soup.

I see no advantages to the change--it just breaks compatibility with existing code. It's common to wrap a type in another type and override just one or two methods, leaving the others forwarded. With your change, it becomes impossible to call the inner object's original method: highly undesirable.

@henryas

This comment has been minimized.

henryas commented Mar 22, 2018

Can you explain why? Where's the logic in that conclusion that supports this assertion? "is-a" and "has-a" are just alphabet soup.

person.DoWork() and person.Car.DoWork() read differently. The first implies you are asking the person to do the work. The latter implies you are asking the person's car to do the work. They have different meanings to the reader, and the current embedded struct allows you to have both as if they are the same and let you change one with the other.

Note that the alphabets soups you mentioned are pretty much taught in programming courses and textbooks. They even have it in Wikipedia. While I appreciate the skepticism towards modern programming practices, it doesn't mean that you should do the opposite everything they do just for the sake of doing it. Not all of them are bad.

Personally, I still think that embedded struct is not needed. I don't use it in my work. However, I have never found myself in a situation where I need to write 30+ methods on a single object, and then write the exact 30+ methods for several other objects. So I wouldn't know the value of automatic method propagation to others.

Ian mentioned that writing forwarding methods take much work and that automatic method propagation has its values. Hence, if method propagation is all that is needed, we can still have it but with some usage clarification (syntax-wise) to avoid confusion to the reader, whether you mean person.DoWork() or person.Car.DoWork().

You brought up some good points about needing to access the inner object. I realize that internally you may still need to refer to the inner object, but externally you don't. Hence, I have updated my earlier post about the alternative solution.

@jozuenoon

This comment has been minimized.

jozuenoon commented Apr 5, 2018

No, no to this change.

  1. embedding standard library structs/interfaces like sync.Mutex,
  2. overriding functionalities in existing structures especially from standard library,
  3. testing / mocking
@MatejLach

This comment has been minimized.

MatejLach commented May 5, 2018

With my current Go project, which requires implementing an OO-heavy W3C standard, (ActivityPub), I rely heavily on struct embedding to simplify the implementation.
Removing embedding might simplify the compiler, but complicates the implementation of many other types of programs. I don't think we should simplify one program, (the compiler), at the expense of complicating many other programs.

I'll be in favour of an alternative of course, but not just an outright removal as is. Once there are interface constrained generics and interfaces are more akin to Rust traits, I can see embedding being unnecessary, but not in isolation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment