面向?qū)ο?OOP)
go并不是一個(gè)純面向?qū)ο蟮木幊陶Z(yǔ)言。在go中的面向?qū)ο螅Y(jié)構(gòu)體替換了類。
Go并沒有提供類class,但是它提供了結(jié)構(gòu)體struct,方法method,可以在結(jié)構(gòu)體上添加。提供了捆綁數(shù)據(jù)和方法的行為,這些數(shù)據(jù)和方法與類類似。
定義結(jié)構(gòu)體和方法
通過以下代碼來(lái)更好的理解,首先在src目錄下創(chuàng)建一個(gè)package命名為oop,在oop目錄下,再創(chuàng)建一個(gè)子目錄命名為employee,在該目錄下創(chuàng)建一個(gè)go文件命名為employee.go。
目錄結(jié)構(gòu):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))
}
然后在oop目錄下,創(chuàng)建文件并命名為main.go,并保存以下內(nèi)容
package main
import "oop/employee"
func main() {
e := employee.Employee {
FirstName: "Sam",
LastName: "Adolf",
TotalLeaves: 30,
LeavesTaken: 20,
}
e.LeavesRemaining()
}
運(yùn)行結(jié)果:
Sam Adolf has 10 leaves remaining
New()函數(shù)替代了構(gòu)造函數(shù)
我們上面寫的程序看起來(lái)不錯(cuò),但是里面有一個(gè)微妙的問題。讓我們看看當(dāng)我們用0值定義employee struct時(shí)會(huì)發(fā)生什么。更改main的內(nèi)容。轉(zhuǎn)到下面的代碼,
package main
import "oop/employee"
func main() {
var e employee.Employee
e.LeavesRemaining()
}
運(yùn)行結(jié)果:
has 0 leaves remaining
通過運(yùn)行結(jié)果可以知道,使用Employee的零值創(chuàng)建的變量是不可用的。它沒有有效的名、姓,也沒有有效的保留細(xì)節(jié)。在其他的OOP語(yǔ)言中,比如java,這個(gè)問題可以通過使用構(gòu)造函數(shù)來(lái)解決。使用參數(shù)化構(gòu)造函數(shù)可以創(chuàng)建一個(gè)有效的對(duì)象。
go不支持構(gòu)造函數(shù)。如果某個(gè)類型的零值不可用,則程序員的任務(wù)是不導(dǎo)出該類型以防止其他包的訪問,并提供一個(gè)名為NewT(parameters)的函數(shù),該函數(shù)初始化類型T和所需的值。在go中,它是一個(gè)命名一個(gè)函數(shù)的約定,它創(chuàng)建了一個(gè)T類型的值給NewT(parameters)。這就像一個(gè)構(gòu)造函數(shù)。如果包只定義了一個(gè)類型,那么它的一個(gè)約定就是將這個(gè)函數(shù)命名為New(parameters)而不是NewT(parameters)。
更改employee.go的代碼:
首先修改employee結(jié)構(gòu)體為非導(dǎo)出,并創(chuàng)建一個(gè)函數(shù)New(),它將創(chuàng)建一個(gè)新Employee。代碼如下:
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))
}
我們?cè)谶@里做了一些重要的改變。我們已經(jīng)將Employee struct的起始字母e設(shè)置為小寫,即我們已經(jīng)將類型Employee struct更改為type employee struct。通過這樣做,我們成功地導(dǎo)出了employee結(jié)構(gòu)并阻止了其他包的訪問。將未導(dǎo)出的結(jié)構(gòu)的所有字段都導(dǎo)出為未導(dǎo)出的方法是很好的做法,除非有特定的需要導(dǎo)出它們。由于我們不需要在包之外的任何地方使用employee struct的字段,所以我們也沒有導(dǎo)出所有字段。
由于employee是未導(dǎo)出的,所以不可能從其他包中創(chuàng)建類型employee的值。因此,我們提供了一個(gè)輸出的新函數(shù)。將所需的參數(shù)作為輸入并返回新創(chuàng)建的employee。
這個(gè)程序還需要做一些修改,讓它能夠工作,但是讓我們運(yùn)行這個(gè)程序來(lái)了解到目前為止變化的效果。如果這個(gè)程序運(yùn)行,它將會(huì)失敗,有以下編譯錯(cuò)誤,
go/src/constructor/main.go:6: undefined: employee.Employee
這是因?yàn)槲覀冇形磳?dǎo)出的Employee,因此編譯器拋出錯(cuò)誤,該類型在main中沒有定義。完美的。正是我們想要的?,F(xiàn)在沒有其他的包能夠創(chuàng)建一個(gè)零值的員工。我們成功地防止了一個(gè)無(wú)法使用的員工結(jié)構(gòu)價(jià)值被創(chuàng)建?,F(xiàn)在創(chuàng)建員工的唯一方法是使用新功能。
修改main.go代碼
package main
import "oop/employee"
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}
運(yùn)行結(jié)果:
Sam Adolf has 10 leaves remaining
因此,我們可以明白,雖然Go不支持類,但是結(jié)構(gòu)體可以有效地使用,在使用構(gòu)造函數(shù)的位置,使用New(parameters)的方法即可。
組成(Composition )替代了繼承(Inheritance)
Go不支持繼承,但它支持組合。組合的一般定義是“放在一起”。構(gòu)圖的一個(gè)例子就是汽車。汽車是由輪子、發(fā)動(dòng)機(jī)和其他各種部件組成的。
博客文章就是一個(gè)完美的組合例子。每個(gè)博客都有標(biāo)題、內(nèi)容和作者信息。這可以用組合完美地表示出來(lái)。
通過嵌入結(jié)構(gòu)體實(shí)現(xiàn)組成
可以通過將一個(gè)struct類型嵌入到另一個(gè)結(jié)構(gòu)中實(shí)現(xiàn)。
示例代碼:
package main
import (
"fmt"
)
/*
我們創(chuàng)建了一個(gè)author struct,它包含字段名、lastName和bio。我們還添加了一個(gè)方法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有字段標(biāo)題、內(nèi)容。它還有一個(gè)嵌入式匿名字段作者。這個(gè)字段表示post struct是由author組成的。現(xiàn)在post struct可以訪問作者結(jié)構(gòu)的所有字段和方法。我們還在post struct中添加了details()方法,它打印出作者的標(biāo)題、內(nèi)容、全名和bio。
*/
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()
}
運(yùn)行結(jié)果:
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast
嵌入結(jié)構(gòu)體的切片
在以上程序的main函數(shù)下增加以下代碼,并運(yùn)行
type website struct {
[]post
}
func (w website) contents() {
fmt.Println("Contents of Website\n")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
運(yùn)行報(bào)錯(cuò):
main.go:31:9: syntax error: unexpected [, expecting field name or embedded type
這個(gè)錯(cuò)誤指向structs []post的嵌入部分。原因是不可能匿名嵌入一片。需要一個(gè)字段名。我們來(lái)修正這個(gè)錯(cuò)誤,讓編譯器通過。
type website struct {
posts []post
}
現(xiàn)在讓我們修改的main函數(shù),為我們的新的website創(chuàng)建幾個(gè)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()
}
運(yùn)行結(jié)果:
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
多態(tài)性(Polymorphism)
Go中的多態(tài)性是在接口的幫助下實(shí)現(xiàn)的。正如我們已經(jīng)討論過的,接口可以在Go中隱式地實(shí)現(xiàn)。如果類型為接口中聲明的所有方法提供了定義,則實(shí)現(xiàn)一個(gè)接口。讓我們看看在接口的幫助下如何實(shí)現(xiàn)多態(tài)。
任何定義接口所有方法的類型都被稱為隱式地實(shí)現(xiàn)該接口。
類型接口的變量可以保存實(shí)現(xiàn)接口的任何值。接口的這個(gè)屬性用于實(shí)現(xiàn)Go中的多態(tài)性。
舉個(gè)例子,一個(gè)虛構(gòu)的組織有兩種項(xiàng)目的收入:固定的賬單和時(shí)間和材料。組織的凈收入是由這些項(xiàng)目的收入之和計(jì)算出來(lái)的。為了保持本教程的簡(jiǎn)單,我們假設(shè)貨幣是美元,我們不會(huì)處理美分。它將使用整數(shù)來(lái)表示。
首先我們定義一個(gè)接口:Income
type Income interface {
calculate() int
source() string
}
接下來(lái),定義兩個(gè)結(jié)構(gòu)體:FixedBilling和TimeAndMaterial
type FixedBilling struct {
projectName string
biddedAmount int
}
type TimeAndMaterial struct {
projectName string
noOfHours int
hourlyRate int
}
下一步是定義這些結(jié)構(gòu)體類型的方法,計(jì)算并返回實(shí)際收入和收入來(lái)源。
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
}
接下來(lái),我們來(lái)聲明一下計(jì)算和打印總收入的calculateNetIncome函數(shù)。
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)
}
上面的calculateNetIncome函數(shù)接受一部分Income接口作為參數(shù)。它通過遍歷切片和調(diào)用calculate()方法來(lái)計(jì)算總收入。它還通過調(diào)用source()方法來(lái)顯示收入來(lái)源。根據(jù)收入接口的具體類型,將調(diào)用不同的calculate()和source()方法。因此,我們?cè)赾alculateNetIncome函數(shù)中實(shí)現(xiàn)了多態(tài)。
在未來(lái),如果組織增加了一種新的收入來(lái)源,這個(gè)函數(shù)仍然可以正確地計(jì)算總收入,而沒有一行代碼更改。
最后我們寫以下主函數(shù):
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)
}
運(yùn)行結(jié)果:
Income From Project 1 = $5000
Income From Project 2 = $10000
Income From Project 3 = $4000
Net income of organisation = $19000
假設(shè)該組織通過廣告找到了新的收入來(lái)源。讓我們看看如何簡(jiǎn)單地添加新的收入方式和計(jì)算總收入,而不用對(duì)calculateNetIncome函數(shù)做任何更改。由于多態(tài)性,這樣是可行的。
首先讓我們定義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
}
廣告類型有三個(gè)字段adName, CPC(cost per click)和noof點(diǎn)擊數(shù)(cost per click)。廣告的總收入是CPC和noOfClicks的產(chǎn)品。
修改主函數(shù):
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)
}
運(yùn)行結(jié)果:
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
綜上,我們沒有對(duì)calculateNetIncome函數(shù)做任何更改,盡管我們添加了新的收入方式。它只是因?yàn)槎鄳B(tài)性而起作用。由于新的Advertisement類型也實(shí)現(xiàn)了Income接口,我們可以將它添加到incomeStreams切片中。calculateNetIncome函數(shù)也在沒有任何更改的情況下工作,因?yàn)樗梢哉{(diào)用Advertisement類型的calculate()和source()方法。