接口是什么?
接口是属于面向对象编程的范畴,接口描述了对象(类)的行为 。接口仅仅是一个描述,描述对象(或类)应该实现哪些行为。但是具体如何实现接口并不关注,而是由具体对象(或类)决定。
Golang中的接口其实就是一个方法签名的集合。如果某个类型实现了某个接口中的所有方法,那么我们说该类型实现了这个接口。这其实跟面向对象编程中接口的定义与实现非常类似。接口描述一个类型应该实现的方法集合,但是并不决定具体如何实现,具体的实现还是由具体的类型决定的。
举个例子,WashingMachine(洗衣机)可以定义为包含Cleaning()
和Drying()
方法签名的接口。而所有实现了这两个方法的类型都算是实现了这个接口。
声明一个接口并实现该接口
下面我们声明一个接口,并创建一个类型来实现该接口。
Copy package main
import "fmt"
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
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 // 由于MyString实现了该VowelsFinder接口,因此可赋值
fmt.Printf("Vowels are %c", v.FindVowels())
}
声明接口的语法如下:
Copy type InterfaceName interface {
MethodSignature1
MethodSignature2
...
}
这里我们声明了一个接口VowelsFinder
,该接口包含一个方法签名:FindVowels() []rune
。然后创建了一个类似MyString
。
我们给该类型绑定了一个方法 FindVowels() []rune
,这时我们便说 MyString
实现了接口 VowelsFinder
,这跟Java确实不一样,在Java中需要显示的使用关键字 implements
来声明某个类实现了某个接口。Golang中不需要这种显示声明,只要某个类型实现了某个接口中的所有方法,那么该类型就隐式的实现了该接口。
前面程序中我们将MyString
类型的变量name
赋值给了VowelsFinder
类型的变量v
。由于MyString
实现了接口VowelsFinder
,因此这个赋值是合法的。后面的v.FindVowels()
调用了MyString
绑定的方法来打印出了找到的所有元音字母,该程序输出:
这样我们便声明了一个接口,并实现了该接口。
接口的实际应用
前面的例子展示了如何声明一个接口并实现,但是并不实用。我们完全可以调用name.FindVowels()
,而不是调用v.FindVowels()
来完成相同的功能,也就是说完全没必要声明一个接口也能实现相同的功能。
现在我们来看一个比较实用的例子,根据公司里每个员工的薪资来计算整个公司的总开销。场景其实很简单,比如公司共有两个员工,那么这两个员工的薪资总和即为公司的总开销,为了简单我们假设薪资的单位统一为USD。
Copy 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
}
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)
}
这里我们声明了一个接口SalaryCalculator
,该接口含一个方法签名CalculateSalary() int
。接着声明了两种员工类型,Permanent
和Contract
,分别表示正式工和合同工。正式工的薪资包含basicpay
和pf
两部分,合同工薪资仅包含basicpay
。Permanent
和Contract
均实现了SalaryCalculator
接口。
函数totalExpense
接收的入参是SalaryCalculator
类型的切片,我们可以将实现了该接口的变量存入该切片,比如Permanent
类型和Contract
类型。然是在totalExpense
函数看来,它只关心入参是SalaryCalculator
类型的切片。由于所有实现了SalaryCalculator
接口的类型都绑定了CalculateSalary
方法,因此无论切片中存放的是哪个具体的类型,都可以调用该类型绑定的CalculateSalary
方法。
这种程序设计的最大好处是可扩展性,假设该公司加入了一种新的员工类型Freelancer
,即自由职业者,那么只要该类型也实现了接口SalaryCalculator
即可,这时我们就可以将该类型的变量添加到切片employees
中,而函数totalExpense
完全不需要升级。
前面的程序输出结果如下:
Copy Total Expense Per Month $14050
接口的内部表示
Golang中我们可以把接口看作是由type
和value
组成的一个元组(type, value)
。type
表示接口内部具体的数据类型,value
表示接口内部具体的数据值。
这里的元组 两个字可能不太好理解,这里多说几句,我个人觉得元组 这个中文翻译比较晦涩难懂,但是也没有更好的词替代...。大家可以这么理解元组:由多个元组成的一个组合。那么这里的元是什么意思呢?其实很简单,它类似于我们初高中学习的一元二次方程组 中的元,比如(x, y)
表示一个二维坐标系中的点,这里的x
和y
就分别是一元。因此元组这个词一般用来表示类似于(x, y)
这种多元组合。
从接口的声明语法type InterfaceName interface {}
也可以看出来,Golang中的接口本质上也是一种数据类型,既然是一种数据类型,我们就可以声明该类型的变量,比如var v VowelsFinder
,然后我们会给该变量赋值,比如前面的:
Copy name := MyString("Sam Anderson")
var v VowelsFinder
v = name
因此接口类型变量v
内部有一个具体数据类型MyString
和具体值"Sam Anderson"
。
我们实现一个例子来理解下:
Copy 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)
t.Test()
}
这里的接口Tester
含有一个方法签名Test()
。我们声明了一个该接口类型的变量var t Tester
,由于MyFloat
类型实现了该接口,因此我们将MyFloat
类型的变量f
赋值给变量t
,这时t
内部的具体类型即为MyFloat
,具体值即为89.7
。describe
函数打印出了具体类型和具体值。该程序输出结果如下:
Copy Interface type main.MyFloat value 89.7
89.7
空接口
如果一个接口没有一个方法签名,那么我们称这个接口为空接口。空接口可以简短的表示为interface {}
。由于空接口不含方法签名,因此相当于所有类型都实现了空接口。
Copy 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{})
接收的入参类型是一个空接口,因此我们可以传入任何类型的参数。 我们分别传入了string
、int
以及struct
,该程序执行结果如下:
Copy Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
类型断言
类型断言指的是断言某个接口类型变量底层具体的数据类型。语法如下:
其中T
为断言(猜测)的底层具体数据类型,i.(T)
的返回值是底层T
类型的具体数据值。我们通过一个例子来理解下:
Copy package main
import "fmt"
func assert(i interface{}) {
v := i.(int)
fmt.Println(v)
}
func main() {
var s interface{} = 56
assert(s)
}
这里接口变量s
的具体类型为int
。我们通过i.(int)
来获取接口变量i
内部的具体值。该程序输出为:56
。
前面断言成功是因为s
确实是int
类型,但是假设s
不是int
类型会怎么样呢?我们尝试如下:
Copy 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
,我们尝试断言为int
类型,或尝试获取其内部值,执行会有运行时错误:
Copy panic: interface conversion: interface {} is string, not int
可以改用如下方式解决:
如果接口变量i
的内部具体数据类型确实是T
,则v
即为内部的具体值,ok
为true
;反之,则v
被赋值为T
类型的零值,ok
为false
。并且程序不会报错。
Copy 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)
}
该程序执行结果如下:
类型 switch
类型switch
用来判断某个接口内部具体的数据类型是否是多个数据类型中的某一个,并执行匹配上的语句。语法类似于普通的switch
语句,但是普通switch语句比较的是一个值是否匹配,而类型switch
语句比较的是一个类型是否匹配。 类型匹配
的具体语法我们通过一个具体例子来看下:
Copy 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 a 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
的具体类型是否是某个类型。该程序执行结果如下:
Copy I am a string and my value is Naveen
I am a int and my value is 77
Unknown type
前面程序中的89.98
具体类型为float64
,因此未匹配到前两种情况,最后的默认case
得以执行。
我们不仅能判断某个接口变量是否匹配某个具体类型,比如前面的 int
、 string
,还能判断某个接口变量是否匹配某个接口类型。 举例如下:
Copy 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)
}
这里string
类型并未实现接口Describer
,因此执行默认case
。而结构体类型Person
实现了接口Describer
,因此case Describer
得以匹配成功,接着执行v.Describe()
。该程序执行结果如下:
Copy unknown type
Naveen R is 25 years old
实现接口:指针接收器 vs 值接收器
对于接口中声明的某个方法签名,我们既可以基于值接收器
又可基于指针接收器
实现。但是具体用哪种来实现,还是得分场景,这点在前面方法 一节已经提到过:
那么何时使用值接收器,何时又使用指针接收器呢?其实很简单,这本质上还是指针传递和值传递的问题。如果你不想通过指针来改变方法外部定义的变量,那么就使用值传递,否则就使用指针传递。但是用值传递会将变量整体拷贝一份,对于比较大的结构体变量开销比较大,这种情况下考虑到性能可以使用指针接收器。
下面我们举个例子:
Copy package main
import "fmt"
type User interface {
SetIndex(int)
}
type UserMutable struct {
Index int
}
// UserMutable DOES NOT implement User, *UserMutable implements User
func (u *UserMutable) SetIndex(index int) {
u.Index = index
}
type UserImmutable struct {
Index int
}
// UserImmutable implements User
func (u UserImmutable) SetIndex(index int) {
u.Index = index
}
func main() {
//userMutable is type *UserMutable
userMutable := &UserMutable{1}
// but not userMutable := UserMutable{1}
var user User
user = userMutable // 这时user内部具体类型是指针类型
user.SetIndex(9)
//userImmutable is type UserImmutable
userImmutable := UserImmutable{1}
// or userImmutable := &UserImmutable{1}
var user2 User
user2 = userImmutable
user2.SetIndex(9)
fmt.Println("userMutable", userMutable.Index)
// "userMutable 9"
fmt.Println("userImmutable", userImmutable.Index)
// "userImmutable 1"
}
这里我们声明了两个类型UserMutable
和UserImmutable
。从名字也能看出来,UserMutable
表示一种可变类型,我们可通过指针来改变该结构体的属性。
而UserImmutable
表示不可变类型,在其上调用绑定的方法时,由于是值接收器,因此相当于将值拷贝到了一个新的变量上,在函数内部的任何操作都是对这个新拷贝变量的操作,并不会影响原结构体变量。
前面的章节我们已经了解到,Golang提供了非常方便的语法糖来调用结构体绑定的方法,可以通过变量本身或者变量的指针来调用,Golang根据方法接收器具体类型是指针类型还是值类型来帮我们把代码转换一下。但是这点在接口这里不是完全适用。对于指针接收器定义的方法,我们仅能通过指针来访问,无法通过变量来访问。
举例来说就是前面程序中的:
Copy userMutable := &UserMutable{1}
// but not userMutable := UserMutable{1}
var user User
user = userMutable
由于Golang中的接口无法寻址内部具体的变量,因此我们不能寄托于Golang帮我们自动解析出接口内部具体变量地址。具体为什么接口无法寻址内部具体的变量,可以参考这里 和这个问答 。
前面的程序输出如下:
Copy userMutable 9
userImmutable 1
需要说明一下,这里将原文中的两节接口相关内容合成了一节。刚刚使用的例子来自这里 ,跟原文中的例子并非一个,我个人觉得更加合适就直接替换了。
实现多个接口
Golang中一个类型可以实现多个接口,我们举个例子:
Copy 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())
}
该程序声明了两个接口类型,SalaryCalculator
和LeaveCalculator
。由于Employee
结构体类型同时实现了这两个接口中所有方法,因此我们说Employee
同时实现了这两个接口。因此该结构体类型的变量e
可以分别赋值给这两种接口类型的变量,两次赋值都是合法的。我们可以这样理解,将结构体变量e
赋值给不同的接口变量相当于将该结构体变量分别看作不同的接口类型变量。这就好比我们既可以将一个智能手机
看作是通讯工具
(因为它能用来打电话
),也可以看作是mp4
(因为它能用来播放视频
),还可以看作是游戏机
(因为它能用来玩游戏
),这时我们说智能手机
这个类型同时实现了通讯工具
、mp4
以及游戏机
的接口。
接口嵌套
Golang不支持继承,但是支持接口嵌套,即将多个接口嵌套在另外一个接口内,或者说将多个接口组合在一块生成一个新接口。我们通过一个例子来理解下。
Copy 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())
}
这里接口EmployeeOperations
是由两个子接口SalaryCalculator
和LeaveCalculator
内嵌组合生成的。那么如果一个类型同时实现了SalaryCalculator
和LeaveCalculator
接口,我们就说它实现了EmployeeOperations
接口,建议暂停几秒钟,好好理解下这句话。该程序中Employee
同时实现了SalaryCalculator
和LeaveCalculator
接口,因此也就是实现了EmployeeOperations
接口。因此我们可以将Employee
类型的变量e
赋值给声明好的EmployeeOperations
接口类型变量empOp
。该程序输出如下:
Copy Naveen Ramanathan has salary $5200
Leaves left = 25
接口类型变量的零值
Golang中所有数据类型的变量都有一个默认的零值,即如果近声明了某个类型的变量,而不进行初始化,那么Golang自动帮我们初始化为对应类型的零值,接口类型也不例外。 接口变量的零值为nil
,其内部具体的类型和值也是nil
。
Copy 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)
}
}
这里的d1
仅仅进行了声明,并没有手动初始化,因此被自动初始化为零值nil
,该程序输出如下:
Copy d1 is nil and has type <nil> value <nil>
如果我们尝试在调用nil
接口变量的某个方法,那么程序会报运行时错误,因为nil
的内部具体类型和变量也是nil
。
Copy package main
type Describer interface {
Describe()
}
func main() {
var d1 Describer
d1.Describe()
}
该程序执行报如下运行时错误:
Copy panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104e15f]