类属性和成员方法可见性概述

    在前面几篇教程中,清华尹成大神已经陆续给大家介绍了 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()

    }

    这样一来,在 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 语言的接口实现、反射和泛型。