当前位置: 移动技术网 > IT编程>脚本编程>Go语言 > Go基础系列:Go接口

Go基础系列:Go接口

2018年11月03日  | 移动技术网IT编程  | 我要评论
接口用法简介 接口(interface)是一种类型,用来定义行为(方法)。 但这些行为不会在接口上直接实现,而是需要用户自定义的方法来实现。所以,在上面的Namer接口类型中的方法 都是没有实际方法体的,仅仅只是在接口Namer中存放这些方法的签名( )。 当用户自定义的类型实现了接口上定义的这些方 ...

接口用法简介

接口(interface)是一种类型,用来定义行为(方法)。

type namer interface {
    my_method1()
    my_method2(para)
    my_method3(para) return_type
    ...
}

但这些行为不会在接口上直接实现,而是需要用户自定义的方法来实现。所以,在上面的namer接口类型中的方法my_methodn都是没有实际方法体的,仅仅只是在接口namer中存放这些方法的签名(签名 = 函数名+参数(类型)+返回值(类型))。

当用户自定义的类型实现了接口上定义的这些方法,那么自定义类型的值(也就是实例)可以赋值给接口类型的值(也就是接口实例)。这个赋值过程使得接口实例中保存了用户自定义类型实例。

例如:

package main

import (
    "fmt"
)

// shaper 接口类型
type shaper interface {
    area() float64
}

// circle struct类型
type circle struct {
    radius float64
}

// circle类型实现shaper中的方法area()
func (c *circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

// square struct类型
type square struct {
    length float64
}

// square类型实现shaper中的方法area()
func (s *square) area() float64 {
    return s.length * s.length
}

func main() {
    // circle类型的指针类型实例
    c := new(circle)
    c.radius = 2.5

    // square类型的值类型实例
    s := square{3.2}

    // sharpe接口实例ins1,它只能是值类型的
    var ins1 shaper
    // 将circle实例c赋值给接口实例ins1
    // 那么ins1中就保存了实例c
    ins1 = c
    fmt.println(ins1)

    // 使用类型推断将square实例s赋值给接口实例
    ins2 := s
    fmt.println(ins2)
}

上面将输出:

&{2.5}
{3.2}

从上面输出结果中可以看出,两个接口实例ins1和ins2被分别赋值后,分别保存了指针类型的circle实例c和值类型的square实例s

另外,从上面赋值ins1和ins2的赋值语句上看:

ins1 = c
ins2 := s

是否说明接口实例ins就是自定义类型的实例?实际上接口是指针类型(指向什么见下文)。这个时候,自定义类型的实例c、s称为具体实例,ins实例是抽象实例,因为ins接口中定义的行为(方法)并没有具体的行为模式,而c、s中的行为是具体的。

因为接口实例ins也是自定义类型的实例,所以当接口实例中保存了自定义类型的实例后,就可以直接从接口上调用它所保存的实例的方法。例如:

fmt.println(ins1.area())   // 输出19.625
fmt.println(ins2.area())   // 输出10.24

这里ins1.area()调用的是circle类型上的方法area(),ins2.area()调用的则是square类型上的方法area()。这说明go的接口可以实现面向对象中的多态:可以按需调用名称相同、功能不同的方法

接口实例中存的是什么

前面说了,接口类型是指针类型,但是它到底存放了什么东西?

接口类型的数据结构是2个指针,占用2个机器字长。

当将类型实例c赋值给接口实例ins1后,用println()函数输出ins1和c,比较它们的地址:

println(ins1)
println(c)

输出结果:

(0x4ceb00,0xc042068058)
0xc042068058

从结果中可以看出,接口实例中包含了两个地址,其中第二个地址和类型实例c的地址是完全相同的。而第二个地址c是circle的指针类型实例,所以ins中的第二个值也是指针。

ins中的第一个是指针是什么?它所指向的是一个内部表结构itable,这个table中包含两部分:第一部分是实例c的类型信息,也就是*circle,第二部分是这个类型(circle)的方法集,也就是circle类型的所有方法(此示例中circle只定义了一个方法area())。

所以,如图所示:

注意,上图中的实例c是指针,是指针类型的circle实例。

对于值类型的square实例s,ins2保存的内容则如下图:

方法集(method set)规则

官方手册对method set的解释:https://golang.org/ref/spec#method_sets

实例的method set决定了它所实现的接口,以及通过receiver可以调用的方法。

方法集是类型的方法集合,对于非接口类型,每个类型都分两个method set:值类型实例是一个method set,指针类型的实例是另一个method set。两个method set由不同receiver类型的方法组成:

实例的类型       receiver
--------------------------------------
 值类型:t       (t type)
 指针类型:*t    (t type)或(t *type)

也就是说:

  • 值类型的实例的method set只由值类型的receiver(t type)组成
  • 指针类型的实例的method set由值类型和指针类型的receiver共同组成,即(t type)(t *type)

这是什么意思呢?从receiver的角度去考虑:

receiver        实例的类型
---------------------------
(t type)        t 或 *t
(t *type)       *t

上面的意思是:

  • 如果某类型实现接口的方法的receiver是(t *type)类型的,那么只有指针类型的实例*t才算是实现了这个接口
  • 如果某类型实现接口的方法的receiver是(t type)类型的,那么值类型的实例t和指针类型的实例*t都算实现了这个接口

举个例子。接口方法area(),自定义类型circle有一个receiver类型为(c *circle)的area()方法时,说明实现了接口的方法,但只有circle实例的类型为指针类型时,这个实例才算是实现了接口,才能赋值给接口实例,才能当作一个接口参数。如下:

package main

import "fmt"

// shaper 接口类型
type shaper interface {
    area() float64
}

// circle struct类型
type circle struct {
    radius float64
}

// circle类型实现shaper中的方法area()
// receiver类型为指针类型
func (c *circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // 声明2个接口实例
    var ins1, ins2 shaper

    // circle的指针类型实例
    c1 := new(circle)
    c1.radius = 2.5
    ins1 = c1
    fmt.println(ins1.area())

    // circle的值类型实例
    c2 := circle{3.0}
    // 下面的将报错
    ins2 = c2
    fmt.println(ins2.area())
}

报错结果:

cannot use c2 (type circle) as type shaper
in assignment:
        circle does not implement shaper (area method has
pointer receiver)

它的意思是,circle值类型的实例c2没有实现share接口的area()方法,它的area()方法是指针类型的receiver。换句话说,值类型的c2实例的method set中没有receiver类型为指针的area()方法

所以,上面应该改成:

ins2 = &c2

再声明一个方法,它的receiver是值类型的。下面的代码一切正常。

type square struct{
    length float64
}

// 实现方法area(),receiver为值类型
func (s square) area() float64{
    return s.length * s.length
}

func main() {
    var ins3,ins4 shaper

    // 值类型的square实例s1
    s1 := square{3.0}
    ins3 = s1
    fmt.println(ins3.area())

    // 指针类型的square实例s2
    s2 := new(square)
    s2.length=4.0
    ins4 = s2
    fmt.println(ins4.area())
}

很经常的,我们会直接使用推断类型的赋值方式(如ins2 := c2)将实例赋值给一个变量,我们以为这个变量是接口的实例,但实际上并不一定。正如上面值类型的c2赋值给ins2,这个ins2将是从c2数据结构拷贝而来的另一个副本数据结构,并非接口实例,但这时通过ins2也能调用area()方法:

c2 = circle{3.2}
ins2 := c2
fmt.println(ins2.area())  // 正常执行

之所以能调用,是因为circle类型中有area()方法,但这不是通过接口去调用的。

所以,在使用接口的时候,应当尽量使用var先声明接口类型的实例,再将类型的实例赋值给接口实例(如var ins1,ins2 shaper),或者使用ins1 := shaper(c1)的方式。这样,如果赋值给接口实例的类型实例没有实现该接口,将会报错。

但是,为什么要限制指针类型的receiver只能是指针类型的实例的method set呢?

看下图,假如指针类型的receiver可以组成值类型实例的method set,那么接口实例的第二个指针就必须找到值类型的实例的地址。但实际上,并非所有值类型的实例都能获取到它们的地址。

哪些值类型的实例找不到地址?最常见的是那些简单数据类型的别名类型,如果匿名生成它们的实例,它们的地址就会被go彻底隐藏,外界找不到这个实例的地址。

例如:

package main

import "fmt"

type myint int

func (m *myint) add() myint {
    return *m + 1
}
func main() {
    fmt.println(myint(3).add())
}

以下是报错信息:找不到myint(3)的地址

abc\abc.go:11:22: cannot call pointer method on myint(3)
abc\abc.go:11:22: cannot take the address of myint(3)

这里的myint(3)是匿名的myint实例,它的底层是简单数据类型int,myint(3)的地址会被彻底隐藏,只会提供它的值对象3。

接口类型作为参数

将接口类型作为参数很常见。这时,那些实现接口的实例都能作为接口类型参数传递给函数/方法。

例如,下面的myarea()函数的参数是n shaper,是接口类型。

package main

import (
    "fmt"
)

// shaper 接口类型
type shaper interface {
    area() float64
}

// circle struct类型
type circle struct {
    radius float64
}

// circle类型实现shaper中的方法area()
func (c *circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // circle的指针类型实例
    c1 := new(circle)
    c1.radius = 2.5
    myarea(c1)
}

func myarea(n shaper) {
    fmt.println(n.area())
}

上面myarea(c1)是将c1作为接口类型参数传递给n,然后调用c1.area(),因为实现了接口方法,所以调用的是circle的area()。

如果实现接口方法的receiver是指针类型的,但却是值类型的实例,将没法作为接口参数传递给函数,原因前面已经解释过了,这种类型的实例没有实现接口。

以接口作为方法或函数的参数,将使得一切都变得灵活且通用,只要是实现了接口的类型实例,都可以去调用它。

用的非常多的fmt.println(),它的参数也是接口,而且是变长的接口参数:

$ go doc fmt println
func println(a ...interface{}) (n int, err error)

每一个参数都会放进一个名为a的slice中,slice中的元素是接口类型,而且是空接口,这使得无需实现任何方法,任何东西都可以丢带fmt.println()中来,至于每个东西怎么输出,那就要看具体情况。

接口类型的嵌套

接口可以嵌套,嵌套的内部接口将属于外部接口,内部接口的方法也将属于外部接口。

例如,file接口内部嵌套了readwrite接口和lock接口。

type readwrite interface {
    read(b buffer) bool
    write(b buffer) bool
}
type lock interface {
    lock()
    unlock()
}
type file interface {
    readwrite
    lock
    close()
}

除此之外,类型嵌套时,如果内部类型实现了接口,那么外部类型也会自动实现接口,因为内部属性是属于外部属性的。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网