类属性和方法的可见性

前面我们已经陆续介绍了 Go 语言中面向对象的基本特性,包括自定义类的实现、构造函数、成员方法、类的继承、方法重写等,今天我们来系统介绍下类的属性和成员方法的可见性。

在 Go 语言中,没有类似 PHP 和 Java 那种命名空间的概念,不过 Go 语言也是通过包来管理源代码的,包往往与文件系统的目录结构存在映射关系,Go 语言在寻找变量、函数、类属性及方法的时候,会先查看 GOPATH 这个系统环境变量,然后根据该变量配置的路径列表依次去对应路径下的 src 目录下根据包名查找对应的目录,如果对应目录存在,则再到该目录下查找对应的变量、函数、类属性和方法,因此我们可以把归属于同一个目录的文件看作归属于同一个包,归属同一个包的代码具备以下特性:

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

另外,需要注意的是 main 函数作为程序的入口函数,只能存在于 main 包中,main 包通常对应 src 目录,但也可以将其它子目录声明为 main 包。

Go 语言中,无论是变量、函数还是类属性及方法,它们的可见性都是与包相关联的,而不是类似传统面向编程那样,类属性和方法的可见性封装在对应的类中,然后通过 privateprotectedpublic 这些关键字来描述其可见性,Go 语言没有这些关键字,和变量和函数一样,对应 Go 语言的自定义类来说,属性和方法的可见性根据其首字母大小写来决定,如果属性名或方法名首字母大写,则可以在其他包中直接访问这些属性和方法,否则只能在包内访问,所以 Go 语言中的可见性都是包一级的,而不是类一级的。

下面我们根据上面介绍的包特性及可见性将前面编写的 AnimalDog 类放到 src 目录下的 animal 包中,然后在 src 目录下的 oop.go 文件中调用这两个类。

首先我们在 src 目录下创建一个 animal 子目录,然后在这个子目录下创建源文件 animal.go 用于保存 Animal 类代码:

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
}

我们在这个类中新增了 NewAnimal 方法作为 Animal 的构造函数,由于构造函数肯定需要在包以外的地方调用,所以将其首字母大写了,Animal 类包含了 name 属性,首字母小写表示只在 animal 包内可见,然后我们在同一个目录下创建 dog.go 用于保存 继承了 Animal 类的 Dog 类源码:

package animal

type Dog struct {
    *Animal
}

func (d Dog) FavorFood() string {
    return "骨头"
}

func (d Dog) Call() string {
    return "汪汪汪"
}

Dog 类中,我们定义了一个 *Animal 类型的匿名属性,表示它继承了 *Animal 的所有成员方法,然后我们在 Dog 类中扩展了父类的 FavorFoodCall 成员方法。

最后,我们在 src 目录下创建一个 oop.go 文件来编写调用代码:

package main

import (
    "animal"
    "fmt"
)

func main()  {
    ani := animal.NewAnimal("狗")
    dog := animal.Dog{ani}
    fmt.Println(dog.GetName(), dog.Call(), dog.FavorFood())
}

由于 AnimalDog 类都是定义在 animal 包中,所以需要通过 import 引入 animal 包,然后经过初始化后,就可以调用 dog 实例上的可见属性和方法了,在这里我们是无法调用 Animal 类的 name 属性的,因为该属性只在 animal 包内可见。同样,如果 GetNameCall 或者 FavorFood 任意一个方法首字母小写,那么这里调用也会报错,提示找不到该属性字段或方法。

此外,我们还可以对引入的包设置别名,这在引入包的路径很长或者最后一级目录同名时适用,比如我们可以把上述 animal 包设置别名 animal2

package main

import (
    animal2 "animal"
    "fmt"
)

func main()  {
    ani := animal2.NewAnimal("狗")
    dog := animal2.Dog{ani}
    fmt.Println(dog.GetName(), dog.Call(), dog.FavorFood())
}

上一篇: 通过组合实现类的继承和方法重写

下一篇: 接口篇(一):接口定义与实现