Go is not an object-oriented language, but uses composition, nesting, and interfaces to support code reuse and polymorphism. About structure nesting: By anonymously nesting a named structure type, the outer structure type gets all the exported members of the anonymous member type, as well as all the exported methods of that type. Take this example:

type ShapeInterface interface {    GetName() string}type Shape struct {    name string}func (s *Shape) GetName() string {    return s.name}type Rectangle struct {    Shape    w, h float64}Copy the code

Shape defines the GetName() method, and Rectangle anonymously nested Shape to obtain the member method GetName(). Rectangle and Shape are both implementations of the ShapeInterface interface.

I initially thought that this was no different from object-oriented inheritance, treating the inner structure as a parent class, and by nesting the structure you could get the methods of the parent class and override the methods of the parent class as needed, which is what I used in actual project programming. Until one day…

Since we have a lot of demand for promotion, we have two or three times a month and large promotion activities every quarter. Product managers also rack their brains to think of ways to increase user activity and order volume. Each time is the front play is not the same, but the last is to participate in the task points, after sharing the lottery. So I was tasked with designing a generic process. Design interfaces and basic implementation types according to the common parts of each requirement, and reserve methods for subclass implementation, dealing with different preconditions each time, this is not often done in object oriented things.

So let’s use that ShapeInterface example just to understand it.

Type ShapeInterface interface {Area() float64 GetName() String PrintArea()} Float64 {return float (s *Shape) GetName() string {float (s *Shape) float64 {return float (s *Shape) GetName() string {float (s *Shape) float64 {float (s *Shape) GetName() string { Return s.name}func (s *Shape) PrintArea() {FMT.Printf("%s: Area %v\r\n", s.name, s.arrea ())} Rectangle struct {Shape w, h float64}func (r *Rectangle) Area() float64 {return Rectangle * r.h} Type Circle struct {Shape r float64}func (c *Circle) Area() float64 {return c * c * math.Pi}func (c *Circle) PrintArea() { fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area())}Copy the code

We added the Area() and PrintArea() methods to the ShapeInterface. Since each Shape has a different formula for calculating the Area, the Area in the base implementation Shape simply returns 0.0. The Rectange, a Rectange, was given the task of calculating its Area by overriding Area(), which prints its own Area using the PrintArea() method of a Shape.

So far, these have been my ideas, and I feel very excited after planning them, feeling that I have mastered the essence of the idea of Composition… According to this idea, I finished writing the whole process. The unit test only measured each sub-function. The preconditions were too complicated, and I was in charge of other projects in the team, and I didn’t have enough time, so I handed it over to my team mates and asked them to help me test the whole process.

Let’s run the above example. To see the difference, we write a Circle and override Area() and PrintArea() with this type.

func main() { s := Shape{name: "Shape"} c := Circle{Shape: Shape{name: "Circle"}, r: 10} r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4} listshape := []c{&s, &c, &r} for _, si := range listshape { si.PrintArea() //!! Guess which Area() method gets called!! }}Copy the code

The following output is displayed:

Shape: Area 0 circle: 314.1592653589793 a Rectangle Area: Area 0Copy the code

The PrintArea() method of Rectangle does not call the Area() method of Rectangle implemented by combining Shape. Instead, Rectangle calls the Area() method of Shape. Circle calls its own Area() because it overwrites PrintArea().

The PrintArea() method in the similar example in the project is much more complicated than this and carries the responsibility of a standardized process. Surely you can’t implement the PrintArea() method every time you combine it. That’s not a design, and it doesn’t make sense, right?

A bit of Googling turned up some detailed explanations. The behavior we expect above is called a virtual method: expect PrintArea() to call the overwritten Area(). While there are no inheritance or virtual methods in Go, shape.printarea () is defined by calling shape.area (). Shape does not know if it is embedded in any structure, so it cannot “dispatch” method calls to virtual runtime methods.

The Go language specification: the selector describes the exact rules to follow when evaluating x.f expressions (where f may be methods) to select the last method to call. The key statement in it is

  • A selector f can represent a field or method f of type T, or a field or method f that can refer to nested anonymous fields of T. The number of anonymous fields traversed to f is called their depth in T.

  • For a value x of type T or * T (where T is not a pointer or interface type), x.f represents the shallowest field or method in T of f.

Back to our example:

For Rectangle types, si.printarea () will call shape.printarea () because there is no PrintArea() method defined for Rectangle types (no recipients are PrintArea() methods for *Rectangle), An implementation of the shape.printarea () method calls shape.area () instead of rectangle.area ()- as discussed earlier, Shape is unaware of the existence of Rectangle. So you should see the output:

Rectangle : Area 0Copy the code

So since inheritance is not supported in Go, how to solve a similar problem with composition? We can define a PrintArea by defining a method that takes parameters to the ShapeInterface interface.

func  PrintArea (s ShapeInterface){    fmt.Printf("Interface => %s : Area %v\r\n", s.GetName(), s.Area())}Copy the code

Since it is not as simple as the example, I decided to define a method like InitShape to initialize the process. Here I will make some adjustments to the ShapeInterface and Shape type to make it more understandable.

type ShapeInterface interface { Area() float64 GetName() string SetArea(float64)}type Shape struct { name string area float64}... func (s *Shape) SetArea(area float64) { s.area = area}func (s *Shape) PrintArea() { fmt.Printf("%s : Area %v\r\n", s.name, s.area)}... func InitShape(s ShapeInterface) error { area, err := s.Area() if err ! = nil { return err } s.SetArea(area) ... }Copy the code

For Rectangle and Circle shapes, SetArea() will store the Area calculated by Area() in the Area field for future use.

type Rectangle struct {    Shape    w, h float64}func (r *Rectangle) Area() float64 {    return r.w * r.h}r := &Rectangle {    Shape: Shape{name: "Rectangle"},    w: 5, 4}InitShape(r)r.PrintArea()Copy the code

This case is the first time I’ve studied the difference between inheritance and composition since I started writing code in Go, and how to reuse code and provide polymorphic support in Go in a composition way. I think many of you who are used to object-oriented languages have more or less encountered the same problem. After all, it takes deliberate practice to break a mindset. Since I can’t reveal the design of the company code, I will share my experience with you with this simple example. If you have similar problems or have other questions when designing interfaces and types in Go, please leave a comment below.

Recommended reading

Go Web programming – Hash user passwords using BCRPYt