Recently, when refactoring code, a large number of interfaces have been abstracted. Also use these abstract interfaces to do a lot of pseudo-inheritance operations, greatly reduce the code redundancy, but also increase the readability of the code.
Then I searched the articles about Go inheritance casually, and found that some articles had too much code, and the code format was extremely rough, and the naming was extremely arbitrary, similar to A and B, which made people forget who inherited who after looking at them. I had to Go back to look at the logic again.
Even though it’s just sample code, I think it still needs to be concise, clear, and straightforward. That’s why I’m writing this blog post. Here’s a quick look at how inheritance is implemented in Go.
1. Simple combinations
Speaking of inheritance, we all know that there is no extends keyword in Go, which means that there is no native level of inheritance support for Go. That’s why I used the term pseudo-inheritance at the beginning of my article. Essentially, Go uses interfaces to implement something called composition, and Go is inheritance using composition, or more precisely, inheritance using composition instead of composition, to give you a very simple example.
1.1 Implement parent classes
Let’s use an animal that’s easy to understand — the cat — for example, so without further ado, let’s just look at the code.
type Animal struct {
Name string
}
func (a *Animal) Eat(a) {
fmt.Printf("%v is eating", a.Name)
fmt.Println()
}
type Cat struct {
*Animal
}
cat := &Cat{
Animal: &Animal{
Name: "cat",
},
}
cat.Eat() // cat is eating
Copy the code
1.2 Code Analysis
First, we implemented an Animal structure, representing the Animal class. The Name field is declared to describe the Name of the animal.
Then, an Eat method with Animal as receiver is implemented to describe the Animal’s eating behavior.
Finally, a Cat structure is declared that combines the Cat fields. Instantiate a cat, call Eat, and you’ll see normal output.
As you can see, the Cat structure itself has no Name field and does not implement the Eat method. The only thing we have is a combination of the Animal parent class, so now we’ve proved that we’ve inherited through composition.
2. An elegant combination
Those familiar with Go might say something like this when they see the code above
This is too rough I never said this sentence
Indeed, the above is just for those of you who haven’t heard of the Go group yet. As a simple example to understand Go’s combinatorial inheritance, this is perfectly fine. But when it comes to real development, that’s not enough.
For example, if I’m the user of this abstract class, if I get the Animal class, I don’t know at a glance what it does and what methods it can call. Also, there is no universal way to initialize, which means there will be duplicate code wherever initialization is involved. If there are initiality-related changes made later, only one after the other. So next, let’s make some optimizations to the code above.
2.1 Abstract Interface
Interfaces are used to describe the behavior of a class. For example, the animal interface we’re going to abstract is going to describe the behavior of being an animal. Common sense tells us that animals can Eat, bark, move, and so on. Here’s an interesting analogy.
The interface is like a sign, like a Starbucks. Starbucks is a sign.
What do you think of when you see this sign? American? Frappuccino? Matcha latte? Or the lattes, or even the decor.
That’s what a good interface does, and that’s why we need abstract interfaces.
// Interface to simulate animal behavior
type IAnimal interface {
Eat() Describe the act of eating
}
// Animal The parent of all animals
type Animal struct {
Name string
}
// Animal implements the eating interface described in IAnimal
func (a *Animal) Eat(a) {
fmt.Printf("%v is eating\n", a.Name)
}
// The animal constructor
func newAnimal(name string) *Animal {
return &Animal{
Name: name,
}
}
// The cat structure combines animal
type Cat struct {
*Animal
}
// Implement cat constructor to initialize animal structure
func newCat(name string) *Cat {
return &Cat{
Animal: newAnimal(name),
}
}
cat := newCat("cat")
cat.Eat() // cat is eating
Copy the code
There’s really no definition of a constructor in Go. For example, in Java, we can use constructors to initialize variables. For example, Integer num = new Integer(1). In Go, the user needs to simulate the implementation of the constructor by initializing the structure himself.
And then here we implement a subclass Cat that uses composition instead of inheritance to call methods in Animal. After we run it, we can see that the Cat structure does not have a Name field and does not implement the Eat method, but it still works fine. This proves that we have implemented inheritance through composition.
2.2 Rewriting method
// the cat structure IAnimal Eat method
func (cat *Cat) Eat(a) {
fmt.Printf("children %v is eating\n", cat.Name)
}
cat.Eat()
// children cat is eating
Copy the code
As you can see, the Cat structure has reimplemented the Eat method in Animal, so it’s overwritten.
2.3 Parameter polymorphism
What does that mean? For example, how do we solve function parameter polymorphism in Java? One solution that might come to mind for those familiar with Java is wildcards. In a nutshell, using wildcards allows the function to accept all parent types of a class or all subtypes of a class. But I personally don’t think readability is particularly friendly for people unfamiliar with Java.
In Go, it’s very convenient.
func check(animal IAnimal) {
animal.Eat()
}
Copy the code
In this function you can handle all the unit types that combine Animal, which in Java is an upper bound wildcard, that is, a wildcard that can handle any particular type and the classes derived from that particular type, or, in other words, any Animal.
3. Summary
Every coin has two sides, and optimization is no exception. Lots of abstract interfaces can really simplify code and make it look elegant and comfortable. But again, there is an understanding cost to reviewing code for others who are unfamiliar with it. Imagine looking at a piece of code, full of interfaces, clicking through several layers before you see the implementation. More, down to find suddenly found in another interface broken, have to manually go to another registered place to find.
These are some of the problems I think we face when optimizing:
- elegant
- Can be read
- performance
Sometimes it’s hard to do all three, such as writing code that looks bad but performs better than elegant code. For example, it looks elegant, but is not readable, etc.
Again, a quote I used to write on my blog
What suits you is the best
In this case, choose the solution that best suits you based on the specific situation of your project. There is no one-size-fits-all solution.
Share a recent play guitar see toxic chicken soup, learning is the same.
There are no shortcuts on the way to practice. It’s all detours
Previous articles:
- Go using Seed to get repeated random number problems
- The difference between game servers and Web servers
- Go source code parsing -Println story
- Go Module is used as package manager to build go Web server
- WebAssembly is complete – learn about wASM in its past and present
- Jack Bauer opens a restaurant – from monomer applications to microservices
Related:
- Wechat official account: full stack notes of SH (or directly search wechat LunhaoHu in the interface of adding official account)