[Go语言入门] 13 Go语言接口(interface)详解

13 Go语言接口(interface)详解

13.1 接口概念

接口是对其他类型行为的概括与抽象。

很多面向对象的语言都有接口这个概念,Go语言的接口的独特之处在于它是隐式实现。换句话说,对于一个具体的类型,无须声明它实现了哪些接口,只要该类型提供了接口所必须的方法即可。这种设计让你无须改变已有类型的实现,就可以为这些类型扩展新的接口,对于那些不能修改包的类型,这一点特别有用。

Go语言中提供了一种类型叫做接口类型。接口是一种抽象类型,它并没有暴露所含数据的布局或内部结构,当然也没有哪些数据的基本操作,它所提供的仅仅是一些方法而已。如果你拿到一个接口类型的值,你无从知道它是什么,你能知道的仅仅是它能做什么,也就是说,仅仅能知道它提供了哪些方法。

一个接口类型定义了一套方法,如果一个具体的类型要实现该接口,那么必须实现该接口类型定义中的所有方法。


13.2 声明接口类型

声明接口类型的语法:

type 接口类型名 interface {
    Method1(param_list) [return_list]
    Method2(param_list) [return_list]
    ...
}

示例:

// 声明一个接口类型Shape,它包括两个方法GetArea和GetPerimeter。
type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

Go语言标准库中也声明了很多接口类型,比如标准库的io包中定义下面这几个接口类型:

io.Writer是一个广泛使用的接口,它负责对所有可以写入字节的类型的抽象,包括文件、内存缓冲区、网络连接、HTTP客户端、打包器、散列器等。io.Reader接口抽象了所有可以读取字节的类型。io.Closer抽象了所有可以关闭的类型,比如文件或者网络连接。

package io

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

我们还可以通过组合已有的接口得到新的接口。比如下面的例子:

type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

如上的语法成为组合接口。

我们也可以不通过组合接口的方式,而是直接写出接口的方法定义:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

当然,也可以一部分方法是通过组合接口的方式定义,另一部分是直接写出的:

type ReadWriter interface {
    Reader
    Write(p []byte) (n int, err error)
}

三种声明的效果是一致的。方法定义的顺序也是无意义的,真正有意义的只有接口的方法集合。


13.3 实现接口

如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。

一个接口可以被多个类型实现,一个类型也可以实现多个接口。

实现接口示例:

/// 本示例中声明了接口Shape,声明了类型Circle、Square、Triangle;
/// Circle类型实现了接口的所有方法,因此Circle实现了接口Shape;
/// Square类型实现了接口的所有方法,因此Square实现了接口Shape;
/// Triangle类型只实现了接口的一部分方法,因此Triangle未实现接口Shape。

type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

type Circle struct {
    R float64
}

// Circle实现Shape接口的GetArea方法
func (c Circle)GetArea() float64 {
	return 3.14 * c.R * c.R
}

// Circle实现Shape接口的GetPerimeter方法
func (c Circle)GetPerimeter() float64 {
	return 3.14 * 2 * c.R
}

type Square struct {
    W float64
    H float64
}

// Square实现Shape接口的GetArea方法
func (s Square)GetArea() float64 {
    return s.W * s.H
}

// Square实现Shape接口的GetPerimeter方法
func (s Square)GetPerimeter() float64 {
    return 2 * (s.W + s.H)
}

type Triangle struct {
    W float64
    H float64
}

// Triangle只实现了Shape接口的GetArea方法,没有实现GetPerimeter方法,因此Triangle没有实现Shape接口。
func (t Triangle)GetArea() float64 {
    return t.W * t.H / 2
}

也可以通过指针类型(*T)来实现接口:

/// 本示例中声明了接口Shape,声明了类型Circle、Square;
/// *Circle类型实现了接口的所有方法,因此*Circle实现了接口Shape;
/// *Square类型实现了接口的所有方法,因此*Square实现了接口Shape。

type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

type Circle struct {
    R float64
}

// *Circle实现Shape接口的GetArea方法
func (c *Circle)GetArea() float64 {
	return 3.14 * c.R * c.R
}

// *Circle实现Shape接口的GetPerimeter方法
func (c *Circle)GetPerimeter() float64 {
	return 3.14 * 2 * c.R
}

type Square struct {
    W float64
    H float64
}

// *Square实现Shape接口的GetArea方法
func (s *Square)GetArea() float64 {
    return s.W * s.H
}

// *Square实现Shape接口的GetPerimeter方法
func (s *Square)GetPerimeter() float64 {
    return 2 * (s.W + s.H)
}

Go语言对接口实现的规定:

  • 实现接口的方法时,如果接收者都是T类型,称作T类型实现了该接口。
  • 实现接口的方法是,如果接收者都是*T类型,称作*T类型实现了该接口。
  • 如果方法的接收者为T类型,可以使用任意*T类型的接收者来调用,因此在Go语言中规定,如果通过T类型实现了一个接口,那么*T类型也自动实现了该接口。
  • 如果方法的接收者为*T类型,仅能通过可取地址的T类型接收者来调用,不能用不可取地址的T类型接收者(比如字面量)来调用,因此在Go语言中规定,如果通过*T类型实现了一个接口,那么T类型并没有实现该接口。

综上所述,当你在类型T中实现一个接口时,最好采取接收者要么全都是T类型,要么全都是*T类型。如果一个接口实现时混合使用两种方式,最终导致的结果就是只有*T类型实现了该接口,T类型没有实现该接口。


13.4 接口赋值

接口变量

当使用接口类型声明一个变量,这个变量称作接口变量。

可以将任意一个实现了该接口的类型的实例赋值给接口变量,此后通过变量调用接口方法,调用就是该实例的方法。

声明接口变量与声明其他类型变量的语法格式是相同的:

// var_name是变量名,InterfaceType是一个接口类型名
var var_name InterfaceType

给接口变量赋值

给接口变量赋值需要满足如下条件:仅当一个表达式的值为nil或实现了该接口时,这个表达式才可以赋给接口变量。

示例:

/// 本示例中声明了接口Shape,声明了类型Circle、Square、Triangle;
/// Circle实现了接口Shape,从而*Circle也自动实现了接口Shape;
/// *Square实现了接口Shape;
/// Triangle没有实现接口Shape。
package main

import "fmt"

type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

type Circle struct {
    R float64
}

func (c Circle)GetArea() float64 {
	return 3.14 * c.R * c.R
}

func (c Circle)GetPerimeter() float64 {
	return 3.14 * 2 * c.R
}

type Square struct {
    W float64
    H float64
}

func (s *Square)GetArea() float64 {
    return s.W * s.H
}

func (s *Square)GetPerimeter() float64 {
    return 2 * (s.W + s.H)
}

type Triangle struct {
    W float64
    H float64
}

func main() {
    var s Shape
    fmt.Println(s)							// 输出:  <nil>
    
    s = nil
    fmt.Println(s)							// 输出:  <nil>
    
    s = Circle{10}							// 等号右侧表达式的值是Circle类型,Circle类型实现了Shape接口,因此可以赋值给Shape接口变量
    fmt.Println(s)							// 输出:  {10}
    
    s = &Circle{10}							// 等号右侧表达式的值是*Circle类型,*Circle类型实现了Shape接口,因此可以赋值给Shape接口变量
    fmt.Println(s)							// 输出:  &{10}
    
    s = &Square{10, 10}						// 等号右侧表达式的值是*Square类型,*Square类型实现了Shape接口,因此可以赋值给Shape接口变量
    fmt.Println(s)							// 输出:  &{10 10}
    
    // s = Square{10, 10}					// 编译错误:cannot use Square literal (type Square) as type Shape in assignment: Square does not implement Shape (GetArea method has pointer receiver)
    										// 因为等号右侧表达式的值是Square类型,Square类型并没有实现Shape接口(尽管*Square类型实现了Shape)
    
    // s = Triangle{10, 10}					// 编译错误。很明显Triangle没有实现Shape接口。
    
    
    // 接口变量可以赋值给相同类型的接口变量
    s = Circle{10}
    var s2 Shape = s
    fmt.Println(s2)							// 输出:{10}
}

接口赋值时的拷贝方式

如果T类型实现了接口I,那么*T类型也自动实现了接口I,此时,即可以把一个T类型的实例赋值给接口I的变量,也可以把一个*T类型的实例指针赋值给接口I的变量。他们有什么不同呢?

  • 如果把T类型的实例赋值给接口变量,那么将拷贝该实例的数据结构到接口变量中。
  • 如果把*T类型的实例指针赋值给接口变量,那么仅拷贝指针值到接口变量中。
  • 如果将一个接口变量赋值给另一个接口变量,两个接口变量将会引用同一个实例。

示例:

package main

import "fmt"

type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

type Circle struct {
    R float64
}

func (c Circle)GetArea() float64 {
	return 3.14 * c.R * c.R
}

func (c Circle)GetPerimeter() float64 {
	return 3.14 * 2 * c.R
}

func main() {
    var s Shape
    var s2 Shape
    
    c := Circle{10}
    s = c							// 此时将复制整个Circle实例到s中,s中存放的是c的一个副本
    s2 = s							// s2与s引用的是同一个Circle实例
    c.R = 11
    fmt.Println(c)					// 输出:{11}
    fmt.Println(s)					// 输出:{10}
    fmt.Println(s2)					// 输出:{10}
    
    c = Circle{10}
    s = &c							// 此时将复制一个*Circle指针到s中,s中存放的是一个指向c的指针
    s2 = s							// s2中的指针值也会指向c
    c.R = 11
    fmt.Println(c)					// 输出:{11}
    fmt.Println(s)					// 输出:&{11}
    fmt.Println(s2)					// 输出:&{11}
}

13.5 接口调用

可以向接口变量赋值任何实现了该接口的类型的实例。然后通过接口变量调用接口的方法。而实际调用的方法该变量实际引用的实例的类型所定义的方法。

这个特性称为多态。

示例:

package main

import "fmt"

type Shape interface {
    GetArea() float64
    GetPerimeter() float64
}

type Circle struct {
    R float64
}

func (c Circle)GetArea() float64 {
    fmt.Println("enter Circle.GetArea")
	return 3.14 * c.R * c.R
}

func (c Circle)GetPerimeter() float64 {
	return 3.14 * 2 * c.R
}

type Square struct {
    W float64
    H float64
}

func (s Square)GetArea() float64 {
    fmt.Println("enter Square.GetArea")
    return s.W * s.H
}

func (s Square)GetPerimeter() float64 {
    return 2 * (s.W + s.H)
}

func main() {
    var s Shape
    
    s = Circle{10}
    s.GetArea()								// 输出:enter Circle.GetArea
    
    s = Square{10, 10}
    s.GetArea()								// 输出:enter Square.GetArea
}


13.6 interface{}

interface{}不包含任何方法,正因为如此,所有的类型都实现了interface{}interface{}看起来好像没什么作用,但是它可用来存储任意类型的值。它有点类似C语言的void *类型。

示例:

var a interface{}
var i int = 100
s := "Hello World"

// a可以存储任意类型的值
a = i
fmt.Println(a)
a = s
fmt.Println(a)


13.7 接口值的内存结构

从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:一个具体的类型和该类型的一个值。二者称为接口的动态类型和动态值。

在我们的概念模型中,用类型描述符来提供每个类型的具体信息,比如它的名字和方法集。对于一个接口值,类型部分就是用类型描述符来表述。

接口值得内存布局如下所示:
在这里插入图片描述


我们首先来看一下接口的零值。在Go语言中,变量默认初始化为其零值。接口的零值就是把它的动态类型和动态值都设置为nil。如下图所示:
在这里插入图片描述

一个接口值是否为nil取决于它的动态类型,所以接口的零值是一个nil接口值,因为它的动态类型是nil。

可以通过== nil 或者 != nil来判断接口值是否为nil。调用一个nil接口值的任何方法都会导致崩溃。

var s Shape
s.GetArea()		// 崩溃:s是nil接口值

我们再来看一下接口值是某类型的一个实例的情形,我们以之前示例代码中的的Circle类型为例,当把一个Circle实例赋值给接口变量时,接口值的内存布局如下:
在这里插入图片描述

可以看出,接口值的动态类型是Circle类型,接口值的动态值是一个指向Circle实例的指针


我们再来看一下接口值是某类型的实例的指针时的情形,我们还是以示例代码种的Circle类型为例,当把一个Circle实例的指针赋值给接口变量时,接口值的内存布局如下:
在这里插入图片描述

可以看出,接口值的动态类型是*Circle类型(注意与前面的那种情形不同),接口值的动态值仍然是一个指向Circle实例的指针。


一般来讲,在编译时我们无法知道一个接口值的动态类型会是什么,所以通过接口来做调用必然需要使用动态分发。即,编辑器必须生成一段代码来从类型描述符拿到对应的方法地址,再间接的调用该方法地址。调用该方法的接口者就是接口值的动态值。


接口值可以用==和!=操作符来比较。如果两个接口值都是nil或者二者的动态类型完全一致且动态值都相等,那么两个接口值相等。


注意:比较两个接口时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如slice),那么这个比较在执行时会发生崩溃:

var x interface{} = []int{1, 2, 3}
fmt.Println(x == x)		// 崩溃

当处理错误或者调试时,能拿到接口值得动态类型是很有帮助的。可以使用fmt包的%T来实现这个需求:

var w io.Writer
fmt.Printf("%T\n", w)			// <nil>

w = os.Stdout
fmt.Printf("%T\n", w)			// *os.File

w = new(bytes.Buffer)
fmt.Printf("%T\n", w)			// *bytes.Buffer

注意:含有空指针的非空接口

nil的接口值(其中不包含任何信息)与仅仅动态值为nil的接口值是不一样的。

示例:

var w io.Writer
fmt.Printf("%T\n", w)				// <nil>
fmt.Println(w == nil)				// true

w = (*bytes.Buffer)(nil)			
fmt.Printf("%T\n", w)				// // *bytes.Buffer
fmt.Println(w == nil)				// false

13.8 类型断言

类型断言是一个作用在接口值上的操作,写出来类似于x.(T),其中x是一个接口类型的表达式,而T是一个类型(称为断言类型)。类型断言会检查接口值得动态类型是否满足指定的断言类型。

  • 如果断言类型T是一个具体类型,那么类型断言就会检查x的动态类型是否就是T。如果检查成功,类型断言的结果就是x的动态值,类型当然就是T。欢聚话说,类型断言就是用来从接口值中把动态值取出来的操作。如果检查失败,那么操作崩溃。
  • 如果断言类型T是一个接口类型,那么类型断言检查x的动态类型是否满足T。如果检查成功,动态之并没有提取出来,结果仍然是一个接口值,接口值的类型和和值部分也没有变更,只是结果的类型为接口类型T。换句话说,类型断言是一个接口值表达式,从一个接口类型变更为拥有另外一套方法的接口类型,但保留了接口值中的动态类型和动态值部分。

无论使用哪种类型作为断言类型,如果操作数是一个空接口值,类型断言都失败。

我们经常无法确定一个接口值的动态类型,这时就需要检测它是否是某一个特定类型。如果类型断言出现在需要两个结果的赋值表达式中,那么断言不会在失败时崩溃,而是会多返回一个布尔型的返回值来指示断言是否成功。比如:

var w io.Writer = os.Stdout
f, ok := w.(*os.File)			// 成功:ok == true, f == os.Stdout
b, ok := w.(*bytes.Buffer)		// 失败:ok == false, b == nil

13.9 类型分支

switch x.(type) {
case nil:		// ...
case int, uint:	// ...
case bool:		// ...
case string:	// ...
default:		// ... 
}

判断x的类型,然后按顺序来判定哪一个case分支中的类型与其匹配。当一个分支匹配时,对应的代码会执行。

类型分支还支持提取出x的原始值:

switch x := x.(type) {
case nil:		// ...
case int, uint:	// ...
case bool:		// ...
case string:	// ...
default:		// ... 
}

  • 13
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时空旅客er

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值