结构体
Go 语言的面向对象编程与我们之前所熟悉的 PHP、Java 那一套完全不同,没有 class
、extends
、implements
之类的关键字和相应的概念,而是借助结构体来实现类的声明。
type Person struct {
name string //名字
male bool //性别
}
可以理解为类名 Person
,并且包含了 name
、male
两个属性。
可以通过定义形如 NewXXX
这样的全局函数(首字母大写)作为结构体的初始化函数:
func NewPerson(name string, male bool) *Persion {
return &Person{name, male}
}
person := NewPerson("结构体", false)
fmt.Println(person)
成员方法
值方法
要为 Go 结构体定义成员方法,需要在 func
和方法名之间声明方法所属的结构体(有的地方将其称之为接收者声明),以 Person 结构体为例,要为其定义获取
name
值的方法,可以这么做:
func (s Person) GetName() string {
return s.name
}
可以在初始化 Person
结构体后,通过 GetName()
方法获取 name
值:
person := NewPerson("结构体", false)
fmt.Println("Name:", person.GetName())
通过在函数中增加接收者声明的方式定义了函数所归属的类型,这个时候,函数就不再是普通的函数,而是类的成员方法了。
指针方法
如果需要在函数内部修改成员变量的值,并且该修改要作用到该函数作用域以外,那么就需要传入指针类型(结构体是值类型,不是引用类型,所以需要显式传入指针)。在 GetName
方法中,由于不需要对类的成员变量进行修改,所以不需要传入指针。
func (s *P) SetName(name string) {
s.name = name
}
初始化 Person
结构体之后,通过 SetName
方法修改 name
值,然后再通过 GetName
将其打印出来:
person := NewPerson("结构体", false)
person.SetName("结构体测试")
fmt.Println("Name:", person.GetName())
接收者类型为指针的成员方法叫做指针方法,接收者类型为非指针的成员方法叫做值方法。
值方法和指针方法的区别
在 Go 语言中,当我们将成员方法 SetName
所属的类型声明为指针类型时,严格来说,该方法并不属于 Person
结构体,而是属于指向 Person
的指针类型。
当我们有如下情形的考量时,需要将成员方法定义为指针方法:
- 数据一致性:方法需要修改传入的类型实例本身;
- 方法执行效率:如果是值方法,在方法调用时一定会产生值拷贝,而大对象拷贝代价很大。
通过组合实现结构体的继承和方法重写
继承
Go 没有直接提供继承相关的语法实现,但是我们通过组合的方式间接实现类似功能,所谓组合,就是将一个结构体型嵌入到另一个结构体,从而构建新的结构体。
现在有一个 Animal
结构体类型,它有一个属性 Name
用于表示该动物的名称,以及三个成员方法。
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
}
定义一个继承自该类型的子结构体 Cat
type Cat struct {
Animal
}
在 Cat
结构体类型中,嵌入了 Animal
,可以在 Cat
实例上访问所有 Animal
类型包含的属性和方法:
func main() {
animal := Animal{"布偶猫"}
cat := Cat{animal}
fmt.Println(cat.GetName())
fmt.Println(cat.Call())
fmt.Println(cat.FavorFood())
}
通过组合实现了结构体与结构体之间的继承功能。
方法重写
通过在子结构体中定义同名方法来覆盖父类方法的实现,在面向对象编程中这一术语叫做方法重写,比如 Cat
结构体中,可以重写 Call
方法和 FavorFood
方法的实现如下:
func (c Cat) FavorFood() string {
return "鱼和老鼠"
}
func (c Cat) Call() string {
return "喵喵喵"
}
animal := Animal{"布偶猫"}
cat := Cat{animal}
fmt.Print(cat.Animal.Call())
fmt.Println(cat.Call())
fmt.Print(cat.Animal.FavorFood())
fmt.Println(cat.FavorFood())
组合的实现方式更加灵活,不用考虑单继承还是多继承,想要继承哪个类型的方法,直接组合进来就可以了。
继承指针类型的属性和方法
在 Go 语言中,还可以通过指针方式继承某个类型的属性和方法:
type Cat struct {
*Animal
}
在调用时,传入 Animal
实例的指针引用就可以了
func main() {
animal := Animal{"布偶猫"}
cat := Cat{&animal}
fmt.Println(cat.Animal.GetName())
fmt.Print(cat.Animal.Call())
fmt.Println(cat.Call())
fmt.Print(cat.Animal.FavorFood())
fmt.Println(cat.FavorFood())
}
当我们通过组合实现结构体之间的继承时,由于结构体实例本身是值类型,如果传入值字面量的话,实际上传入的是结构体实例的副本,对内存耗费更大,所以组合指针类型性能更好。
为组合类型设置别名
type Cat struct {
animal *Animal
}
...
func main() {
animal := Animal{"布偶猫"}
cat := Cat{&animal}
// 通过 animal 引用 Animal 类型实例
fmt.Println(cat.animal.GetName())
fmt.Print(cat.animal.Call())
fmt.Println(cat.Call())
fmt.Print(cat.animal.FavorFood())
fmt.Println(cat.FavorFood())
}
结构体属性和方法的可见性
在 Go 语言中,无论是变量、函数还是结构体属性和成员方法,可见性都是以包为维度的。Go 语言没有提供private
、protected
和 public
关键字。可见性都是根据其首字母的大小写来决定的,如果变量名、属性名、函数名或方法名首字母大写,就可以在包外直接访问,否则只能在包内访问。