Golang通脉之接口
阅读原文时间:2021年10月24日阅读:1

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组函数或方法的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),不关心属性(数据),只关心行为(方法),请牢记接口(interface)是一种类型。

接口与鸭子类型:

维基百科的定义:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过接口的方式完美支持鸭子类型。

而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。

动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快。

Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。

总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

Go语言的多态性:

在Java语言中,多态是通过继承和重写来体现的,而Go中的多态性就是在接口的帮助下实现的。接口可以在Go中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则该类型实现了这个接口。

任何定义了接口所有方法的类型都被称为隐式地实现了该接口。

类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。

简言之:

  1. 接口是一组方法签名
  2. 接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口

Go语言提倡面向接口编程。

每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type writer interface{
    Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

一个类型只要实现了接口中的全部方法,那么就实现了这个接口。换句话说,接口就是一组需要实现的方法签名

定义一个接口并实现它:

type IPhone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type MobilePhone struct {
}

func (mobilePhone MobilePhone) call() {
    fmt.Println("I am mobile, I can call you!")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

为什么要实现接口呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,IPhone类型的变量能够存储NokiaPhoneMobilePhone类型的变量。

func main() {
    var x IPhone // 声明一个Sayer类型的变量x
    a := NokiaPhone{}  // 实例化一个NokiaPhone
    b := MobilePhone{}  // 实例化一个MobilePhone
    x = a       // 可以把NokiaPhone实例直接赋值给x
    x.call()     // I am Nokia, I can call you!
    x = b       // 可以把Phone实例直接赋值给x
    x.call()     // I am mobile, I can call you!
}

Tips: 观察下面的代码,体味此处_的妙用

// 摘自gin框架routergroup.go
type IRouter interface{ ... }

type RouterGroup struct { ... }

var _ IRouter = &RouterGroup{}  // 确保RouterGroup实现了接口IRouter

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?

定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go 语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样。

type Stringer interface {
    String() string
}

type person struct {
    name string
    age uint
    addr address
}
type address struct {
    province string
    city string
}

func (p person) String()  string{
    return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

func (addr address) String()  string{
    return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}

func printString(s fmt.Stringer){
    fmt.Println(s.String())
}

func main(){
    p := person{}
    printString(p) //正常输出
    printString(p.addr) //正常输出
    printString(&p) //正常输出
}

把变量 p 的指针作为实参传给 printString 函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口

值接收者(p person)实现了 Stringer 接口,那么类型 person 和它的指针类型*person就都实现了 Stringer 接口。

再把接收者改成指针类型:

type Stringer interface {
    String() string
}

type person struct {
    name string
    age uint
    addr address
}
type address struct {
    province string
    city string
}

func (p *person) String()  string{
    return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

func (addr address) String()  string{
    return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}

func printString(s fmt.Stringer){
    fmt.Println(s.String())
}

func main(){
    p := person{}
    printString(p) //cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
    printString(&p) //正常输出
}

修改成指针类型接收者后会发现,提示错误:

cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)

意思就是类型 person 没有实现 Stringer 接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口

总结:

当值类型作为接收者时,person 类型和*person类型都实现了该接口。

当指针类型作为接收者时,只有*person类型实现了该接口。

可以发现,实现接口的类型都有*person,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。分别定义Sayer接口和Mover接口:

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
    name string
}

// 实现Sayer接口
func (d dog) say() {
    fmt.Printf("%s会叫\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover

    var a = dog{name: "旺财"}
    x = a
    y = a
    x.say()
    y.move()
}

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
    move()
}

如狗可以动,汽车也可以动,实现这个关系:

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
}

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("脱水")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗衣服")
}

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

嵌套得到的接口的使用与普通接口一样。

空接口的定义

空接口是指没有定义任何方法签名的接口。由于任何类型都至少实现了0个方法,因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

type I interface{}

func main() {
    var i I
    i = 42 //这个时候i就是int类型
    fmt.Printf("%v,%T\n", i, i)
    i = "hello" //这个时候i就是string类型
    fmt.Printf("%v,%T\n", i, i)
}


42,int
hello,string

空接口的应用

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的映射。

// 空接口作为map值
    var person = make(map[string]interface{})
    person["name"] = "张三"
    person["age"] = 23
    person["married"] = false
    fmt.Println(person)

空接口对切片的影响

  1. 若将一个arrayslice赋值给空接口,这个空接口无法再进行切片

  2. arrayslice赋值给空接口的行为不是复制,而是类似指针效果,只不过无法再进行切片,但元素和原来的arrayslice及其衍生的,都有关联

    func main() {
    s := []int{2, 3, 5, 7, 11, 13}

    var e interface{}
    e = s
    
    f := s[0:3]
    f[2] = 55
    
    fmt.Printf("%T,%v\n", s, s)
    fmt.Printf("%T,%v\n", e, e)
    fmt.Printf("%T,%v\n", f, f)

    }

输出

[]int,[2 3 55 7 11 13]
[]int,[2 3 55 7 11 13]
[]int,[2 3 55]

若改为

func main() {
    s := []int{2, 3, 5, 7, 11, 13}

    var e interface{}
    e = s

    g := e[1:3]
    fmt.Println(g)
}

报错

cannot slice e (type interface {})

空接口的赋值

空接口可以存储任意值,但不代表任意类型就可以存储空接口类型的值

从实现的角度看,任何类型的值都满足空接口。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

但要是把一个空接口类型的对象,再赋值给一个固定类型(比如 int, string等类型)的对象赋值,是会报错的。

func main() {
    // 声明a变量, 类型int, 初始值为1
    var a int = 1

    // 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
    var i interface{} = a

    // 声明b变量, 尝试赋值i
    var b int = i
}

这个报错,它就好比可以放进行礼箱的东西,肯定能放到集装箱里,但是反过来,能放到集装箱的东西就不一定能放到行礼箱了,在 Go 里就直接禁止了这种反向操作。

cannot use i (type interface {}) as type int in assignment: need type assertion

空接口可以存储任意类型的值,那如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

请看下图分解:

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

// 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )

//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )

示例代码:

func main() {
   var i1 interface{} = new (Student)
   s := i1.(Student) //不安全,如果断言失败,会直接panic
   fmt.Println(s)

    var i2 interface{} = new(Student)
    s, ok := i2.(Student) //安全,断言失败,也不会panic,只是ok的值为false
    if ok {
        fmt.Println(s)
    }
}

type Student struct {}

断言其实还有另一种形式,就是用在利用 switch语句判断接口的类型。每一个case会被顺序地考虑。当命中一个case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case匹配的情况:

switch ins:=s.(type) {
    case Triangle:
        fmt.Println("三角形。。。",ins.a,ins.b,ins.c)
    case Circle:
        fmt.Println("圆形。。。。",ins.radius)
    case int:
        fmt.Println("整型数据。。")
    }

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗