1.048596

在Go中实现面向对象编程

至于Go语言到底是否支持面向对象的编程,语言作者给出的答案是Yes and no。在其中有对象的存在和方法,但是没有显式的多态和各种面向对象编程中传统意义上的特性。所以要稍微改变一下之前面向对象的编程思想,不要写出为了面向对象而面向对象的别扭代码。

面向对象的基本特征分别是封装、继承和多态

结构体对象

基本语法

提到面向对象在Go语言中就是靠结构体实现的,并没有分出专门的数据类型给这种编程方法。


func (p Person) GetName() string {
	return p.name
}
func main() {
	person := Person{"Wong", 19}
	// Or use the key-value pair method.
	person := Person{name: "Wong", age: 19}
}

构造函数也是构造对象的一个非常好的方法比起直接当作结构体赋值稍稍复杂一点。

func NewPerson() Person {
	return Person{
		name: "Wong",
		age:  19,
	}
}
  • 元素封装性

因为是使用结构体,元素的封装性只存在于包和包之间,元素名大写包外可读,小写则包外不可读

方法

同时方法也不再需要在结构体内声明,而是需要加入接收者来表明是哪一个结构体的方法。这里分为两种,值接收者指针接收者。同时这里有一个语法糖就是习以为常的.访问函数,其实函数是如下的真面目。

t.Func()==Func(t)

接收器类型可以是几乎“任何”类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是intboolstring 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误invalid receiver type

实现方法的是命名类型

type SliceInt []int
// Define exclusive methods for aliased types.
func (s SliceInt) Sum() int {
	var sum = 0
	for _, v := range s {
		sum += v
	}
	return sum
}

值接收者

值接收者的书写方式就是在函数名前加上(c ClassName)的标注,这样表明了该函数是哪个类的方法。这种传递方法能获取结构体的值,访问结构体的元素,但是不能够修改因为接收者是以值的形式传递给函数的。

func (p Person) GetName() string {
	return p.name
}

指针接收者

因为方法就是面向对象的特殊的函数,函数的第一个参数如上文所说的一样是接收者也就是调用者。那么将调用者以指针的方式传递进函数就能够起到修改对象的值的效果。

func (p *Person) SetName(newName string) {
	p.name = newName
}

调用者

有了上面两种调用方法的方式之后是否需要区分调用者是指针还是不同变量呢?答案是否定的,因为调用这无论是什么形式,调用方法的时候Go编译器已经做好了转换,再加上对指针的统一优化,这样的情况不必区分调用者类型。

面向对象特性

传统的面向对象编程的特性在Go中并没有十分典型的体现,尤其是多态重载方面实现很麻烦。

继承

Golang中继承是通过组合结构体的方式来实现的,它中没有显式的继承,只有匿名结构体嵌套来实现继承。结构体通过匿名嵌套可以大致实现其他语言中类的继承的功能,这一点也为了面向编程提供了帮助,同时支持了多继承

嵌套继承

type Animal struct {
	// code...
}
func (a Animal) move() {
	fmt.Println("move...")
}
type Dog struct {
	name   string
	// Anonymous structure field nested to achieve inheritance.
	animal
}
func main() {
	tom := Dog{name: "tom", animal{...}}
	// In this way, you can inherit the methods of the "parent class", but the
	// initialization must also initialize the elements of the parent class.
	tom.move()
}

构造的时候要记者构造父类的属性,或者调用父类的构造函数,其他基本上和结构体差不多。

代码重写

父类中的代码可以理解为全部都是可填充可替代的,父类的属性可以经过同名的派生类属性去覆盖,同样的方法名也可以起到重新覆盖重写方法的效果,基类中的部分默认可替代

  • 就近原则

在继承之后的派生类对象进行调用某一个元素或者方法的时候,编译器就会采用就近原则查看是否被重写,如果重写则会调用新的,繁殖调用老的。

函数多态

多态性一直都是面向对象语言不可缺少的一部分,但由于Golang的继承方式比较简洁,并没有直接显式的方式来实现多态,但是这并不代表其中无法实现多态。还可以使用interface接口来实现多态的效果,著名的Duck Typing理论很好的解释了接口的特性。

使用接口来做多态的基础

type Animal interface {
	Speak()
}

类来实现接口

type Dog struct{}
func (d Dog) Speak() {
	fmt.Println("Know-wow!")
}
type Cat struct{}
func (c Cat) Speak() {
	fmt.Println("Meow~")
}

使用接口泛指实现接口的数据类型

func AllSpeak(a Animal) {
	a.Speak()
	// Polymorphism is implemented with interfaces.
}

对于接口这样的实现多态的方式,只需要满足like-a的关系而不需要is-a的关系。接口比继承更加的灵活,这样可以实现代码的解耦。遗憾的是目前Go语言不支持函数的重载,不能实现完整的多态。

结构体组合

结构体和结构体之间不只是只有继承这种关系,如果使用有名结构体则是组合,这样的话调用结构体的子结构体时需要使用名称而不是直接通过父结构体实例进行调用了。

type Person struct {
	Home    Building
	Company Building
}
type Building struct {
	Area float64
}