什么是接口
在Go语言中,接口就是方法签名的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。
接口的声明与实现
如下所示,在Go中,如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了Go接口。
package main
import "fmt"
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, c := range ms {
if c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' {
vowels = append(vowels, c)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson")
var v VowelsFinder
v = name
fmt.Printf("Vowels are %c\n", v.FindVowels()) // Vowels are [a e o]
}
接口的实际用途
通过一个例子来说明接口的实际应用场景:根据公司员工的个人薪资,计算公司的总支出。
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
}
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.pf
}
func (c Contract) CalculateSalary() int {
return c.basicpay
}
// 这里体现了接口的妙用,totalExpense 可以扩展新的员工类型,假设公司增加了一种新的员工类型
// Freelancer,它有着不同的薪资结构。Freelancer 只需传递到 totalExpense 的切片参数中,
// 无需 totalExpense 方法本身进行修改。只要 Freelancer 也实现了 SalaryCalculator 接口,
// totalExpense 就能够实现其功能,这样,totalExpense 函数中就实现了多态。
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("Total expense per month $%d\n", expense)
}
func main() {
pemp1 := Permanent{1, 5000, 20}
pemp2 := Permanent{2, 6000, 30}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
totalExpense(employees) // Total expense per month $14050
}
接口的内部表示
我们可以把接口看作内部的一个元组(type, value)
。type
是接口底层的具体类型,而value
是具体类型的值。举个例子:
package main
import "fmt"
type Tester interface {
Test()
}
type MyFloat float64
func (m MyFloat) Test() {
fmt.Println(m)
}
func describe(t Tester) {
fmt.Printf("Interface type %T value %v\n", t, t)
}
func main() {
var t Tester
f := MyFloat(89.7)
t = f
describe(t) // Interface type main.MyFloat value 89.7
t.Test() // 89.7
}
空接口
没有包含方法的接口称为空接口。空接口表示为interface{}
。由于空接口没有方法,因此所有类型都实现了空接口。
类型断言
类型断言用于提取接口的底层值。在语法i.(T)
中,接口i
的具体类型是T
,该语法用于获得接口的底层值。举个例子:
package main
import "fmt"
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
func main() {
var s interface{} = 56 // s 的具体类型是 int
assert(s) // 56
}
在上面的例子中,如果具体类型不是int,assert函数将会报错,所以合理的做法应该是:v, ok := i.(T)
,这样,如果i
的具体类型是T
,那么v
赋值为i
的底层值,而ok
赋值为true
;如果i
的具体类型不是T
,那么ok
赋值为false
,v
赋值为T
类型的零值。
类型选择(Type Switch)
类型选择用于将接口的具体类型与很多case语句所指定的类型进行比较。它与一般的switch语句类似,唯一的区别在于类型选择指定的是类型,而一般的switch指定的是值。
类型选择的语法类似于类型断言。类型断言的语法是i.(T)
,而对于类型选择,类型T
由关键字type
代替,举个例子:
package main
import "fmt"
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I'm a string and my value is %s\n", i.(string))
case int:
fmt.Printf("I'm an int and my value is %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen") // I'm a string and my value is Naveen
findType(77) // I'm an int and my value is 77
findType(89.89) // Unknown 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\n", 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") // Unknown type
p := Person{
name: "Naveen R",
age: 25,
}
findType(p) // Naveen R is 25 years old
}
实现接口:指针接收者与值接收者
我们可以使用值接收者或者指针接收者来实现接口,但在使用指针接收者实现接口时,有一些细节需要注意,如下代码:
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() { // 使用值接收者实现了 Describer 接口
fmt.Printf("%s is %d years old\n", p.name, p.age)
}
type Address struct {
state string
country string
}
func (a *Address) Describe() { // 使用指针接收者实现了 Describer 接口
fmt.Printf("State %s Country %s\n", a.state, a.country)
}
func main() {
var d1 Describer
p1 := Person{"Sam", 25}
d1 = p1
d1.Describe() // Sam is 25 years old
p2 := Person{"James", 32}
d1 = &p2
d1.Describe() // James is 32 years old
var d2 Describer
a := Address{"Washington", "USA"}
// 如果取消下面 d2 = a 这行注释会导致编译报错:因为编译器无法自动获取 a 的地址
// cannot use a (type Address) as type Describer in assignment: Address
// does not implement Describer (Describe method has pointer receiver)
// d2 = a
d2 = &a
d2.Describe()
}
总结:对于使用指针接收者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。而对于接口不同,使用值接收者实现接口的对象,可以把它的值或者指向它的指针赋值给接口变量;而使用指针接收者实现接口的对象,则必须把它的指针赋值给接口变量。
实现多个接口
假设我们声明了两个接口:SalaryCalculator
和LeaveCalculator
,而结构体Employee
同时实现了SalaryCalculator
和LeaveCalculator
接口的方法,那么Employee
就实现了SalaryCalculator
和LeaveCalculator
两个接口。而结构体Employee
变量可以赋值给SalaryCalculator
类型的接口变量,也可以赋值给LeaveCalculator
类型的接口变量。
接口的嵌套
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\n", 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() // Naveen Ramanathan has salary $5200
fmt.Printf("Leaves left = %d\n", empOp.CalculateLeavesLeft()) // Leaves left = 25
}
接口的零值
接口的零值是nil
。对于值为nil
的接口,其底层值和具体类型都为nil
,当试图调用它的方法时,程序会产生panic异常。
package main
import "fmt"
type Describer interface {
Describe()
}
func main() {
var d Describer
if d == nil {
// Output: d is nil and has type <nil> value <nil>
fmt.Printf("d is nil and has type %T value %v\n", d, d)
}
}
reference:
https://studygolang.com/articles/12266
https://studygolang.com/articles/12325