What is the idiomatic way in Go to create a complex hierarchy of structures?

I am writing a translator in Go and I am looking for an idiomatic way of storing AST. I read the source code for the Go compiler, and it looks like they used empty method interfaces to represent the AST. For example, we have the following hierarchy,

Object --Immovable ----Building ----Mountain --Movable ----Car ----Bike 

Thus, the above hierarchy is implemented by the method of "empty method".

 type Object interface { object() } type Immovable interface { Object immovable() } type Building struct { ... } type Mountain struct { ... } type Movable interface { Object movable() } type Car struct { ... } type Mountain struct { ... } func (*Building) object() {} func (*Mountain) object() {} func (*Car) object() {} func (*Bike) object() {} func (*Building) immovable() {} func (*Mountain) immovable() {} func (*Car) movable() {} func (*Bike) movable() {} 

The above code is a contrived example, and that is how the Go compiler implemented an AST with dozens of empty methods. But why? Notice how many empty methods are defined. This can be complicated by increasing the depth of the hierarchy.

The comments indicate that empty methods prohibit the assignment of incompatible types. In our example *Car cannot be assigned *Immovable .

It is so simple in other languages, such as C ++, that support inheritance. I cannot think of another way of representing AST.

The way to implement the Go Go AST compiler may be idiomatic, but is it just as straightforward?

+7
inheritance struct interface go embedding
source share
1 answer

Go is not a (completely) object-oriented language : it does not have classes, but does not have type inheritance ; but it supports a similar construct called attachment at both the struct level and the interface level, and it has methods .

Interfaces in Go are just fixed sets of methods. A type implicitly implements an interface if its set of methods is a superset of the interface (there is no declaration of intent).

Empty methods are great if you want to explicitly or explicitly state that your type implements an interface (because it is not explicitly specified). Official Go FAQ: How can I guarantee that my type satisfies the interface?

 type Fooer interface { Foo() ImplementsFooer() } 

If you want to distinguish between a type hierarchy (for example, you do not want the object to be both Movable and Immovable ), they must have different sets of methods (there must be at least one method in each of the sets of Movable and Immovable methods that is missing in others), because if the sets of methods contain the same methods, the implementation of one of them will automatically implement the other, so you can assign a Movable object to a variable of type Immovable .

Adding an empty method to an interface with the same name will give you this distinction, assuming that you will not add such methods to other types.

Reducing the number of empty methods

Personally, I have no problem with empty methods. However, there is a way to reduce them.

If you also create a struct implementation for each type in the hierarchy, and each implementation implements the struct implementation one level higher, the method set one level higher will automatically exit without any noise:

An object

Object and ObjectImpl implementation:

 type Object interface { object() } type ObjectImpl struct {} func (o *ObjectImpl) object() {} 

The property

Immovable and ImmovableImpl implementation:

 type Immovable interface { Object immovable() } type ImmovableImpl struct { ObjectImpl // Embed ObjectImpl } func (o *Immovable) immovable() {} 

Note ImmovableImpl only adds the immovable() method, object() "inherited".

Building

Building implementation:

 type Building struct { ImmovableImpl // Embed ImmovableImpl struct // Building-specific other fields may come here } 

Note Building does not add any new methods, but automatically it is an Immovable object.

The advantage of this technique increases significantly if the number of subtypes increases or if interface types have more than one token method (since all methods are inherited).

+5
source share

All Articles