Go 經典入門系列 26:結構體取代類?
歡迎來到 Golang 系列教程[1]的第 26 篇。
Go 支持面向對象嗎?
Go 并不是完全面向對象的編程語言。Go 官網的 FAQ[2] 回答了 Go 是否是面向對象語言,摘錄如下。
可以說是,也可以說不是。雖然 Go 有類型和方法,支持面向對象的編程風格,但卻沒有類型的層次結構。Go 中的“接口”概念提供了一種不同的方法,我們認為它易于使用,也更為普遍。Go 也可以將結構體嵌套使用,這與子類化(Subclassing)類似,但并不完全相同。此外,Go 提供的特性比 C++ 或 Java 更為通用:子類可以由任何類型的數(shù)據來定義,甚至是內建類型(如簡單的“未裝箱的”整型)。這在結構體(類)中沒有受到限制。
在接下來的教程里,我們會討論如何使用 Go 來實現(xiàn)面向對象編程概念。與其它面向對象語言(如 Java)相比,Go 有很多完全不同的特性。
使用結構體,而非類
Go 不支持類,而是提供了結構體。結構體中可以添加方法。這樣可以將數(shù)據和操作數(shù)據的方法綁定在一起,實現(xiàn)與類相似的效果。
為了加深理解,我們來編寫一個示例吧。
在示例中,我們創(chuàng)建一個自定義包,它幫助我們更好地理解,結構體是如何有效地取代類的。
在你的 Go 工作區(qū)創(chuàng)建一個名為 oop 的文件夾。在 opp 中再創(chuàng)建子文件夾 employee。在 employee 內,創(chuàng)建一個名為 employee.go 的文件。
文件夾結構會是這樣:
workspacepath?->?oop?->?employee?->?employee.go
請將 employee.go 里的內容替換為如下所示的代碼。
package?employee
import?(
?"fmt"
)
type?Employee?struct?{
?FirstName???string
?LastName????string
?TotalLeaves?int
?LeavesTaken?int
}
func?(e?Employee)?LeavesRemaining()?{
?fmt.Printf("%s?%s?has?%d?leaves?remaining",?e.FirstName,?e.LastName,?(e.TotalLeaves?-?e.LeavesTaken))
}
在上述程序里,第 1 行指定了該文件屬于 employee 包。而第 7 行聲明了一個 Employee 結構體。在第 14 行,結構體 Employee 添加了一個名為 LeavesRemaining 的方法。該方法會計算和顯示員工的剩余休假數(shù)。于是現(xiàn)在我們有了一個結構體,并綁定了結構體的方法,這與類很相似。
接著在 oop 文件夾里創(chuàng)建一個文件,命名為 main.go。
現(xiàn)在目錄結構如下所示:
workspacepath?->?oop?->?employee?->?employee.go
workspacepath?->?oop?->?main.go
main.go 的內容如下所示:
package?main
import?"oop/employee"
func?main()?{
?e?:=?employee.Employee?{
??FirstName:?"Sam",
??LastName:?"Adolf",
??TotalLeaves:?30,
??LeavesTaken:?20,
?}
?e.LeavesRemaining()
}
我們在第 3 行引用了 employee 包。在 main()(第 12 行),我們調用了 Employee 的 LeavesRemaining() 方法。
由于有自定義包,這個程序不能在 go playground 上運行。你可以在你的本地運行,在 workspacepath/bin/oop 下輸入命令 go install opp,程序會打印輸出:
Sam?Adolf?has?10?leaves?remaining
使用 New() 函數(shù),而非構造器
我們上面寫的程序看起來沒什么問題,但還是有一些細節(jié)問題需要注意。我們看看當定義一個零值的 employee 結構體變量時,會發(fā)生什么。將 main.go 的內容修改為如下代碼:
package?main
import?"oop/employee"
func?main()?{
?var?e?employee.Employee
?e.LeavesRemaining()
}
我們的修改只是創(chuàng)建一個零值的 Employee 結構體變量(第 6 行)。該程序會輸出:
has?0?leaves?remaining
你可以看到,使用 Employee 創(chuàng)建的零值變量沒有什么用。它沒有合法的姓名,也沒有合理的休假細節(jié)。
在像 Java 這樣的 OOP 語言中,是使用構造器來解決這種問題的。一個合法的對象必須使用參數(shù)化的構造器來創(chuàng)建。
Go 并不支持構造器。如果某類型的零值不可用,需要程序員來隱藏該類型,避免從其他包直接訪問。程序員應該提供一種名為 NewT(parameters) 的函數(shù),按照要求來初始化 T 類型的變量。按照 Go 的慣例,應該把創(chuàng)建 T 類型變量的函數(shù)命名為 NewT(parameters)。這就類似于構造器了。如果一個包只含有一種類型,按照 Go 的慣例,應該把函數(shù)命名為 New(parameters), 而不是 NewT(parameters)。
讓我修改一下原先的代碼,使得每當創(chuàng)建 employee 的時候,它都是可用的。
首先應該讓 Employee 結構體不可引用,然后創(chuàng)建一個 New 函數(shù),用于創(chuàng)建 Employee 結構體變量。在 employee.go 中輸入下面代碼:
package?employee
import?(
?"fmt"
)
type?employee?struct?{
?firstName???string
?lastName????string
?totalLeaves?int
?leavesTaken?int
}
func?New(firstName?string,?lastName?string,?totalLeave?int,?leavesTaken?int)?employee?{
?e?:=?employee?{firstName,?lastName,?totalLeave,?leavesTaken}
?return?e
}
func?(e?employee)?LeavesRemaining()?{
?fmt.Printf("%s?%s?has?%d?leaves?remaining",?e.firstName,?e.lastName,?(e.totalLeaves?-?e.leavesTaken))
}
我們進行了一些重要的修改。我們把 Employee 結構體的首字母改為小寫 e,也就是將 type Employee struct 改為了 type employee struct。通過這種方法,我們把 employee 結構體變?yōu)榱瞬豢梢玫模乐蛊渌鼘λ脑L問。除非有特殊需求,否則也要隱藏所有不可引用的結構體的所有字段,這是 Go 的最佳實踐。由于我們不會在外部包需要 employee 的字段,因此我們也讓這些字段無法引用。
同樣,我們還修改了 LeavesRemaining() 的方法。
現(xiàn)在由于 employee 不可引用,因此不能在其他包內直接創(chuàng)建 Employee 類型的變量。于是我們在第 14 行提供了一個可引用的 New 函數(shù),該函數(shù)接收必要的參數(shù),返回一個新創(chuàng)建的 employee 結構體變量。
這個程序還需要一些必要的修改,但現(xiàn)在先運行這個程序,理解一下當前的修改。如果運行當前程序,編譯器會報錯,如下所示:
go/src/constructor/main.go:6:?undefined:?employee.Employee
這是因為我們將 Employee 設置為不可引用,因此編譯器會報錯,提示該類型沒有在 main.go 中定義。很完美,正如我們期望的一樣,其他包現(xiàn)在不能輕易創(chuàng)建零值的 employee 變量了。我們成功地避免了創(chuàng)建不可用的 employee 結構體變量?,F(xiàn)在創(chuàng)建 employee 變量的唯一方法就是使用 New 函數(shù)。
如下所示,修改 main.go 里的內容。
package?main
import?"oop/employee"
func?main()?{
?e?:=?employee.New("Sam",?"Adolf",?30,?20)
?e.LeavesRemaining()
}
該文件唯一的修改就是第 6 行。通過向 New 函數(shù)傳入所需變量,我們創(chuàng)建了一個新的 employee 結構體變量。
下面是修改后的兩個文件的內容。
employee.go
package?employee
import?(
?"fmt"
)
type?employee?struct?{
?firstName???string
?lastName????string
?totalLeaves?int
?leavesTaken?int
}
func?New(firstName?string,?lastName?string,?totalLeave?int,?leavesTaken?int)?employee?{
?e?:=?employee?{firstName,?lastName,?totalLeave,?leavesTaken}
?return?e
}
func?(e?employee)?LeavesRemaining()?{
?fmt.Printf("%s?%s?has?%d?leaves?remaining",?e.firstName,?e.lastName,?(e.totalLeaves?-?e.leavesTaken))
}
main.go
package?main
import?"oop/employee"
func?main()?{
?e?:=?employee.New("Sam",?"Adolf",?30,?20)
?e.LeavesRemaining()
}
運行該程序,會輸出:
Sam?Adolf?has?10?leaves?remaining
現(xiàn)在你能明白了,雖然 Go 不支持類,但結構體能夠很好地取代類,而以 New(parameters) 簽名的方法可以替代構造器。
關于 Go 中的類和構造器到此結束。祝你愉快。
下一教程 - 組合取代繼承[3]
via: https://golangbot.com/structs-instead-of-classes/
作者:Nick Coghlan[4]譯者:Noluye[5]校對:polaris1119[6]
本文由 GCTT[7] 原創(chuàng)編譯,Go 中文網[8] 榮譽推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]FAQ: https://golang.org/doc/faq#Is_Go_an_object-oriented_language
[3]組合取代繼承: https://studygolang.com/articles/12680
[4]Nick Coghlan: https://golangbot.com/about/
[5]Noluye: https://github.com/Noluye
[6]polaris1119: https://github.com/polaris1119
[7]GCTT: https://github.com/studygolang/GCTT
[8]Go 中文網: https://studygolang.com/
推薦閱讀
