Go
面向对象编程的三大特性:封装、继承和多态。
- 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
- 继承:使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
- 多态:不同对象中同种行为的不同实现方式
Go
语言的结构体(struct
)和其他语言的类(class
)有同等的地位,但 Go
语言放弃了包括继
承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。
例如,我们要定义一个矩形类型
type Rect struct {
x, y float64
width, height float64
}
然后我们定义成员方法 Area() 来计算矩形的面积:
func (r *Rect) Area() float64 {
return r.width * r.height
}
在定义了 Rect
类型后,该如何创建并初始化 Rect
类型的对象实例呢?这可以通过如下几种方法实现:
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
在 Go
语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如 bool
类型的零值为 false
,int
类型的零值为 0,string
类型的零值为空字符串。
在 Go
语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以 NewXXX
来命名,表示“构造函数”:
func NewRect(x, y, width, height float64) *Rect {
return &Rect{x, y, width, height}
}
详细参见:https://blog.csdn.net/wohu1104/article/details/106202892 中的结构体初始化章节。
1. 封装
package main
import "fmt"
type data struct {
val int
}
func (p_data *data) set(num int) {
p_data.val = num
}
func (p_data *data) show() {
fmt.Println(p_data.val)
}
func main() {
p := &data{4}
p.set(10)
p.show()
}
或者
package main
import (
"fmt"
)
// 矩形结构体
type Rectangle struct {
Length int
Width int
}
// 计算矩形面积
func (r *Rectangle) Area() int {
return r.Length * r.Width
}
func main() {
r := Rectangle{4, 2}
// 调用 Area() 方法,计算面积
fmt.Println(r.Area())
}
2. 继承
确切地说,Go
语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go
语言的继承方式采用的是匿名组合的方式。
package main
type Base struct {
Name string
}
func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }
type Foo struct {
Base
...
}
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}
以上代码定义了一个 Base
类(实现了 Foo()
和 Bar()
两个成员方法),然后定义了一个 Foo
类,该类从Base
类“继承”并改写了 Bar()
方法(该方法实现时先调用了基类的 Bar()
方法)。
在“派生类” Foo
没有改写“基类” Base
的成员方法时,相应的方法就被“继承”,例如在上面的例子中,调用foo.Foo()
和调用 foo.Base.Foo()
效果一致。
此外,在 Go
语言中你还可以随心所欲地修改内存布局,如:
type Foo struct {
... // 其他成员
Base
}
这段代码从语义上来说,和上面给的例子并无不同,但内存布局发生了改变。“基类” Base
的数据放在了“派生类” Foo
的最后。
另外,在 Go
语言中,你还可以以指针方式从一个类型“派生”
type Foo struct {
*Base
...
}
这段 Go
代码仍然有“派生”的效果,只是 Foo
创建实例的时候,需要外部提供一个 Base
类实例的指针。
另外,我们必须关注一下接口组合中的名字冲突问题,比如如下的组合:
type X struct {
Name string
}
type Y struct {
X
Name string
}
组合的类型和被组合的类型都包含一个 Name
成员,会不会有问题呢?
答案是否定的。所有的 Y
类型的 Name
成员的访问都只会访问到最外层的那个 Name
变量,X.Name
变量相当于被隐藏起来了。
那么下面这样的场景呢:
type Logger struct {
Level int
}
type Y struct {
*Logger
Name string
*log.Logger
}
显然这里会有问题。因为之前已经提到过,匿名组合类型相当于以其类型名称(去掉包名部分)作为成员变量的名字。按此规则,Y
类型中就相当于存在两个名为 Logger
的成员,虽然类型不同。
因此,我们预期会收到编译错误。有意思的是,这个编译错误并不是一定会发生的。假如这两个 Logger
在定义后再也没有被用过,那么编译器将直接忽略掉这个冲突问题,直至开发者开始使用其中的某个 Logger
。
Woman
结构体中包含匿名字段 Person
,那么 Person
中的属性也就属于 Woman
对象。
package main
import "fmt"
type Person struct {
name string
}
type Man struct {
Person
sex string
}
func main() {
man := Man{Person{"wohu"}, "男"}
fmt.Println(man.name) // wohu
fmt.Println(man.sex) // 男
}
package main
import "fmt"
type parent struct {
val int
}
type child struct {
parent
num int
}
func main() {
c := child{parent{1}, 2}
fmt.Println(c.num)
fmt.Println(c.val)
}
3. 多态
在面向对象中,多态的特征为:不同对象中同种行为的不同实现方式。在 Go 语言中可以使用接口实现这一特征。
package main
import (
"fmt"
)
// 正方形
type Square struct {
side float32
}
// 长方形
type Rectangle struct {
length, width float32
}
// 接口 Shaper
type Shaper interface {
Area() float32
}
// 计算正方形的面积
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
// 计算长方形的面积
func (r *Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
// 创建并初始化 Rectangle 和 Square 的实例,由于这两个实例都实现了接口中的方法,
//所以这两个实例,都可以赋值给接口 Shaper
r := &Rectangle{10, 2}
q := &Square{10}
// 创建一个 Shaper 类型的数组
shapes := []Shaper{r, q}
// 迭代数组上的每一个元素并调用 Area() 方法
for n, _ := range shapes {
fmt.Println("矩形数据: ", shapes[n])
fmt.Println("它的面积是: ", shapes[n].Area())
}
}
/*
矩形数据: &{10 2}
它的面积是: 20
图形数据: &{10}
它的面积是: 100
*/
package main
import "fmt"
type act interface {
write()
}
type xiaoming struct {
}
type xiaobai struct {
}
func (xm *xiaoming) write() {
fmt.Println("xiaoming write")
}
func (xf *xiaobai) write() {
fmt.Println("xiaobai write")
}
func main() {
/*
> 接口特点:
> + 接口只有方法声明、没有实现,没有数据字段
> + 接口可以匿名嵌入其它接口,或者嵌入到结构中
> 接口是用来定义行为的类型,这些被定义的行为不由接口直接实现,
> 而是由用户定义的类型实现,**一个实现了这些方法的具体类型是这个接口类型的实例。**
**如果用户定义的类型实现了某个接口类型声明的一组方法,
那么这个用户定义的类型的值就可以赋给这个接口类型的值。
这个赋值会把用户定义的类型存入接口类型的值。**
*/
var w act
xm := xiaoming{}
xb := xiaobai{}
w = &xm
w.write()
w = &xb
w.write()
}
输出结果:
xiaoming write
xiaobai write
或者以下代码,将结构体初始化封装为函数。
// 创建初始化函数,初始化结构体对象,返回为接口对象
func NewXiaoming(xm xiaoming) act {
return &xm
}
// 创建初始化函数,初始化结构体对象,返回为接口对象
func NewXiaobai(xb xiaobai) act {
return &xb
}
func main() {
m := NewXiaoming(xiaoming{})
m.write()
b := NewXiaobai(xiaobai{})
b.write()
}
Go
中的接口可以说是方法特征的集合表达。要实现其接口,只需要实现接口中的所有方法即可。
package main
import (
"fmt"
)
type animal interface {
run()
breath()
}
type dog struct {
legs int
nose string
}
type fish struct {
fin string
gill string
}
func (d dog) run() {
fmt.Printf("Dog runs with %d legs\n", d.legs)
}
func (d dog) breath() {
fmt.Printf("Dog breath with %s\n", d.nose)
}
func (f fish) run() {
fmt.Printf("Fish runs with %s\n", f.fin)
}
func (f fish) breath() {
fmt.Printf("Fisn breath with %s\n", f.gill)
}
func behavior(an animal) {
an.run()
an.breath()
}
func main() {
d := dog{
legs: 4,
nose: "nose",
}
f := fish{
fin: "fin",
gill: "gill",
}
behavior(d)
behavior(f)
}
输出结果:
Dog runs with 4 legs
Dog breath with nose
Fish runs with fin
Fisn breath with gill
程序首先定义了一个 animal
接口,它有 run()
、breath()
两个方法。接着定义了两种类型,分别是 dog
和 fish
,显然这两种类型的动物都拥有 animal
动物类的两个方法,因而各自实现了它们。最后一个带有 animal
参数的 behavior
函数的出现很好地诠释了面向对象中多态的构造形式。