Go 面向组合编程

在面向对象的语言如 Java, C++ 中,面向对象的 3 大特性:封装、继承、多态。
而 Go 语言并不是一门面向对象的语言,没有类的概念,接口的出现让结构和方法解耦,因此 Go 诞生了面向组合编程。

封装

封装即实现数据的隐藏,有权限才能访问。

在 Java 中只需要用要 private, protected, public 这三个关键字修饰字段或方法(或不写),即可实现 4 种访问层次控制:类中访问,子类访问,公开访问和包内访问。

而在 Go 语言中,只有 2 种访问层次控制:包内访问和包外访问。
实现方式:字段名、结构体名或方法名开头字母大写则为包外访问,小写则为包内访问。

  1. // hello.go
  2. package hello
  3. type MyHelloWord struct { // main 包也能使用该结构体名
  4. Name string // main 包也能访问该字段
  5. age int // 只能在 hello 包内访问该字段
  6. }
  7. func SayHello(name string) {...} // main 包也能调用改方法
  8. func myAge(age int) {...} // 只能在 hello 包内调用
  9. func (this *MyHelloWord) cool() {...} // 只能在 hello 包内调用
  10. func (this *MyHelloWord) Cool() {...} // main 包也能调用改方法
  11. // main.go
  12. package main
  13. import "hello"
  14. func main() {
  15. m := hello.MyHelloWord{Name: "zwjason", age: 22} // fail
  16. hello.SayHello(m.Name) // ok
  17. hello.myAge(m.age) // fail
  18. }

其实 hello.go 中 MyHelloWord 结构体的定义,在 main.go 中甚至不能定义 MyHelloWord 和 *MyHelloWord 类型以及与它的变量(如 m) ,因为 age 字段不能在包外访问。

一个比较规范的、包外访问的结构体定义如下:

  1. package person
  2. type Person struct {
  3. name string
  4. age int
  5. }
  6. /* 包外访问方法 */
  7. // 构造函数
  8. func New(name string, age int) *Person {
  9. return &Person{
  10. name: name,
  11. age: age,
  12. }
  13. }
  14. // name 的 setter 方法
  15. func (p *Person) SetName(name string) {
  16. p.name = name
  17. }
  18. // name 的 getter 方法
  19. func (p *Person) Name() string {
  20. return p.name
  21. }
  22. // age 的 setter 方法
  23. func (p *Person) SetAge(age int) {
  24. p.age = age
  25. }
  26. // age 的 getter 方法
  27. func (p *Person) Age() int {
  28. return p.age
  29. }
  30. // 其他包外访问方法
  31. /* 包内访问方法 */
  32. // 自定义一些包内方法,例如写一些辅助方法使得写一些包外方法更简洁
  33. // 如容器 list 的实现中,很多包外方法都用到了相同的逻辑
  34. // 把这部分逻辑抽了出来定义为包内方法,使得包外方法写得更简洁

仔细观察可以发现,getter 方法没有 get 开头,在 Go 中写 getter 方法只需要把字段的首字母大写作为函数名即可(如果字段本身就是首字母大写的,就没必要写 setter 和 getter 了)。

继承

在 Java 中使用 entends 关键字实现继承,implement 关键字实现接口,子类能够继承父类中非 private 的字段和方法以实现代码复用和扩展。

而在 Go 中没有继承机制,但是却可以使用组合实现类似继承的功能,组合即结构体嵌套。

  1. type Student struct {
  2. Person *person.Person // 首字母大写
  3. id string
  4. score int
  5. }

新定义一个结构体 Student,内嵌一个 *Person 字段,注意 Person 字段首字母大写,否则无法在包外引用 Person 的提供的所有的包外方法和字段。

详细代码如下:

  1. // student.go
  2. package student
  3. import (
  4. "fmt"
  5. "person"
  6. )
  7. type Student struct {
  8. Person *person.Person
  9. id string
  10. score int
  11. }
  12. /* 包外访问方法 */
  13. func New(name string, age int, id string, score int) *Student {
  14. return &Student{
  15. Person: person.New(name, age),
  16. id: id,
  17. score: score,
  18. }
  19. }
  20. func (s *Student) ShowInfo() {
  21. fmt.Println(s.Person.Name())
  22. fmt.Println(s.Person.Age())
  23. fmt.Println(s.id)
  24. fmt.Println(s.score)
  25. }
  26. /* 包内访问方法 */
  27. // 自定义包内访问方法
  28. // main.go
  29. package main
  30. import (
  31. "student"
  32. )
  33. func main() {
  34. s := student.New("zwjason", 22, "201826010505", 100)
  35. s.ShowInfo() // Student 的方法
  36. s.Person.ShowInfo() // Person 的方法
  37. }
  38. 输出如下:
  39. zwjason
  40. 22
  41. 201826010505
  42. 100
  43. bankarian
  44. 20

多态

多态指的是一个实体可以具有多种形式,在 Java 中表现为子类对象可以以父类的身份出现。
以如下继承结构为例:
image.png
在代码中我们可以使用 Person p = new Student() 这样的写法,即把子类对象赋给了它的父类类型变量。

在 Go 中,采用接口来实现类似功能。
实现原理:若一个类型实现了一个接口,那么该类型的实例可以赋给它所实现的接口类型的变量。
我们把上面 person.go 中的 ShowInfo() 方法改为接口:

  1. // person.go
  2. // 接口
  3. type Infoer interface {
  4. ShowInfo()
  5. }
  6. // main.go
  7. func main() {
  8. s := student.New("zwjason", 22, "201826010505", 100)
  9. p := person.New("bankarian", 20)
  10. var info person.Infoer = s // Student 实例以 Infoer 的身份出现
  11. info.ShowInfo() // 调用的是 Student 实现的
  12. info = p // Person 实例以 Infoer 的身份出现
  13. info.ShowInfo() // 调用的是 Person 实现的
  14. }
  15. 输出如下:
  16. zwjason
  17. 22
  18. 201826010505
  19. 100
  20. bankarian
  21. 20

Person 和 Student 类型都实现了 Infoer 接口,两者的实例都可以赋给 Infoer 变量,而且两者的 ShowInfo() 方法相互独立,互不影响,这可以看作是粗略的方法重载
多种类型共同实现一种接口类型,每种类型各自拥有一套接口内定义的方法集合,互不影响。
但是要注意如下调用规则:
Go 语言规范定义了接口方法集的调用规则:
类型 T 的可调用方法集包含接受者为 T 或 T 的所有方法集(因为指针可以被解引用);
类型 T 的可调用方法集包含接受者为 T 的所有方法;
类型 T 的可调用方法集不包含接受者为 *T 的方法(因为存储在接口中的值没有地址)

重载

在面向对象中,字段或方法重载也是经常发生的,那么在 Go 中如何实现呢?

方法一

我们可以看到,在上面介绍多态的过程中,将需要重载的方法定义为接口内的方法,然后让结构体都实现这个接口即可实现方法重载。

方法二

首先明白,在 Go 中是如何处理命名冲突的,即由于组合发生了字段名或方法名相同的情况。
命名冲突处理规则:外层覆盖内层,但保留两者的内存空间。
若把 person.go 和 student.go 中的 ShowInfo() 方法仅定义为包外访问,没有接收者,不属于任何接口,那么
student 的 ShowInfo() 将覆盖掉 Person 的 ShowInfo()。