-2

Let's say in a 3rd party library we have an interface and a struct implementing this interface. Let's also assume there is a function that takes ParentInterface as argument, which have different behavior for different types.

type ParentInterface interface {
    SomeMethod()
}

type ParentStruct struct {
    ...
}

func SomeFunction(p ParentInterface) {
    switch x := p.Type {
    case ParentStruct:
        return 1
    }
    return 0
}

In our code we want to use this interface, but with our augmented behavior, so we embed it in our own struct. The compiler actually allows us to call functions about ParentInterface on my struct directly:

type MyStruct struct {
    ParentInterface
}

parentStruct := ParentStruct{...}
myStruct := MyStruct{parentStruct}

parentStruct.SomeMethod()  // Compiler OK.
myStruct.SomeMethod()  // Compiler OK. Result is same. Great.

SomeFunction(parentStruct)  // Compiler OK. Result is 1.
SomeFunction(myStruct.ParentInterface)  // Compiler OK. Result is 1.
SomeFunction(myStruct)  // Compiler OK. Result is 0. (!)

Isn't the last case a problem? I've encountered this kind of bugs more than once. Because I'm happily use MyStruct as an alias of ParentInterface in my code (which is why I define it in the first place), it's so hard to always remember that we cannot call SomeFunction on MyStruct directly (the compiler says we can!).

So what's the best practice to avoid this kind of mistake? Or it's actually a flaw of the compiler, which is supposed to forbid the use of SomeFunction(myStruct) at all since the result is untrustable anyway?

icza
  • 389,944
  • 63
  • 907
  • 827
David M
  • 433
  • 1
  • 4
  • 10
  • 1
    I don't understand what you're trying to do. But the behavior you describe makes perfect sense, since you're checking the underlying type in SomeFunction. To avoid the behavior you see, probably just don't do that--rely on the interface instead. But again, I don't know your goal, so I can't really say how to achieve it. – Jonathan Hall Aug 13 '18 at 06:40

1 Answers1

5

There is no compiler mistake here and your experienced result is the expected one.

Your SomeFunction() function explicitly states it wants to do different things based on the dynamic type of the passed interface value, and that is exactly what happens.

We introduce interfaces in the first place so we don't have to care about the dynamic type that implements it. The interface gives us guarantees about existing methods, and those are the only things you should rely on, you should only call those methods and not do some type-switch or assertion kung-fu.

Of course this is the ideal world, but you should stick to it as much as possible.

Even if in some cases you can't fit everything into the interface, you can again type assert another interface and not a concrete type out of it if you need additional functionality.

A typical example of this is writing an http.Handler where you get the response writer as an interface: http.ResponseWriter. It's quite minimalistic, but the actual type passed can do a lot more. To access that "more", you may use additional type assertions to obtain that extra interface, such as http.Pusher or http.Flusher.

In Go, there is no inheritance and polymorphism. Go favors composition. When you embed a type into another type (struct), the method set of the embedded type will be part of the embedder type. This means any interfaces the embedded type implemented, the embedder will also implement those. And calling methods of those implemented interfaces will "forward" the call to the embedded type, that is, the receiver of those method calls will be the embedded value. This is unless you "override" those methods by providing your own implementation with the receiver type being the embedder type. But even in this case virtual routing will not happen. Meaning if the embedded type has methods A() and B(), and implementation of A() calls B(), if you provide your own B() on the embedder, calling A() (which is of the embedded type) will not call your B() but that of the embedded type.

This is not something to avoid (you can't avoid it), this is something to know about (something to live with). If you know how this works, you just have to take this into consideration and count with it.

Because I'm happily use MyStruct as an alias of ParentInterface in my code (which is why I define it in the first place)

You shouldn't use embedding to create aliases, that is a misuse of embedding. Embedding a type in your own will not be an alias. Implementations of existing methods that check concrete types will "fail" as you experienced (meaning they will not find a match to their expected concrete type).

Unless you want to "override" some methods or implement certain interfaces this way, you shouldn't use embedding. Just use the original type. Simplest, cleanest. If you need aliases, Go 1.9 introduced the type alias feature whose syntax is:

type NewType = ExistingType

After the above declaration NewType will be identical to ExistingType, they will be completely interchangeable (and thus have identical method sets). But know that this does not add any new "real" feature to the language, anything that is possible with type aliases is doable without them. It is mainly to support easier, gradual code refactoring.

icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks for the long explanation. Very educational. I still have a question. Apparently even if not for aliasing it's still possible for a struct to include an interface for whatever reason. Then in which case is `SomeFunction(myStruct)` making sense? This is obvious some extra labor for the compiler - it has to be smart enough to recognize that `myStruct` is actually similar (if not equal) to an instance of `ParentInterface ` so it let `SomeFunction()` go. What's the benefit of doing so (for the compiler) if this is just a wrong practice? – David M Aug 13 '18 at 09:47
  • 1
    @DavidM `SomeFunction()` expects a value that implements the `ParentInterface`. Any value that implements it is valid, accepted, and is checked at compile time. Type of `mystruct` implements that interface, this is the only thing that matters to make the function call. The concrete type of the passed value is irrelevant at the time of the call, the only thing that matters is that it implements the interface type of the param. – icza Aug 13 '18 at 09:51