1. 程式人生 > >Go 系列教程-7 面向物件

Go 系列教程-7 面向物件

Go 系列教程 —— 26. 結構體取代類

 

Go 支援面向物件嗎?

Go 並不是完全面向物件的程式語言。Go 官網的 FAQ 回答了 Go 是否是面嚮物件語言,摘錄如下。

可以說是,也可以說不是。雖然 Go 有型別和方法,支援面向物件的程式設計風格,但卻沒有型別的層次結構。Go 中的“介面”概念提供了一種不同的方法,我們認為它易於使用,也更為普遍。Go 也可以將結構體巢狀使用,這與子類化(Subclassing)類似,但並不完全相同。此外,Go 提供的特性比 C++ 或 Java 更為通用:子類可以由任何型別的資料來定義,甚至是內建型別(如簡單的“未裝箱的”整型)。這在結構體(類)中沒有受到限制。

在接下來的教程裡,我們會討論如何使用 Go 來實現面向物件程式設計概念。與其它面嚮物件語言(如 Java)相比,Go 有很多完全不同的特性。

使用結構體,而非類

Go 不支援類,而是提供了結構體。結構體中可以新增方法。這樣可以將資料和操作資料的方法繫結在一起,實現與類相似的效果。

為了加深理解,我們來編寫一個示例吧。

在示例中,我們建立一個自定義,它幫助我們更好地理解,結構體是如何有效地取代類的。

在你的 Go 工作區建立一個名為 oop 的資料夾。在 opp 中再建立子資料夾 employee。在 employee

 內,建立一個名為 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 的方法。該方法會計算和顯示員工的剩餘休假數。於是現在我們有了一個結構體,並綁定了結構體的方法,這與類很相似。

接著在 oop 資料夾裡建立一個檔案,命名為 main.go

現在目錄結構如下所示:

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() 函式,而非構造器

我們上面寫的程式看起來沒什麼問題,但還是有一些細節問題需要注意。我們看看當定義一個零值的 employee 結構體變數時,會發生什麼。將 main.go 的內容修改為如下程式碼:

package main

import "oop/employee"

func main() {  
    var e employee.Employee
    e.LeavesRemaining()
}

我們的修改只是建立一個零值的 Employee 結構體變數(第 6 行)。該程式會輸出:

has 0 leaves remaining

你可以看到,使用 Employee 建立的零值變數沒有什麼用。它沒有合法的姓名,也沒有合理的休假細節。

在像 Java 這樣的 OOP 語言中,是使用構造器來解決這種問題的。一個合法的物件必須使用引數化的構造器來建立。

Go 並不支援構造器。如果某型別的零值不可用,需要程式設計師來隱藏該型別,避免從其他包直接訪問。程式設計師應該提供一種名為 NewT(parameters) 的 函式,按照要求來初始化 T 型別的變數。按照 Go 的慣例,應該把建立 T 型別變數的函式命名為 NewT(parameters)。這就類似於構造器了。如果一個包只含有一種型別,按照 Go 的慣例,應該把函式命名為 New(parameters), 而不是 NewT(parameters)

讓我修改一下原先的程式碼,使得每當建立 employee 的時候,它都是可用的。

首先應該讓 Employee 結構體不可引用,然後建立一個 New 函式,用於建立 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 結構體變為了不可引用的,防止其他包對它的訪問。除非有特殊需求,否則也要隱藏所有不可引用的結構體的所有欄位,這是 Go 的最佳實踐。由於我們不會在外部包需要 employee 的欄位,因此我們也讓這些欄位無法引用。

同樣,我們還修改了 LeavesRemaining() 的方法。

現在由於 employee 不可引用,因此不能在其他包內直接建立 Employee 型別的變數。於是我們在第 14 行提供了一個可引用的 New 函式,該函式接收必要的引數,返回一個新建立的 employee 結構體變數。

這個程式還需要一些必要的修改,但現在先執行這個程式,理解一下當前的修改。如果運行當前程式,編譯器會報錯,如下所示:

go/src/constructor/main.go:6: undefined: employee.Employee

這是因為我們將 Employee 設定為不可引用,因此編譯器會報錯,提示該型別沒有在 main.go 中定義。很完美,正如我們期望的一樣,其他包現在不能輕易建立零值的 employee 變量了。我們成功地避免了建立不可用的 employee 結構體變數。現在建立 employee 變數的唯一方法就是使用 New 函式。

如下所示,修改 main.go 裡的內容。

package main  

import "oop/employee"

func main() {  
    e := employee.New("Sam", "Adolf", 30, 20)
    e.LeavesRemaining()
}

該檔案唯一的修改就是第 6 行。通過向 New 函式傳入所需變數,我們建立了一個新的 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

現在你能明白了,雖然 Go 不支援類,但結構體能夠很好地取代類,而以 New(parameters) 簽名的方法可以替代構造器。

 

 

Go 系列教程 —— 27. 組合取代繼承

Go 不支援繼承,但它支援組合(Composition)。組合一般定義為“合併在一起”。汽車就是一個關於組合的例子:一輛汽車由車輪、引擎和其他各種部件組合在一起。

通過巢狀結構體進行組合

在 Go 中,通過在結構體內巢狀結構體,可以實現組合。

組合的典型例子就是部落格帖子。每一個部落格的帖子都有標題、內容和作者資訊。使用組合可以很好地表示它們。通過學習本教程後面的內容,我們會知道如何實現組合。

我們首先建立一個 author 結構體。

package main

import (  
    "fmt"
)

type author struct {  
    firstName string
    lastName  string
    bio       string
}

func (a author) fullName() string {  
    return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}

在上面的程式碼片段中,我們建立了一個 author 結構體,author 的欄位有 firstnamelastname 和 bio。我們還添加了一個 fullName() 方法,其中 author 作為接收者型別,該方法返回了作者的全名。

下一步我們建立 post 結構體。

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)
}

post 結構體的欄位有 title 和 content。它還有一個巢狀的匿名欄位 author。該欄位指定 author 組成了 post 結構體。現在 post 可以訪問 author 結構體的所有欄位和方法。我們同樣給 post 結構體添加了 details() 方法,用於列印標題、內容和作者的全名與簡介。

一旦結構體內嵌套了一個結構體欄位,Go 可以使我們訪問其巢狀的欄位,好像這些欄位屬於外部結構體一樣。所以上面第 11 行的 p.author.fullName() 可以替換為 p.fullName()。於是,details() 方法可以重寫,如下所示:

func (p post) details() {  
    fmt.Println("Title: ", p.title)
    fmt.Println("Content: ", p.content)
    fmt.Println("Author: ", p.fullName())
    fmt.Println("Bio: ", p.bio)
}

現在,我們的 author 和 post 結構體都已準備就緒,我們來建立一個部落格帖子來完成這個程式。

package main

import (  
    "fmt"
)

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)
}

func main() {  
    author1 := author{
        "Naveen",
        "Ramanathan",
        "Golang Enthusiast",
    }
    post1 := post{
        "Inheritance in Go",
        "Go supports composition instead of inheritance",
        author1,
    }
    post1.details()
}

在 playground 上執行

在上面程式中,main 函式在第 31 行新建了一個 author 結構體變數。而在第 36 行,我們通過巢狀 author1 來建立一個 post。該程式輸出:

Title:  Inheritance in Go  
Content:  Go supports composition instead of inheritance  
Author:  Naveen Ramanathan  
Bio:  Golang Enthusiast

結構體切片的巢狀

我們可以進一步處理這個示例,使用部落格帖子的切片來建立一個網站。:smile:

我們首先定義 website 結構體。請在上述程式碼裡的 main 函式中,新增下面的程式碼,並執行它。

type website struct {  
        []post
}
func (w website) contents() {  
    fmt.Println("Contents of Website\n")
    for _, v := range w.posts {
        v.details()
        fmt.Println()
    }
}

在你新增上述程式碼後,當你執行程式時,編譯器將會報錯,如下所示:

main.go:31:9: syntax error: unexpected [, expecting field name or embedded type

這項錯誤指出了巢狀的結構體切片 []post。錯誤的原因是結構體不能巢狀一個匿名切片。我們需要一個欄位名。所以我們來修復這個錯誤,讓編譯器順利通過。

type website struct {  
        posts []post
}

可以看到,我給帖子的切片 []post 添加了欄位名 posts

現在我們來修改主函式,為我們的新網站建立一些帖子吧。

修改後的完整程式碼如下所示:

package main

import (  
    "fmt"
)

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()
}

在 playground 中執行

在上面的主函式中,我們建立了一個作者 author1,以及三個帖子 post1post2 和 post3。我們最後通過巢狀三個帖子,在第 62 行建立了網站 w,並在下一行顯示內容。

程式會輸出:

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 系列教程 —— 28. 多型

Go 通過介面來實現多型。我們已經討論過,在 Go 語言中,我們是隱式地實現介面。一個型別如果定義了介面所宣告的全部方法,那它就實現了該介面。現在我們來看看,利用介面,Go 是如何實現多型的。

使用介面實現多型

一個型別如果定義了介面的所有方法,那它就隱式地實現了該介面。

所有實現了介面的型別,都可以把它的值儲存在一個介面型別的變數中。在 Go 中,我們使用介面的這種特性來實現多型

通過一個程式我們來理解 Go 語言的多型,它會計算一個組織機構的淨收益。為了簡單起見,我們假設這個虛構的組織所獲得的收入來源於兩個專案:fixed billing 和 time and material。該組織的淨收益等於這兩個專案的收入總和。同樣為了簡單起見,我們假設貨幣單位是美元,而無需處理美分。因此貨幣只需簡單地用 int 來表示。(我建議閱讀 https://forum.golangbridge.org/t/what-is-the-proper-golang-equivalent-to-decimal-when-dealing-with-money/413 上的文章,學習如何表示美分。感謝 Andreas Matuschek 在評論區指出這一點。)

我們首先定義一個介面 Income

type Income interface {  
    calculate() int
    source() string
}

上面定義了介面 Interface,它包含了兩個方法:calculate() 計算並返回專案的收入,而 source() 返回專案名稱。

下面我們定義一個表示 FixedBilling 專案的結構體型別。

type FixedBilling struct {  
    projectName string
    biddedAmount int
}

專案 FixedBillin 有兩個欄位:projectName 表示專案名稱,而 biddedAmount 表示組織向該專案投標的金額。

TimeAndMaterial 結構體用於表示專案 Time and Material。

type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

結構體 TimeAndMaterial 擁有三個欄位名:projectNamenoOfHours 和 hourlyRate

下一步我們給這些結構體型別定義方法,計算並返回實際收入和專案名稱。

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

在專案 FixedBilling 裡面,收入就是專案的投標金額。因此我們返回 FixedBilling 型別的 calculate() 方法。

而在專案 TimeAndMaterial 裡面,收入等於 noOfHours 和 hourlyRate 的乘積,作為 TimeAndMaterial 型別的 calculate() 方法的返回值。

我們還通過 source() 方法返回了表示收入來源的專案名稱。

由於 FixedBilling 和 TimeAndMaterial 兩個結構體都定義了 Income 介面的兩個方法:calculate() 和 source(),因此這兩個結構體都實現了 Income 介面。

我們來宣告一個 calculateNetIncome 函式,用來計算並列印總收入。

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

上面的函式接收一個 Income 介面型別的切片作為引數。該函式會遍歷這個介面切片,並依個呼叫 calculate() 方法,計算出總收入。該函式同樣也會通過呼叫 source() 顯示收入來源。根據 Income 介面的具體型別,程式會呼叫不同的 calculate() 和 source() 方法。於是,我們在 calculateNetIncome 函式中就實現了多型。

如果在該組織以後增加了新的收入來源,calculateNetIncome 無需修改一行程式碼,就可以正確地計算總收入了。:smile:

最後就剩下這個程式的 main 函數了。

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

在上面的 main 函式中,我們建立了三個專案,有兩個是 FixedBilling 型別,一個是 TimeAndMaterial 型別。接著我們建立了一個 Income 型別的切片,存放了這三個專案。由於這三個專案都實現了 Interface 介面,因此可以把這三個專案放入 Income 切片。最後我們將該切片作為引數,呼叫了 calculateNetIncome 函式,顯示了專案不同的收益和收入來源。

以下完整的程式碼供你參考。

package main

import (  
    "fmt"
)

type Income interface {  
    calculate() int
    source() string
}

type FixedBilling struct {  
    projectName string
    biddedAmount int
}

type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

在 playground 上執行

該程式會輸出:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Net income of organisation = $19000

新增收益流

假設前面的組織通過廣告業務,建立了一個新的收益流(Income Stream)。我們可以看到新增它非常簡單,並且計算總收益也很容易,我們無需對 calculateNetIncome 函式進行任何修改。這就是多型的好處。

我們首先定義 Advertisement 型別,並在 Advertisement 型別中定義 calculate() 和 source() 方法。

type Advertisement struct {  
    adName     string
    CPC        int
    noOfClicks int
}

func (a Advertisement) calculate() int {  
    return a.CPC * a.noOfClicks
}

func (a Advertisement) source() string {  
    return a.adName
}

Advertisement 型別有三個欄位,分別是 adNameCPC(每次點選成本)和 noOfClicks(點選次數)。廣告的總收益等於 CPC 和 noOfClicks 的乘積。

現在我們稍微修改一下 main 函式,把新的收益流新增進來。

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
    popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
    calculateNetIncome(incomeStreams)
}

我們建立了兩個廣告專案,即 bannerAd 和 popupAdincomeStream 切片包含了這兩個建立的廣告專案。

package main

import (  
    "fmt"
)

type Income interface {  
    calculate() int
    source() string
}

type FixedBilling struct {  
    projectName  string
    biddedAmount int
}

type TimeAndMaterial struct {  
    projectName string
    noOfHours   int
    hourlyRate  int
}

type Advertisement struct {  
    adName     string
    CPC        int
    noOfClicks int
}

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

func (a Advertisement) calculate() int {  
    return a.CPC * a.noOfClicks
}

func (a Advertisement) source() string {  
    return a.adName
}
func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
    popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
    calculateNetIncome(incomeStreams)
}

在 playground 中執行

上面程式會輸出:

Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Income From Banner Ad = $1000  
Income From Popup Ad = $3750  
Net income of organisation = $23750

你會發現,儘管我們新增了收益流,但卻完全沒有修改 calculateNetIncome 函式。這就是多型帶來的好處。由於新的 Advertisement同樣實現了 Income 介面,所以我們能夠向 incomeStreams 切片新增 AdvertisementcalculateNetIncome 無需修改,因為它能夠呼叫 Advertisement 型別的 calculate() 和 source() 方法。

 

 

Go 系列教程 —— 29. Defer

 

什麼是 defer?

defer 語句的用途是:含有 defer 語句的函式,會在該函式將要返回之前,呼叫另一個函式。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。

示例

package main

import (  
    "fmt"
)

func finished() {  
    fmt.Println("Finished finding largest")
}

func largest(nums []int) {  
    defer finished()
    fmt.Println("Started finding largest")
    max := nums[0]
    for _, v := range nums {
        if v > max {
            max = v
        }
    }
    fmt.Println("Largest number in", nums, "is", max)
}

func main() {  
    nums := []int{78, 109, 2, 563, 300}
    largest(nums)
}

在 playground 上執行

上面的程式很簡單,就是找出一個給定切片的最大值。largest 函式接收一個 int 型別的切片作為引數,然後打印出該切片中的最大值。largest 函式的第一行的語句為 defer finished()。這表示在 finished() 函式將要返回之前,會呼叫 finished() 函式。執行該程式,你會看到有如下輸出:

Started finding largest  
Largest number in [78 109 2 563 300] is 563  
Finished finding largest

largest 函式開始執行後,會列印上面的兩行輸出。而就在 largest 將要返回的時候,又呼叫了我們的延遲函式(Deferred Function),打印出 Finished finding largest 的文字。:smile:

延遲方法

defer 不僅限於函式的呼叫,呼叫方法也是合法的。我們寫一個小程式來測試吧。

package main

import (  
    "fmt"
)


type person struct {  
    firstName string
    lastName string
}

func (p person) fullName() {  
    fmt.Printf("%s %s",p.firstName,p.lastName)
}

func main() {  
    p := person {
        firstName: "John",
        lastName: "Smith",
    }
    defer p.fullName()
    fmt.Printf("Welcome ")  
}

在 playground 上執行

在上面的例子中,我們在第 22 行延遲了一個方法呼叫。而其他的程式碼很直觀,這裡不再解釋。該程式輸出:

Welcome John Smith

實參取值(Arguments Evaluation)

在 Go 語言中,並非在呼叫延遲函式的時候才確定實參,而是當執行 defer 語句的時候,就會對延遲函式的實參進行求值。

通過一個例子就能夠理解了。

package main

import (  
    "fmt"
)

func printA(a int) {  
    fmt.Println("value of a in deferred function", a)
}
func main() {  
    a := 5
    defer printA(a)
    a = 10
    fmt.Println("value of a before deferred function call", a)

}

在 playground 上執行

在上面的程式裡的第 11 行,a 的初始值為 5。在第 12 行執行 defer 語句的時候,由於 a 等於 5,因此延遲函式 printA 的實參也等於 5。接著我們在第 13 行將 a 的值修改為 10。下一行會打印出 a 的值。該程式輸出:

value of a before deferred function call 10  
value of a in deferred function 5

從上面的輸出,我們可以看出,在呼叫了 defer 語句後,雖然我們將 a 修改為 10,但呼叫延遲函式 printA(a)後,仍然列印的是 5。

defer 棧

當一個函式內多次呼叫 defer 時,Go 會把 defer 呼叫放入到一個棧中,隨後按照後進先出(Last In First Out, LIFO)的順序執行。

我們下面編寫一個小程式,使用 defer 棧,將一個字串逆序列印。

package main

import (  
    "fmt"
)

func main() {  
    name := "Naveen"
    fmt.Printf("Orignal String: %s\n", string(name))
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
}

在 playground 上執行

在上述程式中的第 11 行,for range 迴圈會遍歷一個字串,並在第 12 行呼叫了 defer fmt.Printf("%c", v)。這些延遲呼叫會新增到一個棧中,按照後進先出的順序執行,因此,該字串會逆序打印出來。該程式會輸出:

Orignal String: Naveen  
Reversed String: neevaN

defer 的實際應用

目前為止,我們看到的程式碼示例,都沒有體現出 defer 的實際用途。本節我們會看看 defer 的實際應用。

當一個函式應該在與當前程式碼流(Code Flow)無關的環境下呼叫時,可以使用 defer。我們通過一個用到了 WaitGroup 程式碼示例來理解這句話的含義。我們首先會寫一個沒有使用 defer 的程式,然後我們會用 defer 來修改,看到 defer 帶來的好處。

package main

import (  
    "fmt"
    "sync"
)

type rect struct {  
    length int
    width  int
}

func (r rect) area(wg *sync.WaitGroup) {  
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        wg.Done()
        return
    }
    if r.width < 0 {
        fmt.Printf("rect %v's width should be greater than zero\n", r)
        wg.Done()
        return
    }
    area := r.length * r.width
    fmt.Printf("rect %v's area %d\n", r, area)
    wg.Done()
}

func main() {  
    var wg sync.WaitGroup
    r1 := rect{-67, 89}
    r2 := rect{5, -67}
    r3 := rect{8, 9}
    rects := []rect{r1, r2, r3}
    for _, v := range rects {
        wg.Add(1)
        go v.area(&wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

在 playground 上執行

在上面的程式裡,我們在第 8 行建立了 rect 結構體,並在第 13 行建立了 rect 的方法 area,計算出矩形的面積。area 檢查了矩形的長寬是否小於零。如果矩形的長寬小於零,它會打印出對應的提示資訊,而如果大於零,它會打印出矩形的面積。

main 函式建立了 3 個 rect 型別的變數:r1r2 和 r3。在第 34 行,我們把這 3 個變數新增到了 rects 切片裡。該切片接著使用 for range 迴圈遍歷,把 area 方法作為一個併發的 Go 協程進行呼叫(第 37 行)。我們用 WaitGroup wg 來確保 main 函式在其他協程執行完畢之後,才會結束執行。WaitGroup 作為引數傳遞給 area 方法後,在第 16 行、第 21 行和第 26 行通知 main 函式,表示現在協程已經完成所有任務。如果你仔細觀察,會發現 wg.Done() 只在 area 函式返回的時候才會呼叫。wg.Done() 應該在 area將要返回之前呼叫,並且與程式碼流的路徑(Path)無關,因此我們可以只調用一次 defer,來有效地替換掉 wg.Done() 的多次呼叫

我們來用 defer 來重寫上面的程式碼。

在下面的程式碼中,我們移除了原先程式中的 3 個 wg.Done 的呼叫,而是用一個單獨的 defer wg.Done() 來取代它(第 14 行)。這使得我們的程式碼更加簡潔易懂。

package main

import (  
    "fmt"
    "sync"
)

type rect struct {  
    length int
    width  int
}

func (r rect) area(wg *sync.WaitGroup) {  
    defer wg.Done()
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        return
    }
    if r.width < 0 {
        fmt.Printf("rect %v's width should be greater than zero\n", r)
        return
    }
    area := r.length * r.width
    fmt.Printf("rect %v's area %d\n", r, area)
}

func main() {  
    var wg sync.WaitGroup
    r1 := rect{-67, 89}
    r2 := rect{5, -67}
    r3 := rect{8, 9}
    rects := []rect{r1, r2, r3}
    for _, v := range rects {
        wg.Add(1)
        go v.area(&wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

在 playground 上執行

該程式會輸出:

rect {8 9}'s area 72  
rect {-67 89}'s length should be greater than zero  
rect {5 -67}'s width should be greater than zero  
All go routines finished executing

在上面的程式中,使用 defer 還有一個好處。假設我們使用 if 條件語句,又給 area 方法添加了一條返回路徑(Return Path)。如果沒有使用 defer 來呼叫 wg.Done(),我們就得很小心了,確保在這條新添的返回路徑裡呼叫了 wg.Done()。由於現在我們延遲呼叫了 wg.Done(),因此無需再為這條新的返回路徑新增 wg.Done() 了。

本教程到此結束。祝你愉快。