原文

接口是什么?

在面向对象的世界中,接口的意思是:“接口定义了对象的行为”。它只表明对象应该能做什么,而具体怎么做则由对象内部实现。

Go语言中,接口是方法的集合,若某个类型实现了接口中定义的所有方法,则可以说这个类型实现了这个接口。用OOP的形式来说就是: 接口表明了类型应该有哪些方法,而类型则决定如何实现那些方法

比如,WashingMachine这个接口中可以定义Cleaning()Drying()方法,任何类型实现了这2个方法都可以说是实现了WashingMachine接口。

声明和实现接口

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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())

}

第8行我们创建了一个接口叫做VowelsFinder,并且声明了一个FindVowels() []rune方法,接下来创建了一个MyString类型。

第15行我们给MyString类型添加了FindVowels() []rune方法,现在我们可以说MyString实现了接口VowelsFinder。这里和Java这种需要使用关键字implements显式声明实现某接口不同,GO中并不需要这个关键字,而是如果某个类型全部定义了接口中的方法,则隐式的实现了这个接口。

第28行,我们将MyString类型的变量name转换成了VowelsFinder接口类型,这是可以的因为MyString实现了VowelsFinder接口。接下来通过v.FindVowels()方法来打印”Sam Anderson”中所有的元音字母,程序的输出是Vowels are [a e o]

恭喜!你创造并实现了自己的第一个接口。

接口实践

上面的程序展示了如何创建和实现一个接口,但并没什么实际用处。如果我们使用name.FindVowels()替代v.FindVowels()程序依然可以正常执行。

所以现在我们来看看在实践中如何使用接口。

我们写一个程序来计算公司员工的总公司,为了简洁我们假定都是用美元发工资。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 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 though 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{1, 5000, 20}
pemp2 := Permanent{2, 6000, 30}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
totalExpense(employees)

}

第7行声明了SalaryCalculator接口类型并且包含一个CalculateSalary() int方法。

公司中有2种雇员,PermanentContract。Permanent的工资由basicpaypf构成,而Contract仅有basicpay。各自的CalculateSalary方法在23、28行实现。Permanent和Contract都实现了SalaryCalculator接口。

36行定义的totalExpense方法是一种十分漂亮的接口使用案例,这个方法参数是一个由SalaryCalculator接口类型组成的切片。49行我们把合同工和临时工这2种类型数据组成了一个切片传递给了totalExpense方法,而totalExpense方法通过调用各自的CalculateSalary函数来计算相应的花费。

totalExpense最大的优势就是可以扩展任意多的类型而不用修改本身代码。假定公司现在又有一个Freelancer类型雇员而且有不同的薪资构成,只要它实现了SalaryCalculator接口就可以放到切片参数中。

程序输出为Total Expense Per Month $14050.

接口的内部构造

一个接口内部可以认为是由元组(type, value)构成,type记录接口的具体类型,value则记录接口具体类型的值。

通过代码可以更好的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
)

type Test interface {
Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {
fmt.Println(m)
}

func describe(t Test) {
fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {
var t Test
f := MyFloat(89.7)
t = f
describe(t)
t.Tester()
}

接口Test拥有一个Tester()方法,并且MyFloat实现了它,第24行我们转换MyFloat类型的变量fTest类型并赋值给t。现在具体类型是MyFloat并且值为89.7describe函数输出了接口的真实类型和值,输出如下:

1
2
Interface type main.MyFloat value 89.7  
89.7

空接口

如果一个接口没有任何方法,则被称为空接口。用interface{}来表示。因为空接口没有方法,所以任何接口都实现了空接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
}

在第7行,describe(i interface{})函数使用一个空接口作为参数,所以可以接受任何类型数据。我们分别传递intstringstruct类型数据到函数中,输出如下:

1
2
3
Type = string, value = Hello World  
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}

类型断言

类型断言被用于提取接口的隐含值(underlying value)。

i.(T) 用来获取接口的隐含值,接口i的具体类型为T

一码胜千言,我们来写个类型断言的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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所以我们在第8行使用语法i.(int)来获取i的隐藏整数值,程序输出为56

那么如果真实类型不是int呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函数,并且尝试从中获取一个整数的值,程序将报错panic: interface conversion: interface {} is string, not int

解决这个问题可以使用语法

1
v, ok := i.(T)

如果i的类型是T,那么v将得到i的隐含值并且ok为true。否则ok为false并且v为T类型的零值, 程序不会报错!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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将得到0因为int类型的零值是0。(roy注:把assert函数中的int改成string则将返回空字符串,零值意思应该是默认初始值。)

1
2
56 true  
0 false

类型判断

(roy注:原文是Type Switch,内容就是使用switch语法判断某接口的类型。)

Type Switch用来比较某个接口的具体类型是否在多个指定的case分支中,和switch-case语法类似,区别就是case后的类型不再是普通的值。

语法上和类型断言也很像,在i.(T)语法中,把T用type关键字替换掉就成了type switch。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
}

第8行switch i.(type)指定了一个type switch,每一个case语句都用来比较i的实际类型和后面指定的类型。如果有匹配的则进行相应的输出。执行结果如下:

1
2
3
I am a string and my value is Naveen  
I am an int and my value is 77
Unknown type

88.98是float64类型不匹配任何一项,所以最后输出Unknow type

对某类型和接口进行比较也是可以的,如果我们定义了某个类型并且这个实现了某个接口,那么就可以对这2者进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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接口。19行,变量vDescriber接口类型进行比较。p实现了Describer所以满足第一个case判断,Describe函数也被调用。输出如下:

1
2
unknown type  
Naveen R is 25 years old

这篇文章至此结束,剩下的部分将在第二篇文章中讨论。