接口用法简介

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

type Namer interface {
    my_method1()
    my_method2(para)
    my_method3(para) return_type
    ...
}
my_methodN签名 = 函数名+参数(类型)+返回值(类型)

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

例如:

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()ins2.Area()

接口实例中存的是什么

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

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

cins1println()
println(ins1)
println(c)

输出结果:

(0x4ceb00,0xc042068058)
0xc042068058
c
*Circle

所以,如图所示:

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

s

实际上接口实例中保存的内容,在反射(reflect)中体现的淋漓尽致,reflect所有的一切都离不开接口实例保存的内容。

方法集(Method Set)规则

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

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

实例的类型       receiver
--------------------------------------
 值类型:T       (T Type)
 指针类型:*T    (T Type)或(T *Type)

也就是说:

(T Type)(T Type)(T *Type)

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

receiver        实例的类型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T

上面的意思是:

  • receiver是指针类型的方法只可能存在于指针类型的实例方法集中
  • receiver是值类型的方法既存在于值类型的实例方法集中,也存在于指针类型的方法集中

从实现接口方法的角度上看:

(T *Type)*TT(T Type)T*TT*T
(c *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())
}

所以,从struct类型定义的方法的角度去看,如果这个类型的方法有指针类型的receiver方法,则只能使用指针类型的实例赋值给接口变量,才算是实现了接口。如果这个类型的方法全是值类型的receiver方法,则可以随意使用值类型或指针类型的实例赋值给接口变量。下面这两个对应关系,对于理解很有帮助:

实例的类型       receiver
--------------------------------------
 值类型:T       (T Type)
 指针类型:*T    (T Type)或(T *Type)
 
receiver        实例的类型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T
ins2 := c2
c2 = Circle{3.2}
ins2 := c2
fmt.Println(ins2.Area())  // 正常执行

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

var ins1,ins2 Shaperins1 := 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(3)

普通方法和实现接口方法的区别

对于普通方法,无论是值类型还是指针类型的实例,都能正常调用,且调用时拷贝的内容都由receiver的类型决定

func (T Type) method1   // 值类型receiver
func (T *Type) method2  // 指针类型receiver

指针类型的receiver决定了无论是值类型还是指针类型的实例,都拷贝实例的指针。值类型的receiver决定了无论是值类型还是指针类型的实例,都拷贝实例本身

所以,对于person数据结构:

type person struct {}
p1 := person{}       // 值类型的实例
p2 := new(person)    // 指针类型的实例
p1.method1()p2.method1()p2.method1()p2.method1()(*p2).method1()
p1.method2()p2.method2()p1.method2()p1.method2()(&p1).method2()

而类型实现接口方法时,method set规则决定了类型实例是否实现了接口。

receiver        实例的类型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T

对于接口abc、接口方法method1()、method2()和结构person:

type abc interface {
	method1
	method2
}

type person struct {}
func (T person) method1   // 值类型receiver
func (T *person) method2  // 指针类型receiver

p1 := abc(person)  // 接口变量保存值类型实例
p2 := abc(&person) // 接口变量保存指针类型实例
p2.method1()p2.method2()p1.method1()
p1.method2()

接口类型作为参数

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

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.Area()

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

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

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

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

接口类型的嵌套

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

例如,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()
}

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

更多关于Go基础教程系列之Go接口的使用方法请查看下面的相关链接

您可能感兴趣的文章: