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
- That it does not allow for shared attributes, only functions
- That it requires at least one function to be defined on the interface
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.
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.