Go 面向对象编程篇(四):类属性和成员方法的可见性
一、类属性和成员方法可见性概述
在前面几篇教程中,学院君已经陆续给大家介绍了 Go 语言面向对象编程的基本实现,包括类的定义、构造函数、成员方法、类的继承、方法重写等,今天我们接着来介绍下类属性和成员方法的可见性。
如果你之前有过 Java、PHP 等语言面向对象编程的经验,对可见性这一术语肯定不陌生,所谓可见性,其实是一种访问控制策略,用于表示对应属性和方法是否可以在类以外的地方显式调用,Java 和 PHP 都提供了三个关键字来修饰属性和方法的可见性,分别是 private
、protected
和 public
,分别表示只能在类的内部可见、在子类中可见(对 Java 而言在同一包内亦可见)、以及完全对外公开。
Go 语言不是典型的面向对象编程语言,并且语言本身的设计哲学也非常简单,惜字(关键字)如金,没有提供上面这三个关键字,也没有提供以类为维度管理属性和方法可见性的机制,但是 Go 语言确实有可见性的概念,只不过这个可见性是基于包这个维度的。
二、Go 语言的包管理和基本特性
因此,在定义 Go 语言的类属性和成员方法可见性之前,我们先来大致了解下 Go 语言的包。
PHP 程序员可能对包这个概念有点陌生,你可以把它类比为遵循 PSR4 风格的代码中命名空间的概念进行理解,包是程序代码的逻辑概念,我们通常把处理同一类型业务的代码放到同一个包中,包落到物理实体就是存放源代码的文件系统目录,因此我们可以把归属于同一个目录的文件看作归属于同一个包,这与命名空间有异曲同工之效。
Go 语言基于包为单位组织和管理源码,因此变量、类属性、函数、成员方法的可见性都是基于包这个维度的。包与文件系统的目录结构存在映射关系(和命名空间一样):
在引入 Go Modules 以前,Go 语言会基于
GOPATH
这个系统环境变量配置的路径为根目录(可能有多个),然后依次去对应路径下的src
目录下根据包名查找对应的文件目录,如果目录存在,则再到该目录下的源文件中查找对应的变量、类属性、函数和成员方法;在启用 Go Modules 之后,不再依赖
$GOPATH
定位包,而是基于go.mod
中module
配置值作为根路径,在该模块路径下,根据包名查找对应目录,如果存在,则继续到该目录下的源文件中查找对应变量、类属性、函数和成员方法。
在 Go 语言中,你可以通过 import
关键字导入官方提供的包、第三方包、以及自定义的包,导入第三方包时,还需要通过 go get
指令下载才能使用,如果基于 Go Modules 管理项目的话,这个依赖关系会自动维护到 go.mod
中。
归属同一个包的 Go 代码具备以下特性:
归属于同一个包的源文件包声明语句要一致,即同一级目录的源文件必须属于同一个包;
在同一个包下不同的源文件中不能重复声明同一个变量、函数和类(结构体);
另外,需要注意的是 main
函数作为程序的入口函数,只能存在于 main
包中。
三、Go 语言的类属性和成员方法可见性设置
在 Go 语言中,无论是变量、函数还是类属性和成员方法,它们的可见性都是以包为维度的,而不是类似传统面向编程那样,类属性和成员方法的可见性封装在所属的类中,然后通过 private
、protected
和 public
这些关键字来修饰其可见性。
Go 语言没有提供这些关键字,不管是变量、函数,还是自定义类的属性和成员方法,它们的可见性都是根据其首字母的大小写来决定的,如果变量名、属性名、函数名或方法名首字母大写,就可以在包外直接访问这些变量、属性、函数和方法,否则只能在包内访问,因此 Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的。
下面我们根据上面介绍的包特性及可见性将上篇教程编写的 Animal
、Pet
、Dog
类放到同一级目录下的 animal
包中,然后在 03-compose.go
文件中调用这两个类。
首先,我们在当前目录下创建一个 animal
子目录,然后在这个子目录下创建源文件 animal.go
用于存放 Animal
类代码:
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
用于保存 Pet
类源码:
package animal
type Pet struct {
Name string
}
func (p Pet) GetName() string {
return p.Name
}
接下来,我们在 animal
目录下新建 dog.go
用于存放继承了 Animal
和 Pet
类的 Dog
类源码:
package animal
type Dog struct {
Animal *Animal
Pet Pet
}
func (d Dog) FavorFood() string {
return "骨头"
}
func (d Dog) Call() string {
return "汪汪汪"
}
这里,由于 Dog
类需要在 animal
包以外的地方进行初始化,所以需要将其属性名首字母都都替换成大写字母。
最后,我们 03-compose.go
文件中导入 animal
包,然后调用该包下的 Animal
、Pet
、Dog
类如下:
package main
import (
"fmt"
. "go-tutorial/chapter04/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())
}
这里,注意到我们在通过 import
导入 animal
包时,使用了 .
作为前缀,表示在接下来调用该包中的变量、函数、类属性和成员方法时,无需使用包名前缀 animal.
引用,以免和 main
函数中的 animal
变量名冲突。
对应源码和包的目录结构如下所示:
执行 03-compose.go
:
没有报错,表明代码重构成功。
四、通过私有化属性提升代码的安全性
如果你觉得直接暴露这三个类的所有属性可以被任意修改,不够安全,还可以通过定义构造函数来封装它们的初始化过程,然后把属性名首字母小写进行私有化:
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
}
pet.go
package animal
type Pet struct {
name string
}
func NewPet(name string) Pet {
return Pet{name: name}
}
func (p Pet) GetName() string {
return p.name
}
dog.go
package animal
type Dog struct {
animal *Animal
pet Pet
}
func NewDog(animal *Animal, pet Pet) Dog {
return Dog{animal: animal, pet: pet}
}
func (d Dog) FavorFood() string {
return d.animal.FavorFood() + "骨头"
}
func (d Dog) Call() string {
return d.animal.Call() + "汪汪汪"
}
func (d Dog) GetName() string {
return d.pet.GetName()
}
func (d Dog) GetName() string {
return d.pet.GetName()
}
这样一来,在 03-compose.go
中,就可以看到原来的调用代码都报错了:
因为这些属性名首字母都变成小写了,对应属性变成私有的了,只能在 animal
包内可见。同理,如果 GetName
、Call
或者 FavorFood
任意一个方法首字母小写,那么这里调用也会报错,提示找不到该成员方法。
要完成这些类的初始化,现在需要调用它们的构造函数来实现:
package main
import (
"fmt"
. "go-tutorial/chapter04/animal"
)
func main() {
animal := NewAnimal("中华田园犬")
pet := NewPet("宠物狗")
dog := NewDog(&animal, pet)
fmt.Println(dog.GetName())
fmt.Println(dog.Call())
fmt.Println(dog.FavorFood())
}
执行上述代码,打印结果如下:
好了,关于类属性和成员方法的可见性,学院君就简单介绍到这里,非常简单,下篇教程,我们来探讨 Go 语言的接口实现、反射和泛型。
(本文完)
学习过程中有任何问题,可以通过下面的评论功能或加入「Go 语言研习社」与学院君讨论:
本系列教程首发在 geekr.dev,你可以点击页面左下角阅读原文链接查看最新更新的教程。