什么是接口?
在 Go 中,接口是一组方法签名。当一个类型为接口中的所有方法提供定义时,就说它实现了该接口。
它与 OOP 世界非常相似。接口指定类型应该具有哪些方法,类型决定如何实现这些方法。
例如*,WashingMachine* 可以是具[方法签名 Clean( ) 和 Drying() 的接口。任何为 Clean() 和 Drying() 方法提供定义的类型都被称为实现 WashingMachine 接口。
声明和实现接口
让我们直接深入研究创建接口并实现它的程序。
go
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
创建了一个类型。
在15 行中。我们将FindVowels() []rune
方法添加到接收者MyString
类型中。
就可以说现在MyString
实现该接口VowelsFinder
。
这与 Java 等其他语言有很大不同,在 Java 中,类必须使用关键字显式声明它实现了接口implements
。
这在 Go 中是不需要的,如果类型包含接口中声明的所有方法,则 Go 接口会隐式实现。
在第 28 行中,我们将MyString
类型分配给 v 。这是可能的,因为MyString
实现了接口。 所以可以调用 v.FindVowels()
。
该程序输出
Vowels are [a e o]
恭喜!您已经创建并实现了您的第一个接口。
接口的实际使用
上面的例子教会了我们如何创建和实现接口,但并没有真正展示接口的实际用途。相反v.FindVowels()
,如果我们name.FindVowels()
在上面的程序中使用,它也会起作用,并且接口将没有用处。
现在让我们看一下接口的实际使用。
我们将编写一个简单的程序,根据员工的个人工资计算公司的总费用。为简洁起见,我们假设所有费用均以美元为单位。
go
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()
。
我们公司有两种员工,用Permanent
和Contract
的结构体来定义。 正式员工的工资是基本工资和公积金之和,而合同员工的工资只是基本工资。
两个结构体分别都实现了该接口。
第 36 行中声明的totalExpense
函数表达了接口的美感。此方法将 []SalaryCalculator作为参数传入方法中。
在第 59 行中,我们将一个包含两者的类型切片传递给函数。该函数通过调用相应类型的方法来计算费用。
程序输出
Total Expense Per Month $14050
这样做的最大优点是可以扩展到任何新员工类型,而无需更改任何代码。假设公司增加了一种具有不同薪酬结构的新型员工。这可以在 slice 参数中传递,甚至不需要对函数进行任何代码更改。此方法将执行它应该执行的操作,也将实现接口
让我们修改这个程序并添加新Freelancer
员工。自由职业者的工资是每小时工资和总工作时间的乘积。
go
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)
}
我们在第 1 行添加了Freelancer
结构体。并在第 22 行声明该方法CalculateSalary
。
由于Freelancer
struct 也实现了该接口,因此该方法中不需要更改其他代码。我们在该方法中添加了几个Freelancer
员工。该程序打印,
Total Expense Per Month $32450
接口内部表示
(type, value)
接口可以被认为是由元组在内部表示的。type
是接口的基础具体类型,value是保存具体类型的值。
让我们编写一个程序来更好地理解。
go
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
的变量赋值给 Worker
的具体类型,它包含一个name字段。
该程序输出
Interface type main.Person value {Naveen}
Naveen is working
我们将在接下来的章节中详细讨论如何提取接口的潜在价值。
空接口
**具有零个方法的接口称为空接口。它表示为interface{}
。**由于空接口有零个方法,因此所有类型都实现空接口。
go
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{})
函数采用一个空接口作为参数,因此可以传递任何类型。
我们将string
,int
和struct
给describe
传递。该程序打印,
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
类型断言
类型断言用于提取接口的底层值。
**i.(T)**是用于获取i
具体类型为T
的接口的基础值的语法。
一个程序抵得上一千个字😀。让我们为类型断言编写一个。
go
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 值
该程序打印
56
如果上面程序中的具体类型不是 int 会发生什么?好吧,让我们找出答案。
go
package main
import (
"fmt"
)
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
func main() {
var s interface{} = "Steven Paul"
assert(s)
}
在上面的程序中,我们将s
具体类型传递string
给assert
尝试从中提取 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的零值,并且程序不会死机。
go
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,因为 T
的具体类型不是int
且v
其值为 0,即int
的零值。该程序将打印,
56 true
0 false
类型开关
类型开关用于将接口的具体类型与各种 case 语句中指定的多种类型进行比较。它类似于switch。唯一的区别是 case 指定类型而不是像普通 switch 中那样指定值。
类型切换的语法类似于类型断言。在类型断言的语法中i.(T)
,T
应替换为switch的关键字type
。让我们看看下面的程序是如何工作的。
go
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)
指定了一个类型开关。每个 case 语句都将 i
的具体类型与特定类型进行比较。如果有任何 case 匹配,则打印相应的语句。
该程序输出,
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
打印在最后一行。
还可以将类型与接口进行比较。如果我们有一个类型,并且该类型实现了一个接口,则可以将该类型与它实现的接口进行比较。
为了更清楚起见,让我们编写一个程序。
go
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中是比较Describer
接口类型。p
实现Describer
,因此满足这种情况并Describe()
调用方法。
该程序打印
unknown type
Naveen R is 25 years old
使用指针接收器与值接收器实现接口
我们在上面讨论的所有示例接口都是使用值接收器实现的。也可以使用指针接收器实现接口。在使用指针接收器实现接口时需要注意一个微妙之处。
让我们使用以下程序来理解这一点。
go
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() { //implemented using value receiver
fmt.Printf("%s is %d years old\n", p.name, p.age)
}
type Address struct {
state string
country string
}
func (a *Address) Describe() { //implemented using pointer receiver
fmt.Printf("State %s Country %s", a.state, a.country)
}
func main() {
var d1 Describer
p1 := Person{"Sam", 25}
d1 = p1
d1.Describe()
p2 := Person{"James", 32}
d1 = &p2
d1.Describe()
var d2 Describer
a := Address{"Washington", "USA"}
/* compilation error if the following line is
uncommented
cannot use a (type Address) as type Describer
in assignment: Address does not implement
Describer (Describe method has pointer
receiver)
*/
//d2 = a
d2 = &a //This works since Describer interface
//is implemented by Address pointer in line 22
d2.Describe()
}
在上面的程序中,Person
结构体使用第 13 行中的值接收器实现Describer
接口。
正如我们在讨论方法时已经了解到的那样,具有值接收器的方法同时接受指针和值接收器。对任何值或可以取消引用的值调用值方法是合法的。
p1
是一个Person
类型的值,在地29行,p1
被分配给了d1
,然后在30行调用 d1.Describe()
将打印Sam is 25 years old
。
同样在第 32 行中,d1
被分配给&p2
,因此第 33 行将打印James is 32 years old
Address
结构在第 22 行中的使用指针接收器实现Describer
接口。
上面的程序中的把45 行注释取消,我们会得到编译错误main.go:42: Cannot use a (type Address) as typeDescriber in assignment: Address does notimplementDescriber(Describemethodhaspointerreceiver)
这是因为,该Describer
接口是使用第 22 行中的地址指针接收器实现的,我们正在尝试分配a
值类型,但它尚未实现该Describer
接口。这肯定会让您感到惊讶,因为我们之前了解到带有指针接收器的方法将接受指针和值接收器。那为什么代码不在第 45行工作呢?
原因是在任何已经是指针或可以获取地址的东西上调用指针值方法是合法的。存储在接口中的具体值是不可寻址的,因此编译器不可能自动获取,因此编译器不可能自动获取第 45 行中的a
地址,因此此代码失败。
第 47 行有效,因为我们将a
的地址分配给d2
。
程序的其余部分是不言自明的。该程序将打印,
Sam is 25 years old
James is 32 years old
State Washington Country USA
实现多个接口
一个类型可以实现多个接口。
让我们在下面的程序中看看这是如何完成的。
go
package main
import (
"fmt"
)
type SalaryCalculator interface {
DisplaySalary()
}
type LeaveCalculator interface {
CalculateLeavesLeft() int
}
type Employee struct {
firstName string
lastName string
basicPay int
pf int
totalLeaves int
leavesTaken int
}
func (e Employee) DisplaySalary() {
fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
return e.totalLeaves - e.leavesTaken
}
func main() {
e := Employee {
firstName: "Naveen",
lastName: "Ramanathan",
basicPay: 5000,
pf: 200,
totalLeaves: 30,
leavesTaken: 5,
}
var s SalaryCalculator = e
s.DisplaySalary()
var l LeaveCalculator = e
fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}
上面的程序有两个接口,分别在第 7 行和第 11 行中声明SalaryCalculator
和LeaveCalculator
接口
第 15 行中定义了Employee
结构体,在第 24 行和第 28 行中实现了分别实现了两个接口
在第 41 行中,我们给SalaryCalculator
接口类型分配给的e
变量,
在第 43 行中,我们将相同的e
变量分配给LeaveCalculator
类型的变量。这是可能的,因为Employee
类型同时实现接口。
该程序输出,
Naveen Ramanathan has salary $5200
Leaves left = 25
嵌入接口
虽然 go 不提供继承,但可以通过嵌入其他接口来创建新接口。
让我们看看这是如何完成的。
go
package main
import (
"fmt"
)
type SalaryCalculator interface {
DisplaySalary()
}
type LeaveCalculator interface {
CalculateLeavesLeft() int
}
type EmployeeOperations interface {
SalaryCalculator
LeaveCalculator
}
type Employee struct {
firstName string
lastName string
basicPay int
pf int
totalLeaves int
leavesTaken int
}
func (e Employee) DisplaySalary() {
fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
return e.totalLeaves - e.leavesTaken
}
func main() {
e := Employee {
firstName: "Naveen",
lastName: "Ramanathan",
basicPay: 5000,
pf: 200,
totalLeaves: 30,
leavesTaken: 5,
}
var empOp EmployeeOperations = e
empOp.DisplaySalary()
fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}
上面程序第 15 行中的 EmployeeOperations 接口是通过嵌入 SalaryCalculator 和 LeaveCalculator 接口创建的。
如果任何类型都为SalaryCalculator 和LeaveCalculator 接口中存在的方法提供方法定义,则称为实现EmployeeOperations
接口。
该Employee
结构实现接口,因为它分别在第 29 行和第 33 行中分别实现了接口定义。
在第 46 行中,把Employee
分配给EmployeeOperations
接口,然后通过这去分别调用2个方法
Naveen Ramanathan has salary $5200
Leaves left = 25
接口的零值
接口的零值为 nil。nil 接口既有其基础值,也有具体类型为 nil。
go
package main
import "fmt"
type Describer interface {
Describe()
}
func main() {
var d1 Describer
if d1 == nil {
fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
}
}
上面程序中,该程序将输出nil
d1 is nil and has type <nil> value <nil>
如果我们尝试在接口上调用一个方法,程序会崩溃,因为接口既没有底层值也没有具体类型。
go
package main
type Describer interface {
Describe()
}
func main() {
var d1 Describer
d1.Describe()
}
由于在上面的程序中是,该程序会因运行时错误 panic 而死机:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x25bb70]
goroutine 1 [running]:
main.main()
E:/goproject/structs/main.go:9 +0x10
接口就是这样。祝你今天开心。