Go Interface
本文最后更新于:2024年3月18日 凌晨
Go Interface
在 Go 语言中接口(interface)是一种类型,一种抽象的类型,包含一组方法的集合,接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
interface 是一组 method 的集合,是 duck-type prog ramming 的一种体现,接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机,不关心属性(数据),只关心行为(方法)
接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
1 2 3 4 5 type 接口类型名 interface { 方法名1 (参数列表1 )返回值列表1 方法名2 (参数列表2 )返回值列表2 … }
接口类型名
: Go 语言的接口在命名时,一般会在单词后面添加 er
,如有写操作的接口叫 Writer
,有关闭操作的接口叫 closer
等,接口名最好要能突出该接口的类型含义。
方法名
:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表,返回值列表
:参数列表和返回值列表中的参数变量名可以省略。
1 2 3 type Writer interface { Write([]byte ) error }
实现接口的条件
接口就是规定了一个需要实现的方法列表 ,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么就称它实现了这个接口。
例如定义的 Singer
接口类型,它包含一个 Sing
方法。
1 2 3 4 type Singer interface { Sing() }
因为 Singer
接口只包含一个 Sing
方法,所以只需要给 Bird
结构体添加一个 Sing
方法就可以满足 Singer
接口的要求。
1 2 3 4 func (b Bird) Sing () { fmt.Println("汪汪汪" ) }
这样就称为 Bird
实现了 Singer
接口。
面向接口编程
PHP, Java 等语言中也有接口的概念,不过在 PHP 和 Java 语言中需要显式声明一个类实现了哪些接口,在 Go 语言中使用隐式声明的方式实现接口,只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
Go 语言中的这种设计符合程序开发中抽象的一般规律,例如在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type AliPay struct { }func (a *AliPay) Pay (amount int64 ) { fmt.Printf("使用支付宝付款:%.2f元,\n" , float64 (amount/100 )) }func Checkout (obj *AliPay) { obj.Pay(100 ) }func main () { Checkout(&AliPay{}) }
1 2 3 4 5 6 7 8 type WeChat struct { }func (w *WeChat) Pay (amount int64 ) { fmt.Printf("使用微信付款:%.2f元,\n" , float64 (amount/100 )) }
在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的 Pay 方法还是微信支付的 Pay 方法。
1 2 3 4 5 6 7 8 9 10 11 func CheckoutWithZFB (obj *AliPay) { obj.Pay(100 ) }func CheckoutWithWX (obj *WeChat) { obj.Pay(100 ) }
实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用 Pay 方法时能否正常运行,这就是典型的"不关心它是什么,只关心它能做什么”的场景。
在这种场景下我们可以将具体的支付方式抽象为一个名为 Payer
的接口类型,即任何实现了 Pay
方法的都可以称为 Payer
类型。
1 2 3 4 type Payer interface { Pay(int64 ) }
此时只需要修改下原始的 Checkout
函数,它接收一个 Payer
类型的参数,这样就能够在不修改既有函数调用的基础上,支持新的支付方式。
1 2 3 4 5 6 7 8 9 10 func Checkout (obj Payer) { obj.Pay(100 ) }func main () { Checkout(&AliPay{}) Checkout(&WeChat{}) }
接口类型变量
接口类型的变量能够存储所有实现了该接口的类型变量。
例如在上面的示例中, 支付宝
和 微信
类型均实现了 Payer
接口,此时一个 Payer
类型的变量就能够接收 AliPay
和 WeChat
类型的变量。
1 2 3 4 5 6 7 var x Sayer a := AliPay{} b := WeChat{} x = a x.Pay() x = b x.Pay()
值接收者和指针接收者
结构体方法时既可以使用值接收者也可以使用指针接收者,那么对于实现接口来说使用值接收者和使用指针接收者有什么区别呢?接下来我们通过一个例子看一下其中的区别。
值接收者实现接口
使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Mover interface { Move() }type Dog struct {}func (d Dog) Move () { fmt.Println("Dog is Moving" ) }func main () { var x Mover var d1 = Dog{} x = d1 x.Move() var d2 = &Dog{} x = d2 x.Move() }
指针接收者实现接口
使用指针接收者实现接口之后,只有结构体指针类型的变量都可以赋值给该接口变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type Mover interface { Move() }type Cat struct {}func (c *Cat) Move () { fmt.Println("Cat is Moving" ) }func main () { var x Mover var c1 = &Cat{} x = c1 x.Move() var c2 = Cat{} x = c2 }
接口多实现
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现,例如狗不仅可以叫,还可以动,我们完全可以分别定义 Sayer
接口和 Mover
接口,具体代码示例如下。
1 2 3 4 5 6 7 8 9 type Sayer interface { Say() }type Mover interface { Move() }
Dog
既可以实现 Sayer
接口,也可以实现 Mover
接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Dog struct { Name string }func (d Dog) Say () { fmt.Printf("%s is Saying\n" , d.Name) }func (d Dog) Move () { fmt.Printf("%s is Moving\n" , d.Name) }
1 2 3 4 5 6 7 var d = Dog{Name: "Test" }var s Sayer = dvar m Mover = d s.Say() m.Move()
方法嵌套实现
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Machine interface { wash() dry() }type dryer struct {}func (d dryer) dry () { fmt.Println("dry" ) }type WashingMachine struct { dryer }func (w WashingMachine) wash () { fmt.Println("wash" ) }
接口组合
接口与接口之间可以通过互相嵌套形成新的接口类型,例如 Go 标准库 io
源码中就有很多接口之间互相组合的示例。
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 type Reader interface { Read(p []byte ) (n int , err error) }type Writer interface { Write(p []byte ) (n int , err error) }type Closer interface { Close() error }type ReadWriter interface { Reader Writer }type ReadCloser interface { Reader Closer }type WriteCloser interface { Writer Closer }
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
接口也可以作为结构体的一个字段,我们来看一段 Go 标准库 sort
源码中的示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Interface interface { Len() int Less(i, j int ) bool Swap(i, j int ) }type reverse struct { Interface }
通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以重写该接口的方法。
1 2 3 4 func (r reverse) Less (i, j int ) bool { return r.Interface.Less(j, i) }
Interface
类型原本的 Less
方法签名为 Less(i, j int) bool
,此处重写为 r.Interface.Less(j, i)
,即通过将索引参数交换位置实现反转。
在这个示例中还有一个需要注意的地方是 reverse
结构体本身是不可导出的(结构体类型名称首字母小写), sort.go
中通过定义一个可导出的 Reverse
函数来让使用者创建 reverse
结构体实例。
1 2 3 func Reverse (data Interface) Interface { return &reverse{data} }
这样做的目的是保证得到的 reverse
结构体中的 Interface
属性一定不为 nil
,否者 r.Interface.Less(j, i)
就会出现空指针panic
空接口
空接口的定义
空接口是指没有定义任何方法的接口类型,因此任何类型都可以视为实现了空接口,也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" type Any interface {}type Dog struct {}func main () { var x Any x = "你好" 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) x = Dog{} fmt.Printf ("type:%T value:%v\n" , x, x) }
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
空接口的应用
空接口作为函数的参数
1 2 3 4 func show (a interface {}) { fmt.Printf ("type:%T value:%v\n" , a, a) }
空接口作为 map 的值
1 2 3 4 5 6 var studentInfo = make (map [string ]interface {}) studentInfo["name" ] = "Test" studentInfo["age" ] = 18 studentInfo["married" ] = false fmt.Println (studentInfo)
接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值 之外,还需要记录这个值属于的类型 ,也就是说接口值由"类型”和"值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
我们接下来通过一个示例来加深对接口值的理解,下面的示例代码中,定义了一个Mover
接口类型和两个实现了该接口的Dog
和Car
结构体类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Mover interface { Move () }type Dog struct { Name string }func (d *Dog) Move () { fmt.Println ("Dog is Moving" ) }type Car struct { Brand string }func (c *Car) Move () { fmt.Println ("Car is Moving" ) }
1 2 var m Mover fmt.Println (m == nil )
此时,接口变量m
是接口类型的零值,也就是它的类型和值部分都是nil
,如下图所示。
此时,接口值m
的动态类型会被设置为*Dog
,动态值为结构体变量的拷贝。
然后,我们给接口变量m
赋值为一个*Car
类型的值。
这一次,接口值的动态类型为*Car
,动态值为nil
注意 :此时接口变量m
与nil
并不相等,因为它只是动态值的部分为nil
,而动态类型部分保存着对应值的类型。
接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。
1 2 3 4 5 var ( x Mover = new (Dog) y Mover = new (Car) ) fmt.Println (x == y)
但是有一种特殊情况需要特别注意,如果接口值的保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发 panic
1 2 var z interface {} = []int {1 , 2 , 3 } fmt.Println (z == z)
类型断言
接口值可能赋值为任意类型的值,但是可以借助标准库fmt
包的格式化打印获取到接口值的动态类型。
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 type Mover interface { Move () }type Dog struct { Name string }func (d *Dog) Move () { fmt.Println ("Dog is Moving" ) }type Car struct { Brand string }func (c *Car) Move () { fmt.Println ("Car is Moving" ) }func main () { var m Mover m = &Dog{Name: "旺财" } fmt.Printf ("%T\n" , m) m = new (Car) fmt.Printf ("%T\n" , m) }
fmt
包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。
想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。
var
:表示接口类型的变量。
Type
:表示断言x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
1 2 3 4 5 6 7 8 var n Mover = &Dog{Name: "DogName" } v, ok := n.(*Dog)if ok { fmt.Println ("类型断言成功" ) v.Name = "Test" } else { fmt.Println ("类型断言失败" ) }
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 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 is %v\n" , v) case bool : fmt.Printf ("x is a bool is %v\n" , v) default : fmt.Println ("unsupport type!" ) } }