go语言进阶篇——面向对象(一)
什么是面向对象
在我们设计代码时,比如写一个算法题或者写一个问题结局办法时,我们常常会使用面向过程的方式来书写代码,面向过程主要指的是以解决问题为中心,按照一步步具体的步骤来编写代码或者调用函数,他在问题规模小的情况下简洁快速且十分有效。
当我们遇到的问题比较庞大且复杂的时候,面向过程的代码就会变得难以维护与重复使用,这时我们就需要对问题进行抽象,将一类具有鲜明特色的函数抽象为一类对象,我们只需要通过调用对象中的一个方法来处理某一类特定的问题,对象与对象之间用方法来交互,这种编程方法我们称之为面向对象编程。
面向对象主要的特征有三点:封装,继承,多态,它们在面向对象编程中占据着重要的地位,最后,开始今天的学习之前我们要明确一件事情:面向对象只是一种思想,它并不特指某一部分语言
Go语言的面向对象编程设计
前言
相对于传统的如c++,Java等语言而言,Go语言显得优雅且简洁,它没有去沿袭传统面向对象编程得诸多概念,比如类的继承,接口的实现,构造与析构函数等等,也不再有public
,private
,protected
等访问修饰符
Go语言的优雅之处,它对面向对象编程的支持是语言类型系统中的天然组成部分,整个类型系统通过接口串联,浑然一体
什么是类型系统
类型系统指的是一个语言的类型体系结构,一个典型类型系统一般包括以下内容:
- 基本类型(int,string ,byte,float)
- 复合类型(数组,切片,字典,字符串)
- 可以指向任意对象的类型(比如Go语言中的空接口)
- 值语义与引用语义(值语义指的是数据类型在赋值时会生成副本,彼此之间互不干扰,引用语义主要是多个副本共享一份数据,修改任意一个其他的也会随之修改)
- 面向对象,即所有具备面向对象特征(比如成员方法)的类型
- 接口
Java VS Go类型系统设计
Java
在Java语言中,存在两种完全独立的类型系统、
- 值类型系统,这里主要是基本类型,如int ,float,double等等
- 以
Object
类型为根的对象类型系统,它可以定义成员变量,成员方法,虚函数,这些一般是引用语义,在堆上分配内存
Java 语言中的 Any 类型就是整个对象类型系统的根 —— java.lang.Object
类型,只有对象类型系统中的实例才可以被 Any 类型引用。值类型想要被 Any 类型引用,需要经过装箱 (boxing)过程,比如 int
类型需要装箱成为 Integer
类型。
另外,在 Java 中,只有对象类型系统中的类型才可以实现接口。
Go
Go语言的大多数类型都是值语义,比如基本类型(int,float,double等等)或者是复合类型(数组,结构体等)
类的定义,初始化以及成员方法
类的定义与初始化
Go语言的面向对象编程与我们熟悉的Java等语言不同,它没有像class
,implements
,extend
之类的关键字以及相应的概念,主要还是依靠i结构体来实现的,比如我们现在像创建一个学生类:
type Student struct{
id sting
name string
age uint
sex string
}
类名为student
,并且包括了id
,name
,age
,sex
四个属性,Go语言也不支持构造函数与析构函数
所以我们可以定义全局函数NewStudent
来初始化
func NewStudent(id,nmae,sex string.age uint) *Student{
return &Strudent{id,nmae,sex,age}
}
当然我们也可以初始化指定字段:
func NewStudent1(age int,id,name string) *Student{
return &Student{
id: id,
name: name,
age: age,
}
}
在 Go 语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如 bool
类型的零值为 false
,int
类型的零值为 0,string
类型的零值为空字符串,float
类型的零值为 0.0
。
定义成员方法
值方法
由于 Go 语言不支持 class
这样的代码块,要为 Go 类定义成员方法,需要在 func
和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明),以 Student
类为例,要为其定义获取 name
值的方法,可以这么做:
func (s Student) GetName()string {
return s.name
}
这样一来,我们就可以在初始化 Student
类后,通过 GetName()
方法获取 name
值:
student := NewStudent(1, "学院君", 100)
fmt.Println("Name:", student.GetName())
可以看到,我们通过在函数签名中增加接收者声明的方式定义了函数所归属的类型,这个时候,函数就不再是普通的函数,而是类的成员方法了。
指针方法
在类的成员方法中,可以通过声明的类型变量来访问类的属性和其他方法(Go 语言不支持隐藏的 this
指针,所有的东西都是显式声明)。GetName
是一个只读方法,如果我们要在外部通过 Student
类暴露的方法设置 name
值,可以这么做:
func (s *Student) SetName(name string) {
s.name = name
}
你可能已经注意到,这里的方法声明和前面 GetXXX
方法声明不太一样,Student
类型设置成了指针类型:
s *Student
这是因为 Go 语言面向对象编程不像 PHP、Java 那样支持隐式的 this
指针,所有的东西都是显式声明的,在 GetXXX
方法中,由于不需要对类的成员变量进行修改,所以不需要传入指针,而 SetXXX
方法需要在函数内部修改成员变量的值,并且该修改要作用到该函数作用域以外,所以需要传入指针类型(结构体是值类型,不是引用类型,所以需要显式传入指针)。
我们可以把接收者类型为指针的成员方法叫做指针方法,把接收者类型为非指针的成员方法叫做值方法,二者的区别在于值方法传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部传入结构体实例的值拷贝,修改不会作用到外部传入的结构体实例
选择值方法还是指针方法
当我们有如下情形的考量时,需要将类方法定义为指针方法:
- 数据一致性:方法需要修改传入的类型实例本身;
- 方法执行效率:如果是值方法,在方法调用时一定会产生值拷贝,而大对象拷贝代价很大。
通常我们都会选择定义指针方法。
基于组合来实现类的继承与方法重写
要实现面向对象编程,就必须实现面向对象编程的三大特性:封装、继承和多态。上面我们已经介绍了类的封装,将函数定义为归属某个自定义类型,这就等同于实现了类的成员方法,如果这个自定义类型是基于结构体的,那么结构体的字段可以看做是类的属性。
继承
在go语言中并没有直接的提供有关继承的与凡是线,但是我们可以使用组合的方式来简介实现继承功能。
传统面向对象编程中,显式定义继承关系的弊端有两个:一个是导致类的层级越来越复杂,另一个是影响了类的扩展性,很多软件设计模式的理念就是通过组合来替代继承提高类的扩展性。减下来我们通过几个例子来看一下如何利用组合来实现继承
首先我们可以来定义一个父类Animal
type Animal struct {
Name string
}
func (a *Animal) Call() string{
return "动物的叫声是..."
}
func (a *Animal) FavorFood() string{
return "动物最喜欢的食物是..."
}
func (a *Animal) GetName(name string) string{
a.Name = name
}
如果我们想到一个子类Dog
,可以这么来写:
type Dog struct{
Animal
//other things
}
这里,我们在 Dog
结构体类型中,嵌入了 Animal
这个类型,这样一来,我们就可以在 Dog
实例上访问所有 Animal
类型包含的属性和方法,相当于通过组合实现了继承
多态
在go语言中我们可以通过字子类中定义同名方法来覆盖父类方法,比如我们现在重写一下Animal
中的方法:
func (d *Dog) Call() string{
return "汪汪汪"
}
func (d *Dog) FavorFood() string{
return "骨头"
}
当我们再执行 main
函数时,直接在 Dog
实例上调用 Call
方法或 FavorFood
方法时,调用的就是 Dog
类中定义的方法而不是 Animal
中定义的方法,如果要指定调用Animal
里面的函数,就要按照下面的格式:
dog.Animal.Call()
拓展
可以看到,与传统面向对象编程语言的继承机制不同,这种组合的实现方式更加灵活,我们不用考虑单继承还是多继承,你想要继承哪个类型的方法,直接组合进来就好了。接下来我们来介绍一下继承与多态中常出现的一些问题:
-
多继承同名方法冲突处理
如果组合中不同类型中包含同名的方法,比如下面这种情况:
type Dog struct{ Animal pet }
如果Animal和pet中有同名方法且类
Dog
没有重写该方法,直接在Dog
实例上调用的话会报错,除非我们指定了执行哪个父类的函数 -
调整组合位置会改变内存布局
另外,我们还可以通过任意调整被组合类型的位置来改变类的内存布局:
type Dog struct { Animal Pet }
和
type Dog struct { Pet Animal }
虽然上面两个
Dog
子类的功能一致,但是它们的内存结构不同。 -
为组合类型设置别名
前面的示例调用父类方法时都直接引用的是组合类型(父类)的类型字面量,其实,我们还可以像基本类型一样,为其设置别名,方便引用:
type Dog struct{ pet *Pet animal *Animal }
类属性和成员方法可见性设置
在go语言中,无论是变量,函数还是类属性和成员方法,它们的可见性都是以包为维度的,go没有像public
,private
,protected
这样的关键字来修饰其可见性。它们的可见性都是根据其首字母的大小写来决定的,如果变量名、属性名、函数名或方法名首字母大写,就可以在包外直接访问这些变量、属性、函数和方法,否则只能在包内访问,因此 Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的。
接下来我们来演示一个例子:
首先我们来创建一个animal
包,然后创建一个animal.go
文件:
package animal
type Animal struct {
Name string
}
func (a Animal) Call() string {
return "动物的叫声..."
}
func (a Animal) FavorFood() string {
return "爱吃的食物..."
}
func (a Animal) GetName() string {
return a.Name
}
然后再创建一个pet.go
:
package animal
type Pet struct {
Name string
}
func (p Pet) GetName() string {
return p.Name
}
然后创建dog.go
package animal
type Dog struct {
Animal *Animal
Pet Pet
}
func (d Dog) FavorFood() string {
return "骨头"
}
func (d Dog) Call() string {
return "汪汪汪"
}
最后是main.go
文件
package main
import (
"fmt"
. "animal"
)
func main() {
animal := Animal{Name: "中华田园犬"}
pet := Pet{Name: "宠物狗"}
dog := Dog{Animal: &animal, Pet: pet}
fmt.Println(dog.Animal.GetName())
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
}
这样我们就实现了一个简单的面向对象程序,但是这里文件的变量以及方法名字都是大写,类似于全部都是public
(公有的)如果你觉得直接暴露这三个类的所有属性可以被任意修改,不够安全,还可以通过定义构造函数来封装它们的初始化过程,然后把属性名首字母小写进行私有化:
以animal.go
为例
package animal
type Animal struct {
name string
}
func NewAnimal(name string) Animal {
return Animal{name: name}
}
func (a Animal) Call() string {
return "动物的叫声..."
}
func (a Animal) FavorFood() string {
return "爱吃的食物..."
}
func (a Animal) GetName() string {
return a.name
}
此时运行程序就会:
总结:
上面我们介绍了go语言的类型系统,并且完成了使用go语言来实现一个简单面向对象封装,继承与多态,大家可以多西靠思考,理解一下go语言的面向对象与常见如c++,Java等语言再面向对象实现上的不同,后面博主将介绍有关于接口在面向对象中的使用,以及有关泛型的使用以及基于泛型来实现我们自己封装的简短的数据结构,大家下篇见!,最后的最后,大家如果喜欢,还请收藏加关注,这样才能不迷路哦!!!