1

Suppose I have a set of related types that are returned by a function, treated as a group and have certain shared features. In a classical OOP I would achieve this by having a base class and then implementing each of the related types by subclassing said base class. In Go it would appear the main way of achieving this is to define an interface and then have related classes implement it, like so.

type Ball interface {
    Kick()
}

type Football {
    num int
}

type Soccerball {
    num int
}

func (b *Football) Kick() {
    fmt.Println("Kicking football %d", b.num)
}

func (b *Soccerball) Kick() {
    fmt.Println("Kicking soccer ball %d", b.num)
}

func main() {
    var b Ball
    // The goal is to provide a single type (in this case 'Ball') which 
    // can accomodate both Soccerball and Football (but is not interface{}).

    b = &Sockerball{}
    b = &Football{}
}

The problem with this approach is

  1. That it does not allow for shared attributes, only functions
  2. That it requires at least one function to be defined on the interface

  1. Can be partially remedied by using an embedded struct type with the desired attributes which also implements a separate interface type, but this requires defining at least two types with different names just to accomodate existence of a shared 'union' type.

  2. Can be remedied by creating a 'dummy' function on the interface, but this seems like an abuse of the intended use and is effectively just a way to 'trick' the compiler into type checking said 'union type' which does not really require the use of an interface.

Am I missing something, or is what I have described the standard way to accomplish what I want? Should I even be trying to do this, or is this not idiomatic Go code? I imagine it is quite common to want to have a set of objects with shared attributes that can be treated and accessed as a broader base type, but maybe I am thinking about things in the wrong way.

One thing I have discovered that it is possible to embed an interface type in a struct like so

type Ball interface {
    dummyFunc()
}

type Football {
    num int
    Ball
}

type Soccerball {
    num int
    Ball
}

func main() {
    var b Ball

    b = &Sockerball{}
    b = &Football{}
}

My understanding is that this allows an arbitrary type which implements a given interface to be used as the embedded 'Ball' type making all of the methods it implements on Ball available to the embedding type. By simply not initializing the embedded type (Ball) we can treat the embedding type (Soccerball) as a member of the type which it embeds without having to actually implement the interface methods, thus achieving the desired 'union type', however this relies on the fact that the interface methods will never be called (which would result in a nil pointer exception) and thus requires an implicit recognition that the interface type only exists to group the structs. Aesthetically this is the most desirable result, however semantically it may be considered bad form. If anyone could comment on (or correct) my observations and give me an idea of how this is achieved in practice (and whether my last example is acceptable) I would greatly appreciate it.

  • 1
    please don't cross-post: stackoverflow.com/questions/58783001/… "Cross-posting is frowned upon as it leads to fragmented answers splattered all over the network..." – gnat Nov 9 '19 at 22:50
  • Sorry, it was recommended that I repost the question here. I have deleted the original one on stackoverflow. – curious Nov 9 '19 at 23:12
2

I think you are right, you seem to have explored the space and the limitations that the language imposes.

The issues you had with the first method is that 1, it didn't have shared attributes and that 2, it required at least one function to be able to store it in the same interface.

  1. Shared attributes are a way to store state. If this is your intention then I think having an interface of getter and setter will cover what you are trying to achieve.

  2. interface is the functionality that the object exposes. Therefore if it's a ball then maybe you can play() with it, and there is value in exposing this functionality as an interface. I think you need more context to create a good design - the value you get from storing these structs in the same container will lead you to the interface.

My approach with go is usually to avoid abstractions early in the design process, let them naturally come up, and refactor.

  • Thanks. I didn't think anyone would bother properly wading through my question after this long :P. I've since come to the same conclusion. – curious Jan 24 at 9:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for?Browse other questions tagged or ask your own question.