英文:
is it possible to call overridden method from parent struct in Golang?
问题
我想实现这样的代码,其中B继承自A,并且只覆盖A的Foo()方法,我希望代码打印出B.Foo(),但实际上打印出的是A.Foo(),看起来在Golang中接收器无法像C++那样工作,在C++中启用动态绑定时,代码可以按照我想要的方式工作。
我还发布了另一段代码,它可以工作,但实现起来太困难了,更像是一种hack的方式,我认为这不是Golang的风格。
所以我的问题是:如果父类的Bar()方法有一些逻辑,例如打开一个文件,然后读取一些行,并使用Foo()将这些行输出到stdout,而子类(在示例中是B)想要使用大部分逻辑,唯一的区别是子类希望Foo()将这些行输出到另一个文件。我应该如何实现?我听说Golang的继承不能像C++或Java那样工作,那么在Golang中应该采用什么正确的方式?
package main
import (
"fmt"
)
type A struct {
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
func (a *A) Bar() {
a.Foo()
}
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
func main() {
b := B{A: A{}}
b.Bar()
}
输出:A.Foo()
下面的代码可以工作,但是当写
a := A{}
a.Bar()
时,会遇到编译错误
package main
import (
"fmt"
)
type I interface {
Foo()
}
type A struct {
i I
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
func (a *A) Bar() {
a.i.Foo()
}
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
func main() {
b := B{A: A{}}
b.i = &b // 这里i像b的一个属性
b.Bar()
}
输出:B.Foo()
英文:
I want to implement such code, where B inherit from A and only override A's Foo() method, and I hope the code to print B.Foo(), but it still print A.Foo(), it seems that the receiver in Golang can't work like this in C++, in which when dynamic binding is enabled, the code can work like what I want.
I also post another piece of code, which works, but it's too hard to implement, and more like a hack way, I think it's not a Golang style.
So my problem is: if the parent's Bar() method has some logic, for example, open a file, then read some lines, and use Foo() to output these lines to stdout
, and the Child (in the example is B) wants to use most of them, the only difference is that the Child wants Foo() to output the lines to another file. How should I implement it? I have heard that Golang's inheritance can't work like C++ or Java, and what's right way in Golang?
package main
import (
"fmt"
)
type A struct {
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
func (a *A) Bar() {
a.Foo()
}
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
func main() {
b := B{A: A{}}
b.Bar()
}
output: A.Foo()
the following piece works, but when write
a := A{}
a.Bar()
you will encounter a compiler error
package main
import (
"fmt"
)
type I interface {
Foo()
}
type A struct {
i I
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
func (a *A) Bar() {
a.i.Foo()
}
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
func main() {
b := B{A: A{}}
b.i = &b // here i works like an attribute of b
b.Bar()
output: B.Foo()
答案1
得分: 34
正如你所写的,Go语言并没有真正的继承机制,允许类似继承的特性的方法被称为嵌入(Embedding)。
基本上,这意味着被嵌入的结构体并不知道自己被嵌入了,所以你不能覆盖从它调用的任何东西。你实际上可以获取嵌入结构体的引用,只能从嵌入结构体中获取引用。
所以你最好的方法或多或少类似于你的第二个例子 - 通过某种接口的依赖注入。例如,A拥有对某个实际工作的接口的引用,比如worker
,它可以将数据写入文件或其他操作。然后在实例化B时,你也可以用另一个工作器(即使不嵌入A也可以这样做)替换A的worker
。A只需要执行类似于myWorker.Work()
的操作,而不关心具体是哪个工作器。
英文:
As you wrote, what Go has is not really inheritance, the method that allows inheritance like features is called Embedding.
http://golang.org/doc/effective_go.html#embedding
What it means basically is that the embedded struct has no idea that it is embedded, so you cannot override anything that is called from it. You can actually take the embedded struct and take a reference for it only from the embedding struct.
So your best way to do it is more or less like your second example - through some sort of dependency injection using interfaces. i.e - A has a reference to some interface that does the actual work, say worker
, that writes to a file or whatever. Then when instantiating B, you also replace A's worker
with another worker (you can do it even without embedding A of course). The A just does something like myWorker.Work()
without caring what worker it is.
答案2
得分: 18
我已经为您翻译了代码部分,以下是翻译好的内容:
// 解决方案1:Go的惯用方式,将常见的“方法”实现为带有接口参数的外部函数。
package main
import "fmt"
// Fooer 必须实现 Foo 方法
type Fooer interface {
Foo()
}
// Bar 是一个代理,调用特定实例的 Foo 方法。
func Bar(a Fooer) {
a.Foo()
}
//////////////////////////////////////////////////////////////////////
// 使用示例
func main() {
b := &B{} // 注意这是一个指针
// 也不需要为默认初始化的字段指定值。
Bar(b) // 输出:B.Foo()
}
//////////////////////////////////////////////////////////////////////
// 实现部分
// A 是一个“基类”
type A struct {
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
// B 重写了 A 的方法
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
尝试在 Go Playground 上运行:https://play.golang.org/p/2TbmHUs9_Dt
// 解决方案2:类似于您的第二个选项,使用“接口黑魔法”。然而,由于 Bar() 不仅适用于 A(也适用于 B),让我们将其移到基类中,并隐藏实现细节和所有危险的内容。
package main
import "fmt"
//////////////////////////////////////////////////////////////////////
// 使用示例
func main() {
b := NewB()
b.Bar() // 输出:B.Foo()
a := NewA()
a.Bar() // 输出:A.Foo()
}
//////////////////////////////////////////////////////////////////////
// 实现部分
// aBase 是 A 和 B 的共同“祖先”。
type aBase struct {
ABCD // 嵌入接口。由于它只是一个指针,必须进行初始化!
}
// Bar 是 A 和 B 的共同方法。
func (a *aBase) Bar() {
a.Foo() // aBase 没有定义 Foo 方法,所以它调用嵌入接口的 Foo 方法。
}
// a 类,未导出
type a struct {
aBase
}
func (a *a) Foo() {
fmt.Println("A.Foo()")
}
// b 类,未导出
type b struct {
aBase
}
func (b *b) Foo() {
fmt.Println("B.Foo()")
}
//////////////////////////////////////////////////////////////////////
// 现在,公共函数和方法。
// ABCD 描述了 A 和 B 的所有导出方法。
type ABCD interface {
Foo()
Bar()
}
// NewA 返回新的 a 结构体
func NewA() ABCD {
a := &a{}
a.ABCD = a
return a
}
// NewB 返回新的 b 结构体
func NewB() ABCD {
b := &b{}
b.ABCD = b
return b
}
尝试在 Go Playground 上运行:https://play.golang.org/p/0Zcs_arturP
英文:
Been struggling with this myself. Found 2 solutions:
-
Idiomatic Go way: implement the common "method" as external function with interface as argument.
package main import "fmt" // Fooer has to Foo type Fooer interface { Foo() } // Bar is a proxy, that calls Foo of specific instance. func Bar(a Fooer) { a.Foo() } ////////////////////////////////////////////////////////////////////// // usage func main() { b := &B{} // note it is a pointer // also there's no need to specify values for default-initialized fields. Bar(b) // prints: B.Foo() } ////////////////////////////////////////////////////////////////////// // implementation // A is a "base class" type A struct { } func (a *A) Foo() { fmt.Println("A.Foo()") } // B overrides methods of A type B struct { A } func (b *B) Foo() { fmt.Println("B.Foo()") }
Try it on Go Playground: https://play.golang.org/p/2TbmHUs9_Dt
-
Similar to your second option: interface hackery. However, since Bar() is not specific to A (it is common to A and B), let's move it to base class, and hide implementation details and all dangerous stuff:
package main import "fmt" ////////////////////////////////////////////////////////////////////// // Usage func main() { b := NewB() b.Bar() // prints: B.Foo() a := NewA() a.Bar() // prints: A.Foo() } ////////////////////////////////////////////////////////////////////// // Implementation. // aBase is common "ancestor" for A and B. type aBase struct { ABCD // embed the interface. As it is just a pointer, it has to be initialized! } // Bar is common to A and B. func (a *aBase) Bar() { a.Foo() // aBase has no method Foo defined, so it calls Foo method of embedded interface. } // a class, not exported type a struct { aBase } func (a *a) Foo() { fmt.Println("A.Foo()") } // b class, not exported type b struct { aBase } func (b *b) Foo() { fmt.Println("B.Foo()") } ////////////////////////////////////////////////////////////////////// // Now, public functions and methods. // ABCD describes all exported methods of A and B. type ABCD interface { Foo() Bar() } // NewA returns new struct a func NewA() ABCD { a := &a{} a.ABCD = a return a } // NewB returns new struct b func NewB() ABCD { b := &b{} b.ABCD = b return b }
Try it on Go Playground: https://play.golang.org/p/0Zcs_arturP
答案3
得分: 9
最近我有一个需要做这个的需求,OP提出的组合方法非常好用。
我尝试创建另一个示例来演示父子关系,并使其更易于阅读。
https://play.golang.org/p/9EmWhpyjHf:
package main
import (
"fmt"
"log"
)
type FruitType interface {
Wash() FruitType
Eat() string
}
type Fruit struct {
name string
dirty bool
fruit FruitType
}
func (f *Fruit) Wash() FruitType {
f.dirty = false
if f.fruit != nil {
return f.fruit
}
return f
}
func (f *Fruit) Eat() string {
if f.dirty {
return fmt.Sprintf("The %s is dirty, wash it first!", f.name)
}
return fmt.Sprintf("%s is so delicious!", f.name)
}
type Orange struct {
*Fruit
}
func NewOrange() *Orange {
ft := &Orange{&Fruit{"Orange", true, nil}}
ft.fruit = ft
return ft
}
func NewApple() *Fruit {
ft := &Fruit{"apple", true, nil}
return ft
}
func (o *Orange) Eat() string {
return "The orange is so sour!"
}
func main() {
log.Println(NewApple().Eat())
log.Println(NewApple().Wash().Eat())
log.Println(NewOrange().Eat())
log.Println(NewOrange().Wash().Eat())
}
英文:
Recently I have a need to do this and the composition method proposed by OP works great.
I try to create another example to try to demonstrate the parent and child relationship and make it easier to read.
https://play.golang.org/p/9EmWhpyjHf:
package main
import (
"fmt"
"log"
)
type FruitType interface {
Wash() FruitType
Eat() string
}
type Fruit struct {
name string
dirty bool
fruit FruitType
}
func (f *Fruit) Wash() FruitType {
f.dirty = false
if f.fruit != nil {
return f.fruit
}
return f
}
func (f *Fruit) Eat() string {
if f.dirty {
return fmt.Sprintf("The %s is dirty, wash it first!", f.name)
}
return fmt.Sprintf("%s is so delicious!", f.name)
}
type Orange struct {
*Fruit
}
func NewOrange() *Orange {
ft := &Orange{&Fruit{"Orange", true, nil}}
ft.fruit = ft
return ft
}
func NewApple() *Fruit {
ft := &Fruit{"apple", true, nil}
return ft
}
func (o *Orange) Eat() string {
return "The orange is so sour!"
}
func main() {
log.Println(NewApple().Eat())
log.Println(NewApple().Wash().Eat())
log.Println(NewOrange().Eat())
log.Println(NewOrange().Wash().Eat())
}
答案4
得分: 6
Go语言不支持虚方法重写。因此,你想要使用的设计模式在Go中没有直接支持。这被认为是一种不好的实践,因为改变A.Bar()的实现会破坏所有派生类(如B)的假设,即A.Bar()将调用A.Foo()。你想要使用的设计模式会使你的代码变得脆弱。
在Go中,可以通过定义一个具有Foo()方法的Fooer接口来实现。将Fooer作为参数传递给Bar(),或者将其存储在A的字段中,并由A.Bar()调用。要更改Foo的操作,只需更改Fooer的值。这被称为组合,比通过继承和方法重写来更好地改变Foo的操作。
以下是在Go中实现你想要做的事情的惯用方式:https://play.golang.org/p/jJqXqmNUEHn。在这个实现中,Fooer是A的一个成员字段,由实例工厂函数NewA()
的参数初始化。当Fooer在A的生命周期内不经常改变时,这种设计模式是首选的。否则,你可以将Fooer作为Bar()
方法的参数传递。
这就是我们在Go中改变Foo()
行为的方式。它被称为组合,因为通过改变组成A的实例来改变Bar()
的行为。
package main
import (
"fmt"
)
type Fooer interface {
Foo()
}
type A struct {
f Fooer
}
func (a *A) Bar() {
a.f.Foo()
}
func NewA(f Fooer) *A {
return &A{f: f}
}
type B struct {
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
type C struct {
}
func (c *C) Foo() {
fmt.Println("C.Foo()")
}
func main() {
a := NewA(new(B))
a.Bar()
a.f = &C{}
a.Bar()
}
PS:这是一个可能的实现你想要实现的设计模式的示例,供文档参考:https://play.golang.org/p/HugjIbYbout
英文:
Go does not support virtual method overriding. The design pattern you want to use is thus not directly supported by Go. It is considered bad practice because changing the implementation of A.Bar() would break all derived classes like B that assume A.Foo() will be called by A.Bar(). The design pattern you want to use will make your code brittle.
The way to do it in Go would be to define a Fooer interface with a Foo() method. A Fooer would be passed as argument to Bar() or stored in a field of A and called by A.Bar(). To change the Foo action, change the Fooer value. This is called composition, and it is much better than changing Foo action by inheritance and method overriding.
Here is an idiomatic way to do what you want to do in Go: https://play.golang.org/p/jJqXqmNUEHn. In this implementation, the Fooer is a member field of A that is initialized by a parameter of to the instance factory function NewA()
. This design pattern is preferable when the Fooer doesn't change frequently during the lifetime of A. Otherwise, you may pass the Fooer as parameter of the Bar()
method.
This is how we change the behavior of Foo()
in Go. It is called composition because you change the behavior of Bar()
by changing the instances composing A.
package main
import (
"fmt"
)
type Fooer interface {
Foo()
}
type A struct {
f Fooer
}
func (a *A) Bar() {
a.f.Foo()
}
func NewA(f Fooer) *A {
return &A{f: f}
}
type B struct {
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
type C struct {
}
func (c *C) Foo() {
fmt.Println("C.Foo()")
}
func main() {
a := NewA(new(B))
a.Bar()
a.f = &C{}
a.Bar()
}
PS: Here is a possible implementation of the design pattern you wanted to implement for documentation purpose: https://play.golang.org/p/HugjIbYbout
答案5
得分: 4
package main
import (
"fmt"
)
//-- 工作中的多态
// 通过方法签名指定子类
// 在这里你应该定义可重写的方法
type AChildInterface interface {
Foo()
}
type A struct {
child AChildInterface
}
//-- /工作中的多态
// 硬编码的 A.Bar 方法
func (a *A) Bar() {
a.child.Foo() // Foo() 将被重写 = 在指定的子类中实现
}
//-- 可变方法的默认实现
type ADefaults struct{}
func (ad ADefaults) Foo() {
fmt.Println("A.Foo()")
}
//-- /默认实现
//-- 指定的子类
type B struct {
ADefaults // 从 ADefaults 实现默认的 A 方法,对于这个例子来说不是必需的
}
// 重写指定的方法
func (b B) Foo() {
fmt.Println("B.Foo()")
}
//-- /指定的子类
func main() {
a := A{ADefaults{}}
a.Bar()
// Golang 风格的继承 = 嵌入子类
b := A{B{}} // 注意:我们创建了一个具有指定子类的父类,以改变行为
b.Bar()
}
输出:
A.Foo()
B.Foo()
英文:
package main
import (
"fmt"
)
//-- polymorphism in work
// children specification by methods signatures
// you should define overridable methods here
type AChildInterface interface {
Foo()
}
type A struct {
child AChildInterface
}
//-- /polymorphism in work
// hard A.Bar method
func (a *A) Bar() {
a.child.Foo() // Foo() will be overwritten = implemented in a specified child
}
//-- default implementations of changeable methods
type ADefaults struct{}
func (ad ADefaults) Foo() {
fmt.Println("A.Foo()")
}
//-- /default
//-- specified child
type B struct {
ADefaults // implement default A methods from ADefaults, not necessary in this example
}
// overwrite specified method
func (b B) Foo() {
fmt.Println("B.Foo()")
}
//-- /specified child
func main() {
a := A{ADefaults{}}
a.Bar()
// Golang-style inheritance = embedding child
b := A{B{}} // note: we created __Parent__ with specified __Child__ to change behavior
b.Bar()
}
Output:
A.Foo()
B.Foo()
答案6
得分: 4
从C++/Python转到Go语言(现在一切都是与Web相关的,对吧?!),我也遇到了这个问题。我觉得Go语言中的面向对象编程(OOP)只是半成品。通过嵌入(struct的匿名字段),内部类型的方法可以自动继承,从而引出了继承的概念,但后来发现了一些限制。然而,通过在结构体中嵌入接口,并且遵循一定的规范,可以模拟C++中的构造函数、继承、多态和方法重写。
考虑以下示例代码:https://play.golang.org/p/nqt0haPYt2x
中文翻译如下:
package main
import (
"bytes"
"fmt"
"log"
"math"
"unsafe"
)
// 模拟C++中的多态,通过模板方法设计模式
// ========================== Shape 接口 ==============================
// 类似于C++中的抽象类
type Shape interface {
Area() float32 // 形状的面积
Perimeter() float32 // 形状的周长
Name() string // 形状的名称(如矩形、圆形、正方形等)
}
// ====================== PrintableShapeInfo =============================
type PrintableShapeInfo struct {
Shape // 类似于C++中的继承,尽管Go语言没有这样的概念
preetyPrintPrefix string
}
// 初始化一个新的PrintableShapeInfo对象。该方法是独立的,可以从其他上下文中调用
//
// 备注:模拟C++中的构造函数初始化部分
func (printableShapeInfo *PrintableShapeInfo) Init(preetyPrintPrefix string) {
printableShapeInfo.preetyPrintPrefix = preetyPrintPrefix
}
// 中心方法模拟了模板方法设计模式。通过动态调用(通过指针)正确的方法,打印形状的一些信息
//
// 备注:设计模式的最佳实践建议优先使用组合而不是继承(即模拟一个ShapeInfoPrinter类,该类接受一个Shape接口并打印其信息),
// 为了展示模板方法模式,选择了“Go的继承”模型
func (printableShapeInfo *PrintableShapeInfo) PrintInfo() {
log.Println("PrintableShapeInfo::PrintInfo")
fmt.Printf("%s PrintableShapeInfo::PrintInfo - %s:\n",
printableShapeInfo.preetyPrintPrefix, printableShapeInfo.Name()) // 动态调用(通过指针)形状的Name方法(如Rectangle.Name或Circle.Name或Square.Name)
fmt.Printf("\tArea: %f\n", printableShapeInfo.Area()) // 动态调用(通过指针)形状的Area方法(如Rectangle.Area或Circle.Area或Square.Area)
fmt.Printf("\tPerimeter: %f\n", printableShapeInfo.Perimeter()) // 动态调用(通过指针)形状的Perimeter方法(如Rectangle.Perimeter或Circle.Perimeter或Square.Perimeter)
}
// ====================== Rectangle =============================
type Rectangle struct {
PrintableShapeInfo // 类似于C++中的继承,尽管Go语言没有这样的概念
width float32 // 矩形的宽度
height float32 // 矩形的高度
}
// 创建并初始化一个新的矩形对象,并正确设置其Shape接口的方法集(类似于C++类的虚函数表)
//
// 备注:模拟C++中的构造函数
func NewRectangle(width float32, height float32) *Rectangle {
log.Println("NewRectangle")
rectangle := new(Rectangle) // 分配内存
rectangle.Shape = rectangle // 使用Rectangle的方法设置Shape的特定虚函数表。模板方法模式关键所在
rectangle.Init(width, height) // 初始化类
return rectangle
}
// 初始化一个新的矩形对象。该方法是独立的,可以从其他上下文中调用(例如正方形的Init方法。见下文)
//
// 备注:模拟C++中的构造函数初始化部分
func (rectangle *Rectangle) Init(width float32, height float32) {
log.Println("Rectangle::Init")
// 调用基类PrintableShapeInfo结构体的Init方法
rectangle.PrintableShapeInfo.Init("###")
rectangle.width = width
rectangle.height = height
}
// 计算矩形的面积
func (rectangle *Rectangle) Area() float32 {
log.Println("Rectangle::Area")
return float32(rectangle.width * rectangle.height)
}
// 计算矩形的周长
func (rectangle *Rectangle) Perimeter() float32 {
log.Println("Rectangle::Perimeter")
return float32(2 * (rectangle.width + rectangle.height))
}
// 获取矩形的对象名称
func (rectangle *Rectangle) Name() string {
log.Println("Rectangle::Name")
return "rectangle"
}
// ====================== Circle =============================
type Circle struct {
PrintableShapeInfo // 类似于C++中的继承,尽管Go语言没有这样的概念
radius float32 // 圆的半径
}
// 创建并初始化一个新的圆对象,并正确设置其Shape接口的方法集(类似于C++类的虚函数表)
//
// 备注:模拟C++中的构造函数
func NewCircle(radius float32) *Circle {
log.Println("NewCircle")
circle := new(Circle) // 分配内存
circle.Shape = circle // 使用Circle的方法设置Shape的特定虚函数表。模板方法模式关键所在
circle.Init(radius) // 初始化类
return circle
}
// 初始化一个新的圆对象。该方法是独立的,可以从其他上下文中调用(如果需要的话)
//
// 备注:模拟C++中的构造函数初始化部分
func (circle *Circle) Init(radius float32) {
log.Println("Circle::Init")
// 调用基类PrintableShapeInfo结构体的Init方法
circle.PrintableShapeInfo.Init("ooo")
circle.radius = radius
}
// 计算圆的面积
func (circle *Circle) Area() float32 {
log.Println("Circle::Area")
return math.Pi * float32(circle.radius*circle.radius)
}
// 计算圆的周长
func (circle *Circle) Perimeter() float32 {
log.Println("Circle::Perimeter")
return 2 * math.Pi * float32(circle.radius)
}
// 获取圆的对象名称
func (circle *Circle) Name() string {
log.Println("Circle::Name")
return "circle"
}
// ====================== Rectangle =============================
// 通过矩形来实现正方形
type Square struct {
Rectangle // 类似于C++中的继承,尽管Go语言没有这样的概念
}
// 创建并初始化一个新的正方形对象,并正确设置其Shape接口的方法集(类似于C++类的虚函数表)
//
// 备注:模拟C++中的构造函数初始化
func NewSquare(width float32) *Square {
log.Println("NewSquare")
square := new(Square) // 分配内存
square.Shape = square // 使用Rectangle的方法设置Shape的特定虚函数表。模板方法模式关键所在
square.Init(width) // 初始化类
return square
}
// 初始化一个新的正方形对象。该方法是独立的,可以从其他上下文中调用(如果需要的话)
//
// 备注:模拟C++中的构造函数初始化部分
func (square *Square) Init(width float32) {
log.Println("Square::Init")
// 由于Rectangle字段是匿名的,我们可以直接调用其未重写的方法,但仍然可以通过命名的Rectangle访问它,以及它的(甚至是重写的)方法
square.Rectangle.Init(width, width) // 调用Rectangle的Init方法来初始化其成员。由于Square是以Rectangle的术语实现的,因此不需要其他操作
}
// 计算正方形的面积
func (square *Square) Area() float32 {
log.Println("Square::Area")
// 由于Rectangle字段是匿名的,我们可以直接调用它的未重写的方法,但仍然可以通过命名的Rectangle访问它,以及它的(甚至是重写的)方法
return square.Rectangle.Area()
}
// 计算正方形的周长
func (square *Square) Perimeter() float32 {
log.Println("Square::Perimeter")
// 由于Rectangle字段是匿名的,我们可以直接调用它的未重写的方法,但仍然可以通过命名的Rectangle访问它,以及它的(甚至是重写的)方法
return square.Rectangle.Perimeter()
}
// 获取正方形的对象名称
func (square *Square) Name() string {
log.Println("Square::Name")
return "square"
}
func main() {
// 初始化日志子系统,以便在main函数结束时显示日志
logStringWriter := bytes.NewBufferString("")
log.SetOutput(logStringWriter)
rectangle := NewRectangle(2, 3) // 创建一个矩形对象
rectangle.PrintInfo() // 通过调用Rectangle的Area、Perimeter和Name方法,应该表现出多态行为
circle := NewCircle(2) // 创建一个圆对象
circle.PrintInfo() // 通过调用Circle的Area、Perimeter和Name方法,应该表现出多态行为
square := NewSquare(3) // 创建一个正方形对象
square.PrintInfo() // 通过调用Square的Area、Perimeter和Name方法,应该表现出多态行为
// 打印构造体的大小
fmt.Printf(`
Go构造体的大小:
作为Rectangle结构体看到的Shape接口大小:%d
`, unsafe.Sizeof(rectangle.Shape))
fmt.Printf("\tRectangle结构体大小:%d", unsafe.Sizeof(rectangle))
fmt.Printf(`
作为Circle结构体看到的Shape接口大小:%d
`, unsafe.Sizeof(circle.Shape))
fmt.Printf("\tCircle结构体大小:%d", unsafe.Sizeof(circle))
fmt.Printf(`
作为Square结构体看到的Shape接口大小:%d
`, unsafe.Sizeof(square.Shape))
fmt.Printf("\tCircle结构体大小:%d", unsafe.Sizeof(square))
// 打印日志
fmt.Println("\n\nDumping traces")
fmt.Print(logStringWriter)
return
}
中心方法(模板方法)是PrintInfo,对于任何定义的形状,调用该方法会按预期工作,通过调用正确的Area、Perimeter和Name方法。例如,circle.PrintInfo()将调用circle.Area、circle.Perimeter和circle.Name。
构造函数NewRectangle、NewCircle和NewSquare构造形状对象,分为三个步骤:
- 空间分配
- 方法集(类似于C++的虚函数表)初始化,用于实现多态行为
- 结构体成员初始化,通过Init方法进行
结构体成员初始化是为了更好地重用代码。例如,Rectangle的Init方法调用基类PrintableShapeInfo的Init方法,而Square的Init方法调用基类Rectangle的Init方法(该方法又调用了PrintableShapeInfo的Init方法,如前所述)。
此外,由于接口嵌入,对象的大小只会稍微增加,其中包含一对指向方法集和数据区域的指针,如示例输出所示。
我认为代码看起来相当不错,唯一的担忧是是否特别设置Shape接口的方法集(如NewRectangle、NewCircle和NewSquare函数)会触发一些副作用,因为代码似乎正常工作?!
英文:
Coming from C++/Python, where OOP is much better represented, and discovering go (now everything is web or web related, right?!) I too stumbled upon this issue. I feel that OOP in go is only half-baked. With embedding (struct's anonymous fields), methods of the inner type come along for free, inducing the idea of inheritance, only to learn later on the limitations. However, playing a bit with embedded interfaces within structs and with a bit of discipline, C++-like constructors, inheritance, polymorphism, and methods override can be emulated.
Considering the example - https://play.golang.org/p/nqt0haPYt2x
package main
import (
"bytes"
"fmt"
"log"
"math"
"unsafe"
)
//Emulate C++ like polymorphism in go, through template method design pattern
//========================== Shape interface ==============================
//like C++ abstract classes
type Shape interface {
Area() float32 //Shape's area
Perimeter() float32 //Shape's perimeter
Name() string //Shape's name (like rectangle, circle, square etc.)
}
//====================== PrintableShapeInfo =============================
type PrintableShapeInfo struct {
Shape //like C++ inheritance, although go has no such a thing
preetyPrintPrefix string
}
//Init a new PrintableShapeInfo object. The method is distinct so that it can be called from other contexts as well
//
//Remark: emulates the C++ constructor init part
func (printableShapeInfo *PrintableShapeInfo) Init(preetyPrintPrefix string) {
printableShapeInfo.preetyPrintPrefix = preetyPrintPrefix
}
//The central method emulates the template method design pattern. It prints some info about a shape by dynamically calling (through pointers) the right methods
//
//Remark: the design patterns best practices recommend to favor composition over inheritance (i.e. model a ShapeInfoPrinter class, which takes a Shape interface and prints its info),
//for the sake of showcasting the template method pattern, the "go's inheritange" like model was chosen
func (printableShapeInfo *PrintableShapeInfo) PrintInfo() {
log.Println("PrintableShapeInfo::PrintInfo")
fmt.Printf("%s PrintableShapeInfo::PrintInfo - %s:\n",
printableShapeInfo.preetyPrintPrefix, printableShapeInfo.Name()) //dynamically calls (through a pointer) a shape's Name method (like Rectangle.Name or Circle.Name or Square.Name)
fmt.Printf("\tArea: %f\n", printableShapeInfo.Area()) //dynamically calls (through a pointer) a shape's Area method (like Rectangle.Area or Circle.Area or Square.Area)
fmt.Printf("\tPerimeter: %f\n", printableShapeInfo.Perimeter()) //dynamically calls (through a pointer) a shape's Perimeter method (like Rectangle.Perimeter or Circle.Perimeter or Square.Perimeter)
}
//====================== Rectangle =============================
type Rectangle struct {
PrintableShapeInfo //like C++ inheritence, although go has no such a thing
width float32 //rectangle's width
height float32 //rectangle's heigh
}
//Creates and init a new rectangle object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor
func NewRectangle(width float32, height float32) *Rectangle {
log.Println("NewRectangle")
rectangle := new(Rectangle) //allocate data
rectangle.Shape = rectangle //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
rectangle.Init(width, height) //init class
return rectangle
}
//Init a new rectangle object. The method is distinct so that it can be called from other contexts as well (such as a square Init method. See below)
//
//Remark: emulates the C++ constructor init part
func (rectangle *Rectangle) Init(width float32, height float32) {
log.Println("Rectangle::Init")
//call the base's PrintableShapeInfo struct Init method
rectangle.PrintableShapeInfo.Init("###")
rectangle.width = width
rectangle.height = height
}
//Compute the rectangle's area
func (rectangle *Rectangle) Area() float32 {
log.Println("Rectangle::Area")
return float32(rectangle.width * rectangle.height)
}
//Compute the rectangle's perimeter
func (rectangle *Rectangle) Perimeter() float32 {
log.Println("Rectangle::Perimeter")
return float32(2 * (rectangle.width + rectangle.height))
}
//Get the rectangle's object name
func (rectangle *Rectangle) Name() string {
log.Println("Rectangle::Name")
return "rectangle"
}
//====================== Circle =============================
type Circle struct {
PrintableShapeInfo //like C++ inheritence, although go has no such a thing
radius float32 //circle's radius
}
//Creates and init a new circle object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor
func NewCircle(radius float32) *Circle {
log.Println("NewCircle")
circle := new(Circle) //allocate data
circle.Shape = circle //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
circle.Init(radius) //init class
return circle
}
//Init a new circle object. The method is distinct so that it can be called from other contexts as well if needed
//
//Remark: emulates the C++ constructor init part
func (circle *Circle) Init(radius float32) {
log.Println("Circle::Init")
//call the base's PrintableShapeInfo struct Init method
circle.PrintableShapeInfo.Init("ooo")
circle.radius = radius
}
//Compute the circle's area
func (circle *Circle) Area() float32 {
log.Println("Circle::Area")
return math.Pi * float32(circle.radius*circle.radius)
}
//Compute the circle's perimeter
func (circle *Circle) Perimeter() float32 {
log.Println("Circle::Perimeter")
return 2 * math.Pi * float32(circle.radius)
}
//Get the circle's object name
func (circle *Circle) Name() string {
log.Println("Circle::Name")
return "circle"
}
//====================== Rectangle =============================
//Implement Square in terms of Rectangle
type Square struct {
Rectangle //like C++ inheritance, although go has no such a thing
}
//Creates and init a new square object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor init
func NewSquare(width float32) *Square {
log.Println("NewSquare")
square := new(Square) //allocate data
square.Shape = square //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
square.Init(width) //init class
return square
}
//Init a new square object. The method is distinct so that it can be called from other contexts as well if needed
//
//Remark: emulates the C++ constructor init part
func (square *Square) Init(width float32) {
log.Println("Square::Init")
//since the Rectangle field is anonymous it's nice that we can directly call its un-overwritten methods but we can still access it, as named Rectangle, along with its (even overwritten) methods
square.Rectangle.Init(width, width) //call Rectangle's init to initialize its members. Since Square is implemented in Rectangle's terms, there nothing else needed
}
//Compute the square's area
func (square *Square) Area() float32 {
log.Println("Square::Area")
//since the Rectangle field is anonymous it's nice that we can directly call it's un-overwritten methods but we can still access it, as named Rectangle, along with it's (even overwritten) methods
return square.Rectangle.Area()
}
//Compute the square's perimeter
func (square *Square) Perimeter() float32 {
log.Println("Square::Perimeter")
//since the Rectangle field is anonymous it's nice that we can directly call it's un-overwritten methods but we can still access it, as named Rectangle, along with it's (even overwritten) methods
return square.Rectangle.Perimeter()
}
//Get the square's object name
func (square *Square) Name() string {
log.Println("Square::Name")
return "square"
}
func main() {
//initialize log subsystem so that we can display them at the main's end
// bufWriter := bytes.NewBuffer()
logStringWriter := bytes.NewBufferString("")
log.SetOutput(logStringWriter)
rectangle := NewRectangle(2, 3) //create a Rectangle object
rectangle.PrintInfo() //should manifest polymorphism behavior by calling Rectangle's Area, Perimeter and Name methods
circle := NewCircle(2) //create a Circle object
circle.PrintInfo() //should manifest polymorphism behavior by calling Circle's Area, Perimeter and Name methods
square := NewSquare(3) //create a Square object
square.PrintInfo() //should manifest polymorphism behavior by calling Square's Area, Perimeter and Name methods
//print constructs sizes
fmt.Printf(`
Go constructs sizes:
Shape interface size as seen by Rectangle struct: %d
`, unsafe.Sizeof(rectangle.Shape))
fmt.Printf("\tRectangle struct size: %d", unsafe.Sizeof(rectangle))
fmt.Printf(`
Shape interface size as seen by Circle struct: %d
`, unsafe.Sizeof(circle.Shape))
fmt.Printf("\tCircle struct size: %d", unsafe.Sizeof(circle))
fmt.Printf(`
Shape interface size as seen by Square struct: %d
`, unsafe.Sizeof(square.Shape))
fmt.Printf("\tCircle struct size: %d", unsafe.Sizeof(square))
//print the logs
fmt.Println("\n\nDumping traces")
fmt.Print(logStringWriter)
return
}
The central method (template method) is PrintInfo which, called for any defined shape, works as expected, by calling the right Area, Perimeter, and Name methods. Ex. circle.PrintInfo() will call circle.Area, circle.Perimeter and circle.Name.
The constructor functions, NewRectangle, NewCircle, and NewSquare construct shape objects and they're split into three steps:
- space allocation
- methods set (C++ like vtable) init, needed for polymorphic behavior
- struct members initialization, through Init methods
The struct member initialization is a distinct step for better code reuse. For example, Rectangle Init calls the base PrintableShapeInfo Init method while the Square Init method calls the base Rectangle Init (which calls PrintableShapeInfo Init, as said before).
Also, due to interfaces embedding, the object sizes increases only a bit, with a pair of pointers to the methods set and data area as can be seen in the example output.
I think the code looks pretty decent and the only concern being if specifically setting the Shape's interface method set (as the case of NewRectangle, NewCircle, and NewSquare functions) would trigger some side effects, as the code appears to work correctly?!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论