接口-I
Q7nl1s admin

什么是接口?

在Go中,接口是一组方法的签名。当一个类型为接口中的所有方法提供定义时,它就被称为实现了该接口。这与OOP(面向对象程序设计)世界非常相似。接口指定了一个类型应该有哪些方法,类型决定如何实现这些方法。

例如,WashingMachine可以是一个具有Cleaning()Drying()方法签名的接口。任何为Cleaning()Drying()方法提供定义的类型都被称为实现了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行中,我们将FindVowels()[]Rune方法添加到接收器类型MyString中。现在MyString被认为是实现了VowelsFinder接口。这与其他语言(如Java)完全不同,在其他语言中,一个类必须使用 implements 关键字明确说明它实现了一个接口。这在Go中是不需要的,如果一个类型包含了接口中声明的所有方法,那么Go的接口是隐式实现的。

在第28行中,我们将MyString类型的name分配给VowelsFinder类型的v。下一行中的v.FindVowels()调用MyString类型的FindVowels方法并打印出Sam Anderson字符串中的所有元音。这个程序输出:

1
Vowels are [a e o]  

祝贺你! 你已经创建并实现了你的第一个接口。

接口的实际使用

上面的例子教会了我们如何创建和实现接口,但它并没有真正展示接口的实际用途。如果我们在上面的程序中不使用v.FindVowels(),而是使用name.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
52
53
54
55
56
57
58
59
60
61
62
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)

}

上述程序的第7行声明了SalaryCalculator接口和一个方法CalculateSalary() int

我们在公司有两种员工,即PermanentContract,由第11行和第17行的结构定义。Permanent员工的工资是basicpaypf的总和,而Contract员工的工资仅仅是basicpay。这在第23行和第28行的相应CalculateSalary方法中得到了体现。通过声明这个方法,PermanentContract结构现在都实现了SalaryCalculator接口。

在第36行声明的totalExpense函数表达了接口的魅力。这个方法以SalaryCalculator接口的一个[]SalaryCalculator切片作为参数。59行,我们向totalExpense函数传递了一个包含PermanentContract类型的片断。totalExpense函数通过调用相应类型的CalculateSalary方法来计算费用。这是在第39行完成的。

程序的输出

1
Total Expense Per Month $14050  

这样做最大的好处是,totalExpense可以扩展到任何新的雇员类型,而不需要修改代码。比方说,公司增加了一种新的雇员类型Freelancer,其工资结构不同。这个自由职业者可以直接被传递到totalExpense的切片参数中,甚至不需要对totalExpense函数进行任何一行代码的修改。这个方法将做它应该做的事情,因为Freelancer也将实现SalaryCalculator接口 :)。

让我们修改这个程序,添加新的Freelancer雇员。Freelancer的工资是每小时工资和总工作时间的乘积。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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方法。由于Freelancer结构也实现了SalaryCalculator接口,所以totalExpense方法中不需要修改其他代码。我们在主方法中添加了几个Freelancer的雇员。这个程序会打印。

1
Total Expense Per Month $32450  

接口的内部表示

一个接口可以被认为是由一个元组(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
28
29
30
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行,我们将Person类型的变量p分配给wwWorker类型的。现在w的具体类型是Person,它包含一个nameNaveen的Person。第19行的describe函数打印出接口的值和具体类型。这个程序的输出是

1
2
Interface type main.Person value {Naveen}  
Naveen is working

我们将在接下来的章节中进一步讨论如何提取接口的基本值。

空接口

一个没有方法的接口被称为空接口。它被表示为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{})函数接受一个空接口作为参数,因此可以传递任何类型。

我们在第13、15和21行分别向describe函数传递字符串、intstruct。这个程序会打印出来。

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

类型断言

类型断言是用来提取接口的底层值的。

i.(T)是一种语法,用于获取具体类型为T的接口i的底层值。

一个程序胜过一千字😀。让我们为类型断言写一个程序。

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

第12行中s的具体类型是int。我们使用第8行的语法i.(int)来获取底层的int值。这个程序打印出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)
}

在上面的程序中,我们将具体类型为字符串的s传递给assert函数,该函数试图从中提取一个int值。这个程序会出现报错信息: panic: interface conversion: interface {} is string, not int

为了解决上述问题,我们可以使用以下语法

1
v, ok := i.(T)  

如果i的具体类型是T,那么v将具有i的底层值,并且ok将为真。

如果i的具体类型不是T,那么ok将是假的,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将为假,因为i的具体类型不是intv的值为0,也就是int的零值。这个程序将打印:

1
2
56 true  
0 false

Type switch

type switch用于比较一个接口的具体类型和各种case语句中指定的多个类型。它类似于switch case。唯一的区别是案例指定了类型,而不是像普通switch那样指定了值。

类型切换的语法与类型断言相似。在Type assertion的语法i.(T)中,类型T应该被type switch的关键字type所取代。让我们看看这在下面的程序中是如何运作的。

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的具体类型与一个特定的类型进行比较。如果有任何case匹配,就会打印出相应的语句。这个程序输出。

注:如果尝试在 type switch 外部使用 i.(type) 会出现如下错误:invalid AST: use of .(type) outside type switch

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

第20行的89.98属于float64类型,与任何情况都不匹配,因此在最后一行打印了 Unknown type

也可以将一个类型与一个接口进行比较。如果我们有一个类型,并且该类型实现了一个接口,就有可能将该类型与它所实现的接口进行比较。

让我们写一个程序,以便更清楚地了解。

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行的case语句中,vDescriber接口类型相比较。p实现了Describer,因此满足这个案例,并调用了Describe()方法。

这个程序将打印:

1
2
unknown type  
Naveen R is 25 years old

接口第一部分到此结束。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View