Go 面向组合编程
在面向对象的语言如 Java, C++ 中,面向对象的 3 大特性:封装、继承、多态。
而 Go 语言并不是一门面向对象的语言,没有类的概念,接口的出现让结构和方法解耦,因此 Go 诞生了面向组合编程。
封装
封装即实现数据的隐藏,有权限才能访问。
在 Java 中只需要用要 private, protected, public 这三个关键字修饰字段或方法(或不写),即可实现 4 种访问层次控制:类中访问,子类访问,公开访问和包内访问。
而在 Go 语言中,只有 2 种访问层次控制:包内访问和包外访问。
实现方式:字段名、结构体名或方法名开头字母大写则为包外访问,小写则为包内访问。
// hello.go
package hello
type MyHelloWord struct { // main 包也能使用该结构体名
Name string // main 包也能访问该字段
age int // 只能在 hello 包内访问该字段
}
func SayHello(name string) {...} // main 包也能调用改方法
func myAge(age int) {...} // 只能在 hello 包内调用
func (this *MyHelloWord) cool() {...} // 只能在 hello 包内调用
func (this *MyHelloWord) Cool() {...} // main 包也能调用改方法
// main.go
package main
import "hello"
func main() {
m := hello.MyHelloWord{Name: "zwjason", age: 22} // fail
hello.SayHello(m.Name) // ok
hello.myAge(m.age) // fail
}
其实 hello.go 中 MyHelloWord 结构体的定义,在 main.go 中甚至不能定义 MyHelloWord 和 *MyHelloWord 类型以及与它的变量(如 m) ,因为 age 字段不能在包外访问。
一个比较规范的、包外访问的结构体定义如下:
package person
type Person struct {
name string
age int
}
/* 包外访问方法 */
// 构造函数
func New(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}
// name 的 setter 方法
func (p *Person) SetName(name string) {
p.name = name
}
// name 的 getter 方法
func (p *Person) Name() string {
return p.name
}
// age 的 setter 方法
func (p *Person) SetAge(age int) {
p.age = age
}
// age 的 getter 方法
func (p *Person) Age() int {
return p.age
}
// 其他包外访问方法
/* 包内访问方法 */
// 自定义一些包内方法,例如写一些辅助方法使得写一些包外方法更简洁
// 如容器 list 的实现中,很多包外方法都用到了相同的逻辑
// 把这部分逻辑抽了出来定义为包内方法,使得包外方法写得更简洁
仔细观察可以发现,getter 方法没有 get 开头,在 Go 中写 getter 方法只需要把字段的首字母大写作为函数名即可(如果字段本身就是首字母大写的,就没必要写 setter 和 getter 了)。
继承
在 Java 中使用 entends 关键字实现继承,implement 关键字实现接口,子类能够继承父类中非 private 的字段和方法以实现代码复用和扩展。
而在 Go 中没有继承机制,但是却可以使用组合实现类似继承的功能,组合即结构体嵌套。
type Student struct {
Person *person.Person // 首字母大写
id string
score int
}
新定义一个结构体 Student,内嵌一个 *Person 字段,注意 Person 字段首字母大写,否则无法在包外引用 Person 的提供的所有的包外方法和字段。
详细代码如下:
// student.go
package student
import (
"fmt"
"person"
)
type Student struct {
Person *person.Person
id string
score int
}
/* 包外访问方法 */
func New(name string, age int, id string, score int) *Student {
return &Student{
Person: person.New(name, age),
id: id,
score: score,
}
}
func (s *Student) ShowInfo() {
fmt.Println(s.Person.Name())
fmt.Println(s.Person.Age())
fmt.Println(s.id)
fmt.Println(s.score)
}
/* 包内访问方法 */
// 自定义包内访问方法
// main.go
package main
import (
"student"
)
func main() {
s := student.New("zwjason", 22, "201826010505", 100)
s.ShowInfo() // Student 的方法
s.Person.ShowInfo() // Person 的方法
}
输出如下:
zwjason
22
201826010505
100
bankarian
20
多态
多态指的是一个实体可以具有多种形式,在 Java 中表现为子类对象可以以父类的身份出现。
以如下继承结构为例:
在代码中我们可以使用 Person p = new Student()
这样的写法,即把子类对象赋给了它的父类类型变量。
在 Go 中,采用接口来实现类似功能。
实现原理:若一个类型实现了一个接口,那么该类型的实例可以赋给它所实现的接口类型的变量。
我们把上面 person.go 中的 ShowInfo() 方法改为接口:
// person.go
// 接口
type Infoer interface {
ShowInfo()
}
// main.go
func main() {
s := student.New("zwjason", 22, "201826010505", 100)
p := person.New("bankarian", 20)
var info person.Infoer = s // Student 实例以 Infoer 的身份出现
info.ShowInfo() // 调用的是 Student 实现的
info = p // Person 实例以 Infoer 的身份出现
info.ShowInfo() // 调用的是 Person 实现的
}
输出如下:
zwjason
22
201826010505
100
bankarian
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()。