官方关于golang的继承和重载的FAQ

原文部分来自:https://segmentfault.com/a/1190000022429780

  • 关于类型继承

面向对象的编程,至少在最著名的语言中,涉及对类型之间关系的过多讨论,这些关系通常可以自动派生。Go 采取了不同的方法。

与其要求程序员提前声明两种类型是相关的,在 Go 中,类型会自动满足任何指定其方法子集的接口。除了减少簿记之外,这种方法还有真正的优势。类型可以同时满足多个接口,没有传统多重继承的复杂性。接口可以是非常轻量级的——具有一个甚至零个方法的接口可以表达一个有用的概念。如果出现新想法或用于测试,可以事后添加接口——无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有要管理或讨论的类型层次结构。

可以使用这些想法来构建类似于类型安全的 Unix 管道的东西。例如,了解如何fmt.Fprintf 为任何输出启用格式化打印,而不仅仅是文件,或者bufio包如何与 文件 I/O 完全分离,或者image包如何生成压缩图像文件。所有这些想法都源于io.Writer表示单个方法 ( Write)的单个接口( )。而这只是皮毛。Go 的接口对程序的结构有着深远的影响。

这需要一些时间来适应,但这种隐式的类型依赖是 Go 最高效的事情之一。

​ –faq: https://golang.org/doc/faq#inheritance

  • 关于重载的定义

如果不需要进行类型匹配,则方法分派会得到简化。使用其他语言的经验告诉我们,拥有多种名称相同但签名不同的方法有时很有用,但在实践中也可能会令人困惑和脆弱。仅按名称匹配并要求类型的一致性是 Go 类型系统中一个主要的简化决定。

关于运算符重载,它似乎更方便而不是绝对要求。同样,没有它,事情会更简单。

​ – faq:https://golang.org/doc/faq#overloading

从一个案例引入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
type ShapeInterface interface {
	Area() float64
	GetName() string
	PrintArea()
}

type Shape struct {
	name string
}

func (s *Shape) GetName() string {
	return s.name
}

func (s *Shape) Area() float64 {
	return 0.0
}

func (s *Shape) PrintArea() {
	fmt.Printf("%s : Area %v\r\n", s.GetName(), s.Area())
}

// Rectangle 矩形求面积
type Rectangle struct {
	Shape
	w, h float64
}

func (r *Rectangle) Area() float64 {
	return r.w * r.h
}

// Circle 圆形  : 重新定义 Area 和PrintArea 方法
type Circle struct {
	Shape
	r float64
}

func (c *Circle) Area() float64 {
	return c.r * c.r * math.Pi
}

func (c *Circle) PrintArea() {
	fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area())
}

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 := []ShapeInterface{&s, &c, &r}

	for _, si := range listshape {
		si.PrintArea() //!! 猜猜哪个Area()方法会被调用 !!
	}

}


out: 
Shape : Area 0
Circle : Area 314.1592653589793
Rectangle : Area 0    // 为啥这里没有调用 5 * 4

原因分析:Rectangle通过组合Shape获得的PrintArea()方法并没有去调用Rectangle实现的Area()方法,而是去调用了ShapeArea()方法。Circle是因为自己重写了PrintArea()所以在方法里调用到了自身的Area()

解决方案

1.将要使用的值抽取成一个方法,在初始化中进行赋值。

定义了一个类似InitShape的方法来完成初始化流程,这里我把ShapeInterface接口和Shape类型做一些调整会更好理解一些。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)
  ...
}

对于RectangleCircle这样的组合Shape的类型,只需要按照自己的计算面积的公式实现Area()SetArea()会把Area()计算出的面积存储在area字段供后面的程序使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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()

2.按照接口的定义,实现所有相关联的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Rectangle 矩形求面积 : 重新定义了 Area 方法
type Rectangle struct {
	Shape
	w, h float64
}

func (r *Rectangle) Area() float64 {
	return r.w * r.h
}
// 实现 PrintArea 方法,
func (r *Rectangle) PrintArea() {
	fmt.Printf("%s : Area %v\r\n", r.GetName(), r.Area())
}

3.用组合的方式去减少代码量(推荐)

面向接口编程

  • 接口里面的方法之间不应该存在相互依赖,应该用单一的方法组合去完成你的功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
type ShapeInterface interface {
	GetName() string
	GetArea() float64
}

type Shape struct {
	name string
}

func (s *Shape) GetName() string {
	return s.name
}

func (s *Shape) GetArea() float64 {
	return 0.0
}

// Rectangle 矩形求面积 : 重新定义了 Area 方法
type Rectangle struct {
	Shape
	w, h float64
}

func (r *Rectangle) GetArea() float64 {
	return r.w * r.h
}

// Circle 圆形  : 重新定义 Area 和PrintArea 方法
type Circle struct {
	Shape
	r float64
}

func (c *Circle) GetArea() float64 {
	return c.r * c.r * math.Pi
}

// PrintAreaInterface 抽象出一个输出面积的接口
type PrintAreaInterface interface {
	PrintArea(ShapeInterface)
}

type PrintAreas struct {
}

func (p *PrintAreas) PrintArea(shape ShapeInterface) {
	fmt.Printf("%s : Area %v\r\n", shape.GetName(), shape.GetArea())
}

func NewPrintAreas() PrintAreaInterface {
	return &PrintAreas{}
}

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 := []ShapeInterface{&s, &c, &r}

	areas := NewPrintAreas()

	for _, si := range listshape {
		areas.PrintArea(si)
	}

}

方案一

优点

  • 重复代码量少,可复用重复逻辑方法

缺点

  • 接口抽象不太优雅
  • 容易留坑

方案二

优点

  • 接口职责单一,抽象优雅
  • 不容易留坑,代码可读性好

缺点

  • 重复代码量多

方案三(推荐)

优点

  • 符合golang的面向接口编程哲学

缺点

  • 暂无🐶

小结

由于本人之前是Python转到golang,对于面向对象的理解和golang的设计不太相符,应该是golang不是纯面向对象的语言,文章开头的几个FAQ也阐述了golang对面向对象支持的问题,所以在代码设计和抽象的时候不能完全的按照面向对象的方式去思考,golang应该是组合,而不是继承。和朋友探讨下来最终实现了方案三(推荐),面向接口编程的哲学。