Go教程第十五篇: 接口-1
本文是《Go系列教程》的第十五篇文章,本章我们将讲解接口的第一部分。
什么是接口?
在Go中,接口就是一组方法签名的集合。若一个类定义了接口中的所有方法,我们就说,它实现了这个接口。这和面向对象编程很类似。接口规定了一个类应该有哪些方法,而类可以决定如何实现这些方法。
例如,洗衣机可以是一个接口,它有俩个方法签名: Cleaning()和Drying()。任一个类只要定义了Cleaning()和Drying()方法,我们就说它实现了洗衣机接口。
声明和实现接口
我们来写段程序,创建一个接口并实现它。
package main
import (
"fmt"
)
//interface definition
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson")
var v VowelsFinder
v = name // possible since MyString implements VowelsFinder
fmt.Printf("Vowels are %c", v.FindVowels())
}
在上面的程序中,我们创建了一个接口类型,名叫VowelsFinder,它有一个方法:FindVowels() []rune。在紧接着的一行,又创建了一个类:MyString。在MyString类中,我们添加了一个方法FindVowels()。这时候,我们就可以说MyString实现了VowelsFinder接口。可以看出,这和其他语言例如Java相差很大,对于java而言,一个类必须要使用implements关键字明确地表明它实现了哪个接口。但是在Go里面不必这样搞,对于Go而言,如果一个类包含了接口中声明的所有方法,那么就认为这个类隐式地实现了此接口。
在28行 v = name中,我们把MyString类型的变量name赋值给VowelsFinder类型的变量v。这样做也是可以的,因为MyString实现了VowelsFinder接口。在v.FindVowels()这行代码中,我们又在MyString类型上调用了FindVowels方法,同时打印出字符串"Sam Anderson"中的所有元音字母。程序的输出如下:
Vowels are [a e o]
到目前为止,我们已经创建并实现了第一个接口。
使用接口
上面的案例教会我们如何创建并实现一个接口,但是它并没有真正地告诉我们应该如何使用接口,相比较于v.FindVowels(),如果我们使用name.FindVowels()的话,也是可以正常运行的,这样的话,我们就没有用到接口。
现在,我们就来看下实际工作中,我们应该如何使用接口。
我们写一个简单的程序,这个程序会基于每个员工的工资来计算公司的总花销。为简单起见,我们假定所有的花费都是以美元计。
package main
import (
"fmt"
)
type SalaryCalculator interface {
CalculateSalary() int
}
type Permanent struct {
empId int
basicpay int
pf int
}
type Contract struct {
empId int
basicpay int
}
//salary of permanent employee is the sum of basic pay and pf
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {
return c.basicpay
}
/*
total expense is calculated by iterating through the SalaryCalculator slice and summing
the salaries of the individual employees
*/
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total Expense Per Month $%d", expense)
}
func main() {
pemp1 := Permanent{
empId: 1,
basicpay: 5000,
pf: 20,
}
pemp2 := Permanent{
empId: 2,
basicpay: 6000,
pf: 30,
}
cemp1 := Contract{
empId: 3,
basicpay: 3000,
}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
totalExpense(employees)
}
在上面程序的第七行中,我们声明了SalaryCalculator接口,这个接口中有一个方法:CalculateSalary() int。
我们公司中有俩种员工:正式工permanent和合同工contract。正式工的工资是基础工资basicpay和公积金ProvidentFund的合计。而合同工只有基础工资basicpay。这些都在它们各自的CalculateSalary方法里面实现。permanent和contract都实现了SalaryCalculator接口。
totalExpense函数充分展示了接口的好处。该方法接收了一个[]SalaryCalculator数组作为参数。在程序的第59行 employees := []SalaryCalculator{pemp1, pemp2, cemp1}中,我们传递了一个包含着Permanant和Contract类型的数组给totalExpense函数。totalExpense函数通过调用每个类型各自的CalculateSalary方法计算出各自的花费。程序的输出如下:
Total Expense Per Month $14050
这样做的最大好处就是,它可以扩展任何新的员工类型,而无需更改totalExpense方法的任何代码。比如说,公司新增加了一个员工类型Freelancer,这个新类型的员工有着不同的工资结构。这时,我们可以在不改变totalExpense函数代码的情况下,支持新增的员工类型。只要Freelancer实现了SalaryCalculator接口即可。
我们修改一下这个程序并添加一个新的Freelancer员工类型。Freelancer的工资计算是:每小时的产出率以及总的工作时长。
package main
import (
"fmt"
)
type SalaryCalculator interface {
CalculateSalary() int
}
type Permanent struct {
empId int
basicpay int
pf int
}
type Contract struct {
empId int
basicpay int
}
type Freelancer struct {
empId int
ratePerHour int
totalHours int
}
//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {
return c.basicpay
}
//salary of freelancer
func (f Freelancer) CalculateSalary() int {
return f.ratePerHour * f.totalHours
}
/*
total expense is calculated by iterating through the SalaryCalculator slice and summing
the salaries of the individual employees
*/
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total Expense Per Month $%d", expense)
}
func main() {
pemp1 := Permanent{
empId: 1,
basicpay: 5000,
pf: 20,
}
pemp2 := Permanent{
empId: 2,
basicpay: 6000,
pf: 30,
}
cemp1 := Contract{
empId: 3,
basicpay: 3000,
}
freelancer1 := Freelancer{
empId: 4,
ratePerHour: 70,
totalHours: 120,
}
freelancer2 := Freelancer{
empId: 5,
ratePerHour: 100,
totalHours: 100,
}
employees := []SalaryCalculator{pemp1, pemp2, cemp1, freelancer1, freelancer2}
totalExpense(employees)
}
我们在程序的22行增加了一个Freelancer结构体,并在第39行声明了CalculateSalary()方法。totalExpense方法不需要做出任何改动,因为Freelancer结构体同样实现了SalaryCalculator接口。我们在main方法中,添加了几个Freelancer员工,程序的输出如下:
Total Expense Per Month $32450
接口的内部表示
我们可以认为接口的内部实现是由一个二元组表示的。(type,value)。type是接口的具体类型,value是具体类型的值。
我们来写段程序理解下:
package main
import (
"fmt"
)
type Worker interface {
Work()
}
type Person struct {
name string
}
func (p Person) Work() {
fmt.Println(p.name, "is working")
}
func describe(w Worker) {
fmt.Printf("Interface type %T value %v\n", w, w)
}
func main() {
p := Person{
name: "Naveen",
}
var w Worker = p
describe(w)
w.Work()
}
接口Worker有一个名为work的方法,同时结构体Person实现了此接口。在程序的第27行,我们把Persoon类型的变量p赋值给Worker类型的变量w。
那么,w现在的具体类型就是Person,它有一个name字段的值为“Naveen”。第17行的describe()函数打印出了此接口的值以及具体类型。程序的输出为:
Interface type main.Person value {Naveen}
Naveen is working
我们将在接下来的小节中中讨论如何提取接口的底层值。
空接口
一个方法也没有的接口,我们称之为空接口。它通过interface{}来表示。由于空接口中没有任何方法,那也就意味着,所有的类型都试下了空接口。
package main
import (
"fmt"
)
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
func main() {
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct {
name string
}{
name: "Naveen R",
}
describe(strt)
}
在上面程序的第七行中,describe(i interface{})函数可以接收一个空接口作为形参,因此,所有的类型都可以作为参数传递给describe函数。
我们分别把String、int、struct传递给describe函数,程序的输出如下:
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
类型断言
类型断言可用于提取接口的底层值。
i.(T)语法即可获取到接口i的底层值,它的具体类型是T。
代码胜过千言万语,我们写一段类型断言的代码吧。
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int) //get the underlying int value from i
fmt.Println(s)
}
func main() {
var s interface{} = 56
assert(s)
}
s的具体类型是int。我们使用i.(int)语法来获取i的底层int值。程序打印的值为:56.
那么,如果上面程序中,具体的类型如果不是int的话,会发生什么呢?我们来探究一下。
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
func main() {
var s interface{} = "Steven Paul"
assert(s)
}
在上面的程序中,我们传递一个具体类型为string的变量s给assert函数,assert函数试图从s中提取int值。此时程序会提示信息:
panic: interface conversion: interface {} is string, not int.
要解决上面的问题,我们可以使用语法:
v, ok :=i.(T)
即:
如果i的具体类型是T的话,v会得到i的底层值,OK的值为true。
如果i的具体类型不是T的话,ok的值为false,v的值为T类型的零值,程序不会报错。
package main
import (
"fmt"
)
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
当我们把“Steven Paul”传递给assert函数时,ok的值为false,因为i的具体类型不是int,此时v的值为int的零值,
即:0。程序的输出如下:
56 true
0 false
类型匹配
类型匹配可用于将一个接口的类型和多个不同的类型进行比较。它类似于switch case。区别在于,在switch case中比较的是值,而在这里比较的是类型。
类型匹配的语法和类型断言的语法比较相近。其语法为: i.(type)。我们来看个程序。
package main
import (
"fmt"
)
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I am a string and my value is %s\n", i.(string))
case int:
fmt.Printf("I am an int and my value is %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen")
findType(77)
findType(89.98)
}
在上面的程序中,switch i.(type)指定了一个类型switch。每一个case语句都会把i的具体类型和某个类型进行比较,如果匹配的话,就会打印对应的语句。程序的输出如下:
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type
由于89.98的数据类型是float64,因此它不符合任何的case,因此,就会打印出“Unkown type”。
另外,我们还能把一个类和接口进行比较。若我们有一个类,如果此类实现了某个接口,那么我们就可以让该类型和它所实现的接口进行比较。
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
p := Person{
name: "Naveen R",
age: 25,
}
findType(p)
}
在上面的程序中,Person结构体实现了Describer接口,在第9行的case语句中,v和接口类型Describer进行比较。p实现了Describer接口,因此,这个case语句满足条件,故而Describe()方法会被调用。程序输出:
unknown type
Naveen R is 25 years old
到此为止,我们已经讲述完了接口的第一部分,下面我们将继续讲解接口的第二部分。
感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!
备注
本文系翻译之作原文博客地址
网友评论