go貌似都没有听说过继承,当然这个继承不像c++中通过class类的方式去继承,还是通过struct的方式,所以go严格来说不是面向对象编程的语言,c++和java才是,不过还是可以基于自身的一些的特性实现面向对象的功能,面向对象三大特性:封装、继承、多态
封装
属性
go是接住结构体struct
实现类的声明,比如要定义个学生类,学生类和animal类是面向对象示例介绍中重点,可以通过如下方式实现:
type Student struct {
id uint
name string
male bool
score float64
}
结构体名或者说类名为 Student
,并且包含了 id
、name
、male
、score
四个属性,Go 语言中也不支持构造函数、析构函数,取而代之地,可以通过定义形如 NewXXX
这样的全局函数(首字母大写)作为类的初始化函数
func NewStudent(id uint, name string, male bool, score float64) *Student {
return &Student{id, name, male, score}
}
在这个函数中,我们通过传入的属性字段对 Student 类进行初始化并返回一个指向该类的指针
在 Go 语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如 bool 类型的零值为 false,int 类型的零值为 0,string 类型的零值为空字符串,float 类型的零值为 0.0。接着就可以调用了
package main
import (
"fmt"
student2 "student/stu"
)
func main() {
student := student2.NewStudent(1, "zhiyu",false, 100)
fmt.Println(student)
}
成员函数
getXXX方法
要为 Go 类定义成员方法,需要在 func 和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明),以 Student 类为例,要为其定义获取 name 值的方法,可以这么做:
func (s Student) GetName() string {
return s.name
}
这样就可以使用GetName
方法获取name
属性,通过在函数签名中增加接收者声明的方式定义了函数所归属的类型,这个时候,函数就不再是普通的函数,而是类的成员方法了
setXXX方法
getName
是一个只读方法,setName
是一个可写的方法,所以需要使用指针的方式进行传参,在类的成员方法中,可以通过声明的类型变量来访问类的属性和其他方法
func (s *Student) SetName(name string) {
s.name = name
}
可以把接收者类型为指针的成员方法叫做指针方法
,把接收者类型为非指针的成员方法叫做值方法
,二者的区别在于值方法传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部传入结构体实例的值拷贝,修改不会作用到外部传入的结构体实例.
另外,需要声明的是,在 Go 语言中,当我们将成员方法 SetName
所属的类型声明为指针类型时,严格来说,该方法并不属于 Student
类,而是属于指向 Student
的指针类型,所以,归属于 Student
的成员方法只是 Student
类型下所有可用成员方法的子集,归属于 *Student
的成员方法才是 Student
类完整可用方法的集合。
在调用值方法和指针方法时,需要记住以下两条准则:
- 值方法可以通过指针和值类型实例调用,指针类型实例调用值方法时会自动解引用;
- 指针方法只能通过指针类型实例调用,但有一个例外,如果某个值是可寻址的(或者说左值),那么编译器会在值类型实例调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用
左值和右值
结合上面的两条准则使用示例说明如下:
package main
import "fmt"
type Student struct {
id uint
name string
score float64
}
func NewStudent(id uint, name string, score float64) *Student {
return &Student{id: id, name: name, score: score}
}
func NewStudentV2(id uint, name string, score float64) Student {
return Student{id: id, name: name, score: score}
}
func (s Student) GetName() string {
return s.name
}
func (s *Student) SetName(name string) {
s.name = name
}
func main() {
s := NewStudent(1, "zhiyu", 100)
s.SetName("zhiyu-1") // ok 正常调用指针方法
fmt.Println(s.GetName()) // ok 指针调用值方法自动解引用: (*s).GetName()
s2 := NewStudentV2(2, "zhiyu", 90)
s2.SetName("zhiyu-2") // ok s2 是可寻址的左值,所以实际调用: (&s2).SetName("zhiyu-2")
fmt.Println(s2.GetName()) // ok 正常调用值方法
NewStudent(3, "zhiyu", 80).SetName("zhiyu-3") // ok 正常调用指针方法
NewStudentV2(4, "zhiyu", 99).SetName("zhiyu-4") // err 值类型调用指针方法
}
之所以可以直接在 s2
值实例上调用 SetName
指针方法,是因为 s2
是可寻址的,Go 语言底层会自动将 s2
转化为对应的指针类型 &s2
,所以真正调用的代码是 (&s2).SetName("zhiyu-2")
,而通过 NewStudentV2
(…)返回实例调用 SetName
时,则会报错,因为 NewStudentV2
(…) 是一个不可以寻址的右值。
所谓左值就是可以出现在赋值等号左边的值,而右值只能出现在赋值等号右边,比如函数返回值、字面量、常量值等。左值可寻址,右值不可寻址。
总结下来,就是一个自定义数据类型的方法集合中仅会包含它的所有「值方法」,而该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」,指针方法可以修改所属类型的属性值,而值方法则不能。
值方法|指针方法
有如下情形的考量时,需要将类方法定义为指针方法:
- 数据一致性:方法需要修改传入的类型实例本身;
- 方法执行效率:如果是值方法,在方法调用时一定会产生值拷贝,而大对象拷贝代价很大。
通常我们都会选择定义指针方法
继承
Go
虽然没有直接提供继承相关的语法实现,但是通过组合的方式间接实现类似功能,所谓组合,就是将一个类型嵌入到另一个类型,从而构建新的类型结构。使用了Student
类介绍封装,接着使用Animal
示例继承
package ani
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
}
所有的动物都有以上的三种方法,现在Dog
实现继承Animal
,新增一个dog.go,一般会给自家的爱狗起一个别名
package ani
type Dog struct {
Animal
Alias string
}
func (d Dog) GetAliasName() string {
return d.Alias
}
main.go
中如下所示
package main
import (
"animal/ani"
"fmt"
)
func main() {
animal := ani.Animal{"旺财"}
dog := ani.Dog{animal, "旺旺"}
fmt.Println(dog.GetName())
fmt.Println(dog.Call())
fmt.Println(dog.GetAliasName())
}
这就实现了继承的功能。注意在初始化子类时的顺序和struct中的定义顺序是一致的
多态
所谓多态就是一个函数的多种形态,比如在子类中定义同名函数来覆盖父类方法,专业属于称之为方法重写
。每个动物的喜欢吃的食物和叫声不同,这里实现下Dog
重写方法,在dog.go
中
package ani
type Dog struct {
Animal
Alias string
}
func (d Dog) GetAliasName() string {
return d.Alias
}
func (d Dog) FavorFood() string {
return "大棒骨"
}
func (d Dog) Call() string {
return "旺旺"
}
重写go build
,并在terminal中在animal
根目录下
当然子类也可以直接调用父类Animal
中的方法
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())