前言
在Go语言中,支持组合而不是继承。网上都说可以通过接口和结构体内嵌来模拟面向对象编程中的子类和父类关系。但给的例子或写法感觉都不是很好,难以达到我的目的(比如通过模板模式实现代码的重用等)。因此调查了一下实现方式。
结论
- 可以通过定义 interface + 组合接口变量 + NewXxx 函数中对接口变量赋值(类似于给 this 变量赋值) 的方式实现 Go 的继承和虚函数调用。废话少说, 直接上代码。
// 定义一个表示形状的 Shape 接口
type Shape interface {Name() stringArea() float64 //求面积Perimeter() float64 //求周长String() string
}// 定义实现了部分业务逻辑的抽象基类
type BaseShape struct {Shape // 组合了接口,因此有了其接口的方法声明, 注意: 其值为 nil, 需要在子类的 NewXxx 中对其赋值(相当于设置为 this)name string
}func (bs BaseShape) Name() string {return bs.name
}func (bs BaseShape) AbstractMethod() string {return "base"
}// 此处在 BaseShape 的 String() 方法中调用子类会重载的 Area()/Perimeter() 方法
func (bs BaseShape) String() string {if bs.Shape == nil {// 由于需要手动设置 Shape 的值(不像 C++ 可以在 new 时自动设置), 为了在 nil panic 的时候有更好的提示, 此处主动 panic,提醒这种编码错误panic(fmt.Sprintf("must set Shape in child struct 's new function, name=%s", bs.name))}//这里既可以通过 bs.Xxx() 调用, 也可以通过 bs.Shape.Xxx() 调用.return fmt.Sprintf("name=%s: absMethod=%s, area=%f, perimeter=%f",bs.Name(), bs.AbstractMethod(), bs.Shape.Area(), bs.Shape.Perimeter())
}type Rect struct {BaseShapeWidth float64Height float64
}func (r *Rect) Area() float64 {return r.Width * r.Height
}
func (r *Rect) Perimeter() float64 {return 2 * (r.Width + r.Height)
}func (r *Rect) AbstractMethod() string {return "abs_rect"
}func NewRect(width, height float64) *Rect {rect := &Rect{BaseShape: BaseShape{name: "rect"},Width: width,Height: height,}rect.Shape = rect // 将 BaseShape 中的 Shape 变量设置为 this,父类才可以调用return rect
}type Square struct {Rect
}func NewSquare(sides float64) *Square {square := &Square{Rect{BaseShape: BaseShape{name: "square"},Height: sides,Width: sides,}}square.Shape = squarereturn square
}
type Circle struct {BaseShapeRadius float64
}func (c *Circle) Area() float64 {return math.Pi * c.Radius * c.Radius
}
func (c *Circle) Perimeter() float64 {return 2 * math.Pi * c.Radius
}func (c *Circle) AbstractMethod() string {return "abs_circle"
}func NewCircle(radius float64) *Circle {circle := &Circle{BaseShape: BaseShape{name: "circle"},Radius: radius,}circle.Shape = circle //将 BaseShape 中的 Shape 变量设置为 this,父类才可以调用return circle
}
以上代码实现了如下的继承结构,可以将 String() 方法看成业务方法,其中调用子类提供实现的抽象方法。
测试代码
通过 shape 变量的 String() 方法调用了子类的 Name()/Area()/Perimeter() 等方法.
func TestVirtualFunctionCall(t *testing.T) {var rect Shape = NewRect(10, 20)var circle Shape = NewCircle(10)var square Shape = NewSquare(10)assert.Equal(t, "name=rect: absMethod=base, area=200.000000, perimeter=60.000000", rect.String())assert.Equal(t, "name=square: absMethod=base, area=100.000000, perimeter=40.000000", square.String())assert.Equal(t, "name=circle: absMethod=base, area=314.159265, perimeter=62.831853", circle.String())
}
补充说明
- 通过这种方法,可以成功的在 Go 语言中实现 多态,虚函数 等 C++/Java 才有的概念,从而可以想办法实现各种设计模式,大大提高项目质量。
- 但也存在一些限制,比如:
- 无法在
BaseShape
中再次定义一些虚函数由子类实现并调用(参考AbstractMethod
),如想要其也是虚函数的样子,必须也定义到 interface 中.
- 无法在