Golang通脈之面向物件
面向物件的三大特徵:
- 封裝:隱藏物件的屬性和實現細節,僅對外提供公共訪問方式
- 繼承:使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等
- 多型:不同物件中同種行為的不同實現方式
Go並不是一個純面向物件的程式語言。在 Go 語言中可以使用結構體struct
對屬性進行封裝,結構體就像是類的一種簡化形式。可以在結構體上新增捆綁資料和方法的行為,這些資料和方法與類類似
Go語言沒有“類”的概念,也不支援“類”的繼承等面向物件的概念。Go語言中通過結構體的內嵌再配合介面比面向物件具有更高的擴充套件性和靈活性。
結構體和方法
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)) } func main() { e := employee.Employee { FirstName: "Sam", LastName: "Adolf", TotalLeaves: 30, LeavesTaken: 20, } e.LeavesRemaining() }
New()函式
Java語言中提供了構造方法建立並初始化物件,在Go語言中一般需要自己實現一個對外可見的New函式
func main() {
var e Employee
e.LeavesRemaining()
}
執行結果:
has 0 leaves remaining
通過執行結果可以知道,使用Employee的零值建立的變數是不可用的。它沒有有效的名、姓,也沒有有效的保留細節。在其他的OOP語言中,比如java
,這個問題可以通過使用建構函式來解決。使用引數化建構函式可以建立一個有效的物件。
go不支援建構函式。如果某個型別的零值不可用,則程式設計師的任務是不匯出該型別以防止其他包的訪問,並提供一個名為NewT(parameters)
NewT(parameters)
。這就像一個建構函式。如果包只定義了一個型別,那麼它的一個約定就是將這個函式命名為New(parameters)
而不是NewT(parameters)
。
首先修改employee結構體為非匯出,並建立一個函式New(),它將建立一個新Employee:
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 struct
的起始字母e設定為小寫。
由於employee
是未匯出的,所以不可能從其他包中建立型別employee
的值。因此,提供了一個輸出的新函式。將所需的引數作為輸入並返回新建立的employee
。
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}
執行結果:
Sam Adolf has 10 leaves remaining
因此,雖然Go不支援類,但是結構體可以有效地使用,在使用建構函式的位置,使用New(parameters)的方法即可。
組合與繼承
在 Go 語言中沒有 extends
關鍵字,它使用在結構體中內嵌匿名型別的方法來實現繼承。在Go語言中稱之為組合(Composition)。組合的一般定義是“放在一起”。
舉一個部落格文章例子:每個部落格都有標題、內容和作者資訊。這可以用組合完美地表示出來。
嵌入結構體實現組合
可以通過將一個struct
型別嵌入到另一個結構中實現:
/*
建立了一個author struct,它包含欄位名、lastName和bio。添加了一個方法fullName(),將作者作為接收者型別,這將返回作者的全名。
*/
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
/*
post struct有欄位標題、內容。它還有一個嵌入式匿名欄位作者。這個欄位表示post struct是由author組成的。現在post struct可以訪問作者結構的所有欄位和方法。還在post struct中添加了details()方法
*/
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.author.fullName())
fmt.Println("Bio: ", p.author.bio)
}
func main() {
author1 := author{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1 := post{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1,
}
post1.details()
}
執行結果:
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast
嵌入結構體的切片
在以上程式的main函式下增加以下程式碼,並執行
type website struct {
[]post
}
func (w website) contents() {
fmt.Println("Contents of Website\n")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
執行報錯:
syntax error: unexpected [, expecting field name or embedded type
這個錯誤指向structs []post
的嵌入部分。原因是切片不能當做匿名欄位使用。需要一個欄位名
type website struct {
posts []post
}
修改完完整程式碼如下:
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.fullName())
fmt.Println("Bio: ", p.bio)
}
type website struct {
posts []post
}
func (w website) contents() {
fmt.Println("Contents of Website\n")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
func main() {
author1 := author{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1 := post{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1,
}
post2 := post{
"Struct instead of Classes in Go",
"Go does not support classes but methods can be added to structs",
author1,
}
post3 := post{
"Concurrency",
"Go is a concurrent language and not a parallel one",
author1,
}
w := website{
posts: []post{post1, post2, post3},
}
w.contents()
}
執行結果:
Contents of Website
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast
Title: Struct instead of Classes in Go
Content: Go does not support classes but methods can be added to structs
Author: Naveen Ramanathan
Bio: Golang Enthusiast
Title: Concurrency
Content: Go is a concurrent language and not a parallel one
Author: Naveen Ramanathan
Bio: Golang Enthusiast
介面與多型
Go中的多型性(Polymorphism)是在介面的幫助下實現的。介面可以在Go中隱式地實現。如果型別為介面中宣告的所有方法提供了定義,則該型別實現了這個介面。
任何定義介面所有方法的型別都被稱為隱式地實現該介面。
型別介面的變數可以儲存實現介面的任何值。介面的這個屬性用於實現Go中的多型性。
定義一個正方形 Square
和一個長方形 Rectangle
:
// 正方形
type Square struct {
side float32
}
// 長方形
type Rectangle struct {
length, width float32
}
計算這兩個幾何圖形的面積。但由於他們的面積計算方式不同,需要定義兩個不同的 Area()
方法。
於是,可以定義一個包含 Area()
方法的介面 Shaper
,讓 Square
和 Rectangle
都實現這個接口裡的 Area()
:
// 介面 Shaper
type Shaper interface {
Area() float32
}
// 計算正方形的面積
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
// 計算長方形的面積
func (r *Rectangle) Area() float32 {
return r.length * r.width
}
在 main()
函式中呼叫 Area()
:
func main() {
r := &Rectangle{10, 2}
q := &Square{10}
// 建立一個 Shaper 型別的陣列
shapes := []Shaper{r, q}
// 迭代陣列上的每一個元素並呼叫 Area() 方法
for n, _ := range shapes {
fmt.Println("圖形資料: ", shapes[n])
fmt.Println("它的面積是: ", shapes[n].Area())
}
}
/*Output:
圖形資料: &{10 2}
它的面積是: 20
圖形資料: &{10}
它的面積是: 100
*/
由以上程式碼輸出結果可知:不同物件呼叫 Area()
方法產生了不同的結果,展現了多型的特徵。
總結
- 面向物件的三大特徵是:封裝、繼承和多型
- Go 語言使用結構體對屬性進行封裝,結構體就像是類的一種簡化形式
- 在 Go 語言中,方法是作用在接收者(receiver)上的一個函式,接收者是某種型別的變數
- 名稱首字母的大小寫決定了該變數/常量/型別/介面/結構/函式……能否被外部包匯入
- 無法被匯入的欄位可以使用
getter
和setter
的方式來訪問 - Go 語言使用在結構體中內嵌匿名型別的方法來實現繼承
- 使用介面可以實現多型