Skip to content
On this page

类属性和成员方法的可见性

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

在前面几篇教程中,已经陆续给大家介绍了 Go 语言面向对象编程的基本实现,包括类的定义、构造函数、成员方法、类的继承、方法重写等,今天我们接着来介绍下类属性和成员方法的可见性。

如果你之前有过 Java、PHP 等语言面向对象编程的经验,对可见性这一术语肯定不陌生,所谓可见性,其实是一种访问控制策略,用于表示对应属性和方法是否可以在类以外的地方显式调用,Java 和 PHP 都提供了三个关键字来修饰属性和方法的可见性,分别是 privateprotectedpublic,分别表示只能在类的内部可见、在子类中可见(对 Java 而言在同一包内亦可见)、以及完全对外公开。

Go 语言不是典型的面向对象编程语言,并且语言本身的设计哲学也非常简单,惜字(关键字)如金,没有提供上面这三个关键字,也没有提供以类为维度管理属性和方法可见性的机制,但是 Go 语言确实有可见性的概念,只不过这个可见性是基于包这个维度的。

Go 语言的包管理和基本特性

因此,在定义 Go 语言的类属性和成员方法可见性之前,我们先来大致了解下 Go 语言的包。

PHP 程序员可能对包这个概念有点陌生,你可以把它类比为遵循 PSR4 风格的代码中命名空间的概念进行理解,包是程序代码的逻辑概念,我们通常把处理同一类型业务的代码放到同一个包中,包落到物理实体就是存放源代码的文件系统目录,因此我们可以把归属于同一个目录的文件看作归属于同一个包,这与命名空间有异曲同工之效。

Go 语言基于包为单位组织和管理源码,因此变量、类属性、函数、成员方法的可见性都是基于包这个维度的。包与文件系统的目录结构存在映射关系(和命名空间一样):

  • 在引入 Go Modules 以前,Go 语言会基于 GOPATH 这个系统环境变量配置的路径为根目录(可能有多个),然后依次去对应路径下的 src 目录下根据包名查找对应的文件目录,如果目录存在,则再到该目录下的源文件中查找对应的变量、类属性、函数和成员方法;
  • 在启用 Go Modules 之后,不再依赖 $GOPATH 定位包,而是基于 go.modmodule 配置值作为根路径,在该模块路径下,根据包名查找对应目录,如果存在,则继续到该目录下的源文件中查找对应变量、类属性、函数和成员方法。

在 Go 语言中,你可以通过 import 关键字导入官方提供的包、第三方包、以及自定义的包,导入第三方包时,还需要通过 go get 指令下载才能使用,如果基于 Go Modules 管理项目的话,这个依赖关系会自动维护到 go.mod 中。

归属同一个包的 Go 代码具备以下特性:

  • 归属于同一个包的源文件包声明语句要一致,即同一级目录的源文件必须属于同一个包;
  • 在同一个包下不同的源文件中不能重复声明同一个变量、函数和类(结构体);

另外,需要注意的是 main 函数作为程序的入口函数,只能存在于 main 包中。

Go 语言的类属性和成员方法可见性设置

在 Go 语言中,无论是变量、函数还是类属性和成员方法,它们的可见性都是以包为维度的,而不是类似传统面向编程那样,类属性和成员方法的可见性封装在所属的类中,然后通过 privateprotectedpublic 这些关键字来修饰其可见性。

Go 语言没有提供这些关键字,不管是变量、函数,还是自定义类的属性和成员方法,它们的可见性都是根据其首字母的大小写来决定的,如果变量名、属性名、函数名或方法名首字母大写,就可以在包外直接访问这些变量、属性、函数和方法,否则只能在包内访问,因此 Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的。

下面我们根据上面介绍的包特性及可见性将上篇教程编写的 AnimalPetDog 类放到同一级目录下的 animal 包中,然后在 03-compose.go 文件中调用这两个类。

首先,我们在当前目录下创建一个 animal 子目录,然后在这个子目录下创建源文件 animal.go 用于存放 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 用于保存 Pet 类源码:

Go
package animal

type Pet struct {
    Name string
}

func (p Pet) GetName() string  {
    return p.Name
}

接下来,我们在 animal 目录下新建 dog.go 用于存放继承了 AnimalPet 类的 Dog 类源码:

Go
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 包,然后调用该包下的 AnimalPetDog 类如下:

Go
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

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

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

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 包内可见。同理,如果 GetNameCall 或者 FavorFood 任意一个方法首字母小写,那么这里调用也会报错,提示找不到该成员方法。

要完成这些类的初始化,现在需要调用它们的构造函数来实现:

Go
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 语言的接口实现、反射和泛型。

Released under the MIT License.