go教程8
1. 合取代继承
Go 不支持继承,但它支持组合(Composition)。组合一般定义为“合并在一起”。汽车就是一个关于组合的例子:一辆汽车由车轮、引擎和其他各种部件组合在一起。
通过嵌套结构体进行组合
在 Go 中,通过在结构体内嵌套结构体,可以实现组合。
组合的典型例子就是博客帖子。每一个博客的帖子都有标题、内容和作者信息。使用组合可以很好地表示它们。通过学习本教程后面的内容,我们会知道如何实现组合。
我们首先创建一个 author
结构体。
package main
import(
"fmt"
)
typeauthor
struct{
firstName
string
lastName
string
bio
string
}
func(
a author
)fullName()
string
{
return
fmt
.Sprintf("%s %s",a
.firstName
,a
.lastName
)
}
在上面的代码片段中,我们创建了一个 author
结构体,author
的字段有 firstname
、lastname
和 bio
。我们还添加了一个 fullName()
方法,其中 author
作为接收者类型,该方法返回了作者的全名。
下一步我们创建 post
结构体。
typepost
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"
)
typeauthor
struct{
firstName
string
lastName
string
bio
string
}
func(
a author
)fullName()
string
{
return
fmt
.Sprintf("%s %s",a
.firstName
,a
.lastName
)
}
typepost
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
)
}
funcmain()
{
author1
:=author
{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1
:=post
{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1
,
}
post1
.details()
}
在上面程序中,main 函数在第 31 行新建了一个 author
结构体变量。而在第 36 行,我们通过嵌套 author1
来创建一个 post
。该程序输出:
Title
:Inheritance
inGo
Content
:Go supports composition instead of inheritance
Author
:Naveen Ramanathan
Bio
:Golang Enthusiast
结构体切片的嵌套
我们可以进一步处理这个示例,使用博客帖子的切片来创建一个网站。
我们首先定义 website
结构体。请在上述代码里的 main 函数中,添加下面的代码,并运行它。
typewebsite
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
。错误的原因是结构体不能嵌套一个匿名切片。我们需要一个字段名。所以我们来修复这个错误,让编译器顺利通过。
typewebsite
struct{
posts
[]post
}
可以看到,我给帖子的切片 []post
添加了字段名 posts
。
现在我们来修改主函数,为我们的新网站创建一些帖子吧。
修改后的完整代码如下所示:
package main
import(
"fmt"
)
typeauthor
struct{
firstName
string
lastName
string
bio
string
}
func(
a author
)fullName()
string
{
return
fmt
.Sprintf("%s %s",a
.firstName
,a
.lastName
)
}
typepost
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
)
}
typewebsite
struct{
posts
[]post
}
func(
w website
)contents()
{
fmt
.Println("Contents of Website\n")
for
_,
v
:=range
w
.posts
{
v
.details()
fmt
.Println()
}
}
funcmain()
{
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()
}
在上面的主函数中,我们创建了一个作者 author1
,以及三个帖子 post1
、post2
和 post3
。我们最后通过嵌套三个帖子,在第 62 行创建了网站 w
,并在下一行显示内容。
程序会输出:
Contents of Website
Title
:Inheritance
inGo
Content
:Go supports composition instead of inheritance
Author
:Naveen Ramanathan
Bio
:Golang Enthusiast
Title
:Struct instead of Classes
inGo
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
本教程到此结束。祝你愉快。
2. 多态
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
。
typeIncome
interface{
calculate()
int
source()
string
}
上面定义了接口 Interface
,它包含了两个方法:calculate()
计算并返回项目的收入,而 source()
返回项目名称。
下面我们定义一个表示 FixedBilling
项目的结构体类型。
typeFixedBilling
struct{
projectName
string
biddedAmount
int
}
项目 FixedBillin
有两个字段:projectName
表示项目名称,而 biddedAmount
表示组织向该项目投标的金额。
TimeAndMaterial
结构体用于表示项目 Time and Material。
typeTimeAndMaterial
struct{
projectName
string
noOfHours
int
hourlyRate
int
}
结构体 TimeAndMaterial
拥有三个字段名:projectName
、noOfHours
和 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
函数,用来计算并打印总收入。
funccalculateNetIncome(
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
无需修改一行代码,就可以正确地计算总收入了。
最后就剩下这个程序的 main
函数了。
funcmain()
{
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"
)
typeIncome
interface{
calculate()
int
source()
string
}
typeFixedBilling
struct{
projectName
string
biddedAmount
int
}
typeTimeAndMaterial
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
}
funccalculateNetIncome(
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
)
}
funcmain()
{
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
)
}
该程序会输出:
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()
方法。
typeAdvertisement
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
类型有三个字段,分别是 adName
、CPC
(每次点击成本)和 noOfClicks
(点击次数)。广告的总收益等于 CPC
和 noOfClicks
的乘积。
现在我们稍微修改一下 main
函数,把新的收益流添加进来。
funcmain()
{
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
和 popupAd
。incomeStream
切片包含了这两个创建的广告项目。
package main
import(
"fmt"
)
typeIncome
interface{
calculate()
int
source()
string
}
typeFixedBilling
struct{
projectName
string
biddedAmount
int
}
typeTimeAndMaterial
struct{
projectName
string
noOfHours
int
hourlyRate
int
}
typeAdvertisement
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
}
funccalculateNetIncome(
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
)
}
funcmain()
{
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
)
}
上面程序会输出:
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
切片添加 Advertisement
。calculateNetIncome
无需修改,因为它能够调用 Advertisement
类型的 calculate()
和 source()
方法。
本教程到此结束。祝你愉快。
本文来自博客园,作者:易先讯,转载请注明原文链接:https://www.cnblogs.com/gongxianjin/p/17981810