Go 面向?qū)ο缶幊唐ㄋ模侯悓傩院统蓡T方法的可見性

一、類屬性和成員方法可見性概述
在前面幾篇教程中,學(xué)院君已經(jīng)陸續(xù)給大家介紹了 Go 語言面向?qū)ο缶幊痰幕緦崿F(xiàn),包括類的定義、構(gòu)造函數(shù)、成員方法、類的繼承、方法重寫等,今天我們接著來介紹下類屬性和成員方法的可見性。
如果你之前有過 Java、PHP 等語言面向?qū)ο缶幊痰慕?jīng)驗,對可見性這一術(shù)語肯定不陌生,所謂可見性,其實是一種訪問控制策略,用于表示對應(yīng)屬性和方法是否可以在類以外的地方顯式調(diào)用,Java 和 PHP 都提供了三個關(guān)鍵字來修飾屬性和方法的可見性,分別是 private、protected 和 public,分別表示只能在類的內(nèi)部可見、在子類中可見(對 Java 而言在同一包內(nèi)亦可見)、以及完全對外公開。
Go 語言不是典型的面向?qū)ο缶幊陶Z言,并且語言本身的設(shè)計哲學(xué)也非常簡單,惜字(關(guān)鍵字)如金,沒有提供上面這三個關(guān)鍵字,也沒有提供以類為維度管理屬性和方法可見性的機制,但是 Go 語言確實有可見性的概念,只不過這個可見性是基于包這個維度的。
二、Go 語言的包管理和基本特性
因此,在定義 Go 語言的類屬性和成員方法可見性之前,我們先來大致了解下 Go 語言的包。
PHP 程序員可能對包這個概念有點陌生,你可以把它類比為遵循 PSR4 風(fēng)格的代碼中命名空間的概念進行理解,包是程序代碼的邏輯概念,我們通常把處理同一類型業(yè)務(wù)的代碼放到同一個包中,包落到物理實體就是存放源代碼的文件系統(tǒng)目錄,因此我們可以把歸屬于同一個目錄的文件看作歸屬于同一個包,這與命名空間有異曲同工之效。
Go 語言基于包為單位組織和管理源碼,因此變量、類屬性、函數(shù)、成員方法的可見性都是基于包這個維度的。包與文件系統(tǒng)的目錄結(jié)構(gòu)存在映射關(guān)系(和命名空間一樣):
在引入 Go Modules 以前,Go 語言會基于
GOPATH這個系統(tǒng)環(huán)境變量配置的路徑為根目錄(可能有多個),然后依次去對應(yīng)路徑下的src目錄下根據(jù)包名查找對應(yīng)的文件目錄,如果目錄存在,則再到該目錄下的源文件中查找對應(yīng)的變量、類屬性、函數(shù)和成員方法;在啟用 Go Modules 之后,不再依賴
$GOPATH定位包,而是基于go.mod中module配置值作為根路徑,在該模塊路徑下,根據(jù)包名查找對應(yīng)目錄,如果存在,則繼續(xù)到該目錄下的源文件中查找對應(yīng)變量、類屬性、函數(shù)和成員方法。
在 Go 語言中,你可以通過 import 關(guān)鍵字導(dǎo)入官方提供的包、第三方包、以及自定義的包,導(dǎo)入第三方包時,還需要通過 go get 指令下載才能使用,如果基于 Go Modules 管理項目的話,這個依賴關(guān)系會自動維護到 go.mod 中。
歸屬同一個包的 Go 代碼具備以下特性:
歸屬于同一個包的源文件包聲明語句要一致,即同一級目錄的源文件必須屬于同一個包;
在同一個包下不同的源文件中不能重復(fù)聲明同一個變量、函數(shù)和類(結(jié)構(gòu)體);
另外,需要注意的是 main 函數(shù)作為程序的入口函數(shù),只能存在于 main 包中。
三、Go 語言的類屬性和成員方法可見性設(shè)置
在 Go 語言中,無論是變量、函數(shù)還是類屬性和成員方法,它們的可見性都是以包為維度的,而不是類似傳統(tǒng)面向編程那樣,類屬性和成員方法的可見性封裝在所屬的類中,然后通過 private、protected 和 public 這些關(guān)鍵字來修飾其可見性。
Go 語言沒有提供這些關(guān)鍵字,不管是變量、函數(shù),還是自定義類的屬性和成員方法,它們的可見性都是根據(jù)其首字母的大小寫來決定的,如果變量名、屬性名、函數(shù)名或方法名首字母大寫,就可以在包外直接訪問這些變量、屬性、函數(shù)和方法,否則只能在包內(nèi)訪問,因此 Go 語言類屬性和成員方法的可見性都是包一級的,而不是類一級的。
下面我們根據(jù)上面介紹的包特性及可見性將上篇教程編寫的 Animal、Pet、Dog 類放到同一級目錄下的 animal 包中,然后在 03-compose.go 文件中調(diào)用這兩個類。
首先,我們在當(dāng)前目錄下創(chuàng)建一個 animal 子目錄,然后在這個子目錄下創(chuàng)建源文件 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
}
然后,我們在同一級目錄下創(chuàng)建 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 文件中導(dǎo)入 animal 包,然后調(diào)用該包下的 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 導(dǎo)入 animal 包時,使用了 . 作為前綴,表示在接下來調(diào)用該包中的變量、函數(shù)、類屬性和成員方法時,無需使用包名前綴 animal. 引用,以免和 main 函數(shù)中的 animal 變量名沖突。
對應(yīng)源碼和包的目錄結(jié)構(gòu)如下所示:

執(zhí)行 03-compose.go:

沒有報錯,表明代碼重構(gòu)成功。
四、通過私有化屬性提升代碼的安全性
如果你覺得直接暴露這三個類的所有屬性可以被任意修改,不夠安全,還可以通過定義構(gòu)造函數(shù)來封裝它們的初始化過程,然后把屬性名首字母小寫進行私有化:
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 中,就可以看到原來的調(diào)用代碼都報錯了:

因為這些屬性名首字母都變成小寫了,對應(yīng)屬性變成私有的了,只能在 animal 包內(nèi)可見。同理,如果 GetName、Call 或者 FavorFood 任意一個方法首字母小寫,那么這里調(diào)用也會報錯,提示找不到該成員方法。
要完成這些類的初始化,現(xiàn)在需要調(diào)用它們的構(gòu)造函數(shù)來實現(xiàn):
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())
}
執(zhí)行上述代碼,打印結(jié)果如下:

好了,關(guān)于類屬性和成員方法的可見性,學(xué)院君就簡單介紹到這里,非常簡單,下篇教程,我們來探討 Go 語言的接口實現(xiàn)、反射和泛型。
(本文完)
學(xué)習(xí)過程中有任何問題,可以通過下面的評論功能或加入「Go 語言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點擊頁面左下角閱讀原文鏈接查看最新更新的教程。
