1、基本语法
在Golang 中,interface是一组 method 的集合,是 duck-type programming 的一种体现。不关心属性(数据),只关心行为(方法)。具体使用中你可以自定义自己的 struct,并提供特定的 method 就可以把它当成 interface 来使用。
if something looks like a duck, swims like a duck and quacks like a duck then it’s probably a duck.
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名称 interface { 方法名1(参数列表1) 返回值列表1 方法名2(参数列表2) 返回值列表2 … }
其中:
- 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口交Stringer等。接口名最好要能突出该接口的含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
2、实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
我们来定义一个 Animal 接口:
type Animal interface{
sleep()
}
定义 Dog 和 Cat 两个结构体:
type Dog struct {
name string
}
type Cat struct {
name string
}
因为Animal 接口里面只有一个 sleep 方法,所以我们只需要给 Dog 和 Cat 类分别实现 sleep 方法就可以实现 Animal 接口了。
func (d Dog) sleep() {
fmt.Printf("%s,正在那边熟睡", d.name)
}
func (c Cat) sleep() {
fmt.Printf("%s,正在那边熟睡", c.name)
}
接口的实现就是这么简单,只要实现了接口中的方法,就实现了这个接口。
3、接口类型变量
那实现了接口有什么用呢?
接口类型变量能够存储所有实现了该接口的实例。例如上面的示例中,Animal类型的变量能够存储Dog 和 Cat 类型的变量。
func foo(animal Animal) {
animal.sleep()
}
func main() {
var a Animal
a = Dog{name: "川普"}
a.sleep()
foo(a)
a = Cat{name: "拜登"}
a.sleep()
foo(a)
}
4、值和指针接受者实现接口
使用值接收者实现接口和使用指针接收者实现接口有什么区别呐?接下来我们通过一个例子看一下其中的区别。
4.1、值接收者实现接口
package main
import "fmt"
type Animal interface {
sleep()
}
type Dog struct {
name string
}
func (d Dog) sleep() {
fmt.Printf("%s,正在那边熟睡", d.name)
}
func main() {
var a Animal
var UsaPresident = Dog{name: "川普"}
a = UsaPresident //a可以接收Dog类型
a.sleep() // 将Dog类型UsaPresident拷贝给接收者方法sleep的d,然后执行sleep方法
a = &UsaPresident //a可以接收*Dog类型
// 将*Dog类型UsaPresident取值操作后拷贝给接收者方法sleep的d,然后执行sleep方法
a.sleep()
}
从上面的代码中我们可以发现,使用值接收者实现接口后,不管是Dog结构体对象,还是接口体指针对象都可以赋值给该接口变量。因为Go语言中有对指针变量求值的语法糖,Dog指针a内部会自动求职 川普 结构体对象,然后拷贝赋值。
4.2、指针接收者实现接口
同样的代码我们再来测试一下使用指针接收者有什么区别:
package main
import "fmt"
type Animal interface {
sleep()
}
type Dog struct {
name string
}
func (d *Dog) sleep() { // 指针接收者实现接口
fmt.Printf("%s,正在那边熟睡", d.name)
}
func main() {
var a Animal
var UsaPresident = Dog{name: "川普"}
a = UsaPresident //a不可以接收Dog类型
a = &UsaPresident //a可以接收*Dog类型
// 将*Dog类型UsaPresident取值操作后拷贝给接收者方法sleep的d,然后执行sleep方法
a.sleep()
}
此时实现Animal的接口是 Dog 类型,所以不能给 a 传入 Dog 类型的 UsaPresident,此时a 只能存储Dog类型的值,即:&UsaPresident。
5、类型与接口的关系
5.1、一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如,狗可以跑,也可以叫。我们就分别定义Runner接口和Sayer接口。
// Sayer接口
type Sayer interface {
say()
}
// Runner接口
type Runner interface {
run()
}
Dog 既可以实现sleep接口,也可以实现Run接口。
package main
import "fmt"
// Sayer接口
type Sayer interface {
say()
}
// Runner接口
type Runner interface {
run()
}
type Dog struct {
name string
}
func (d Dog) say() {
fmt.Printf("%s汪汪汪叫\n", d.name)
}
func (d Dog) run() {
fmt.Printf("%s吐舌头跑\n", d.name)
}
func main() {
var s Sayer
var r Runner
d := Dog{name: "旺财"}
s = d
s.say() // 实现 Sayer接口
r = d
r.run() // 实现 Runner 接口
}
5.2、多个类实现同一接口
Go语言中不同的类型还可以实现同一接口,首先我们定义一个 Runner 接口,它要求必须有一个 run 方法:
// Runner 接口
type Runner interface {
run()
}
例如狗可以跑,汽车也可以跑,可以使用如下代码实现这个关系:
// Runner 接口
type Runner interface {
run()
}
type Car struct {
brand string
}
type Dog struct {
name string
}
func (d Dog) run() {
fmt.Printf("%s正在吐舌头跑\n", d.name)
}
func (c Car) run() {
fmt.Printf("%s正在飞速行驶\n", c.brand)
}
这个时候我们在代码中就可以把狗和汽车当成一个会动物的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的 run 方法就可以了。
// Runner 接口
type Runner interface {
run()
}
type Car struct {
brand string
}
type Dog struct {
name string
}
func (d Dog) run() {
fmt.Printf("%s正在吐舌头跑\n", d.name)
}
func (c Car) run() {
fmt.Printf("%s正在飞速行驶\n", c.brand)
}
6、类型嵌套
一个类型(struct)必须实现了接口中的所有方法才能称为实现了该接口。
// Runner 接口
type Animal interface {
sleep()
run()
}
type Dog struct {
name string
}
// 只实现一个不行,不算是对接口的继承
func (d Dog) run() {
fmt.Printf("%s正在吐舌头跑\n", d.name)
}
//func (d Dog) sleep() {
// fmt.Printf("%s正在侧卧着睡觉\n", d.name)
//}
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体实现。
package main
import (
"fmt"
)
type WashingMachine interface {
wash()
dry()
}
// 甩干机
type Dryer struct {
brand string
}
// 实现WashingMachine接口的dry()方法
func (d Dryer) dry() {
fmt.Println("甩干衣服")
}
// 海尔洗衣机
type Haier struct {
name string
Dryer
}
// 实现WashingMachine接口的wash()方法
func (h Haier) wash() {
fmt.Println("洗衣服")
}
func main() {
var wm WashingMachine
wm = Haier{"海尔", Dryer{"西门子"}}
wm.wash()
wm.dry()
}
7、接口嵌套
接口与接口间可以通过嵌套创造出新的接口
package main
import "fmt"
// Sleep 接口
type Sleep interface {
sleep()
}
// Runner接口
type Runner interface {
run()
}
//Animal接口,嵌套了Sleep、Runner接口
type Animal interface {
Sleep
Runner
}
type Dog struct {
name string
}
func (d *Dog) run() {
fmt.Printf("%s正在吐舌头跑\n", d.name)
}
func (d *Dog) sleep() {
fmt.Printf("%s正在侧翻睡\n", d.name)
}
func main() {
var a Animal
dog := &Dog{name: "旺财"}
a = dog
a.sleep() // 接口需要传入指针
a.run()
}
8、空接口
8.1、空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任何类型的变量。
func main() {
var x interface{}
s := "hello sgg"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
x = 100
fmt.Printf("type:%T value:%v\n", x, x)
x = true
fmt.Printf("type:%T value:%v\n", x, x)
}
8.2、空接口的应用
8.2.1、空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
8.2.2、空接口作为map的值
使用空接口实现可以保存任意值的字典。
func main() {
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "sgg"
studentInfo["age"] = 22
studentInfo["married"] = true
fmt.Println(studentInfo)
}
8.2.3、类型断言
一个接口的值(简称接口值)是由 一个具体类型 和 具体类型的值 两部分组成的。这两部分分别称为接口的 动态类型 和 动态值。
想要判断空接口中的值,这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
- x: 表示类型为interface{}的变量。
- T: 表示断言x 可能是的类型。
该语法返回两个参数,第一个参数是 x 转换为 T 类型后的变量,第二个值是一个布尔值,若为 true则表示断言成功,为 fasle 则表示断言失败。
举个例子:
func main() {
var x interface{}
x = "hello world"
v, ok := x.(string) // 判断x的类型,v是值,ok:true 或者 false
if ok {
fmt.Println(v) // hello world
} else {
fmt.Println("类型断言失败")
}
}
上面的例子中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现。
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string类型,value is %v\n", v)
case int:
fmt.Printf("x is a int类型, value is %v\n", v)
case bool:
fmt.Printf("x is a bool类型,value is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
func main() {
justifyType(12)
justifyType(true)
justifyType("hello sbh")
}
因为空接口可以存储任意类型的值的特点,所以空接口在Go语言中使用十分广泛。
关于接口需要注意的是,只有当两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。