Funky's NoteBook

Go Core Dev Interface

字数统计: 2,910阅读时长: 12 min
2019/01/24 Share

接口

接口是一个编程规约,一组方法签名的组合。接口是没有具体实现的逻辑,不能定义字段

  • 变量和实例

实例里蕴含了变量值、变量类型和附着在类型上的方法等语义。实例和面向对象编程中的对象改了类似,我们使用实例来代表具体类型的变量,接口变量只有值和类型的概念,所有接口类型变量仍然称为接口变量,接口内部存放的具体类型变量被称为接口指向的实例。接口只有声明没有实现,所有定义一个新接口,通常又变成声明一个新接口,定义接口和声明接口两者代表相同的意思。

  • 空接口

最常使用的接口字面量类型就是空接口 interface{} ,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型实例。

非命名类型由于不能定义自己的方法,所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。

接口声明

  • 接口字面量类型的声明语法
1
2
3
4
interface {
MethodSignature1
MethodSignature2
}
  • 接口命名类型使用type类型关键字声明语法
1
2
3
4
type InterfaceName interface {
MethodSignature1
MethodSignature2
}

使用接口字面量场景很少,一般只有空接口类型变量的声明才会使用。

接口支持嵌入匿名接口字段,就是一个接口定义里可以包括其他接口,Go 编译器会自动进行展开处理,有点类似C语言中的宏的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Reader interface {
Read(p []byte) (n int, err error)
}

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

// 如下三种声明等价,最终展开模式都是第三种模式
type ReadWriter interface {
Reader
Writer
}

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

type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
方法声明
1
2
// 方法声明 = 方法名 + 方法签名
MethodName (InputTypeList) OutTypeList

接口中的方法声明非常类似于C语言的函数声明概念,Go 编译器在做接口匹配判断时是杨校验方法名称和方法签名的。

声明新接口类型的特点
  • 接口命名一般以”er”结尾
  • 接口定义内部方法声明不需要 func 引导
  • 接口定义中,方法声明没有方法实现

接口初始化

单纯声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。接口作为一个抽象层,起到抽象和适配的作用。没有初始化的接口变量,默认值为nil。

1
2
var i io.Reader
fmt.Printf("%T\n", i) // <nil>

接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接的初始化方法。

  • 实例赋值接口
  • 接口变量赋值接口变量
1
2
3
4
5
file, _ := os.Openfile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)

var rw io.ReadWriter = file
// io.ReadWriter 接口可以直接赋值给 io.Writer 接口变量
var w io.Writer = rw

接口方法调用

接口方法调用不同于普通函数调用。接口方法调用最终地址是在运行时期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。

直接调用未初始化的接口变量方法会出现 panic 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
type Printer interface {
Print()
}

type S struct {}
func (s S) Print() {
println("print")
}

func main() {
var i Printer
// 没有初始化会产生 panic
// i.Print()
i = S{}
i.Print()
}

接口的动态类型和静态类型

  • 动态类型

接口绑定的具体实例为接口的动态类型。接口可以绑定不同类型的实例,所有接口的动态类型是随着其绑定的不同类型实例而发生变化的。

  • 静态类型

接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(方法的顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以直接赋值。

接口运算

类型断言

接口类型断言的语法形式如下:

1
i.(TypeName)

i 必须是接口变量,如果是具体类型变量,则编译器会报错,TypeName 可以是接口类型名,也可以是具体类型名。

接口查询的两层语义
  • 如果 TypeName 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 TypeName。

  • 如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 TypeName 接口。

接口断言的两种语法实现

直接赋值模式如下:

1
o := i.(TypeName)

语义分析:

  • TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName,则变量 o 的类型就是 TypeName,变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本)
  • TypeName 是接口类型名,如果接口 i 绑定的实例类型满足接口类型 TypeName,则变量 o 的类型就是接口类型 TypeName,o 底层绑定的具体类型实例 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

type Inter interface {
Ping()
Pong()
}

type Anter interface {
Inter
String()
}

type St struct {
Name string
}

func (St) Ping() {
println("ping\n")
}

func (*St) Pong() {
println("pong\n")
}

func main() {
st := &St{"andes"}
var i interface{} = st
// 判断 i 绑定的实例是否实现接口类型 Inter
o := i.(Inter)
o.Ping()
o.Pong()

// 如下语句引发 panic 因为 i 没有实现接口 Anter String
// p := i.(Anter)
// p.String()

// 判断 i 绑定的实例是否就是具体类型 St
s := i.(*St)
fmt.Printf("%s", s.Name)
}

comma,ok 表达模式:

1
2
3
if o, ok := i.(TypeName); ok {
// do something
}
  • TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName 则 ok 为 True,变量 o 的类型就是 TypeName,变量 o 的值 就是接口绑定实例值的副本
  • TypeName 是接口类型名,如果接口 i 绑定的实例类型满足接口类型 TypeName,则 ok 为True,变量 o的类型就是接口类型TypeName,o底层绑定的具体类型实例是 i 绑定的实例的副本

  • 如果上述都不满足,则 ok 为 false,变量 o 是 TypeName类型的零值。

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
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

type Inter interface {
Ping()
Pong()
}

type Anter interface {
Inter
String()
}

type St struct {
Name string
}

func (St) Ping() {
println("ping\n")
}

func (*St) Pong() {
println("pong\n")
}

func main() {
st := &St{"andes"}
var i interface{} = st
// 判断 i 绑定的实例是否实现了接口类型 Inter
if o, ok := i.(Inter); ok{
o.Ping()
o.Pong()
}
// 判断 i 绑定的实例是否实现了接口类型 Anter
if p, ok := i.(Anter); ok{
p.String()
}
// 判断 i 绑定的实例是否就是具体类型St
if s, ok := i.(*St); ok{
fmt.Println("%s", s.Name)
}
}

类型查询

  • i 必须是接口类型 (接口未初始化在返回 nil)
  • case 字句后可以跟非接口类型名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE)
if err != nil {
log.Fatal(err)
}
defer f.close()

var i io.Reader = f
switch v := i.(type) {
// i 绑定实例是 *osFlie 类型,实现了 io.ReadWriter 匹配成功
case io.ReadWriter:
// v 是 io.ReadWriter 接口类型,可调用 Write 方法
v.Write([]byte("io.ReadWriter\n"))
// 由于上一个已经匹配,所以不会走到这里
case *os.File:
v.Write([]byte("io.ReadWriter\n"))
v.Sync()
default:
return
}
  • 如果 case 后面是具体类型名,且接口变量 i 绑定的实例类型和该具体类型相同,则匹配成功, v 为具体类型变量,v 的值是 i 绑定实例值的副本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE)
if err != nil {
log.Fatal(err)
}
defer f.close()

var i io.Reader = f
switch v := i.(type) {
// i 绑定实例是 *osFlie 类型, 匹配成功
case *os.File:
v.Write([]byte("io.ReadWriter\n"))
v.Sync()
// 由于上一个已经匹配,所以不会走到这里
case io.ReadWriter:
// v 是 io.ReadWriter 接口类型,可调用 Write 方法
v.Write([]byte("io.ReadWriter\n"))
default:
return
}
  • 如果 case 后面跟多个类型,用逗号分隔,接口变量 i 绑定的实例类型只要和其中一个类型匹配,则直接使用 o 赋值给 v,相当于 v := o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
f, err := os.OpenFile("xxx", os.O_RDWR|os.O_CREATE)
if err != nil {
log.Fatal(err)
}
defer f.close()

var i io.Reader = f
switch v := i.(type) {
// i 绑定实例是 *osFlie 类型, 匹配成功
case *os.File, io.ReadWriter:
if v == i {
fmt.Println(true) // true
}
default:
return
}
  • 如果 所以case 不满足,则执行 default 语句,此时执行的仍然是v := o ,最终 v 的值是 o。此时使用 v 没有任何意义

  • fallthrough 语句不能在该 Type Switch 语句中使用。

不推荐用法:

1
switch i := i.(type) {}

推荐用法:

1
switch v := i.(type) {}
  • 类型查询和断言有相同的语义只是语法格式不同。
  • 类型查询使用 case 判断,当然类型断言也可以使用 if 判断打到同样效果。

接口的优点和使用形式

  • 优点:解耦、实现泛型
  • 接口使用形式
    • 作为结构内嵌字段
    • 作为函数或方法的形参
    • 作为函数或方法的返回值
    • 作为其他接口定义的嵌入字段

空接口

Go 语言没有泛型,如果一个喊需要接收任意类型的参数,则参数类型可以使用空接口类型,这是没有弥补泛型的一种手段。例如:

1
func Fprint(w io.Writer, a ...interface{}) (n int, err error)

空接口与 nil

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
32
package main
import "fmt"
type Inter interface {
Ping()
Pong()
}

type St struct {}

func (St) Ping() {
println("ping")
}

func (*St) Pong() {
println("pong")
}

func main() {
var st *St = nil
var it Inter = st

fmt.Printf("%p\n", st)
fmt.Printf("%p\n", it)
if it != nil {
it.Pong()

// 下面的语句会导致 panic
// 方法转换为喊调用,第一个参数是St 类型,由于 *St 是 nil 无法获取指针所指的对象值
// 因此导致 panic
// it.Ping()
}
}
CATALOG
  1. 1. 接口
    1. 1.0.1. 接口声明
      1. 1.0.1.1. 方法声明
      2. 1.0.1.2. 声明新接口类型的特点
    2. 1.0.2. 接口初始化
    3. 1.0.3. 接口方法调用
    4. 1.0.4. 接口的动态类型和静态类型
  2. 1.1. 接口运算
    1. 1.1.1. 类型断言
      1. 1.1.1.1. 接口查询的两层语义
      2. 1.1.1.2. 接口断言的两种语法实现
    2. 1.1.2. 类型查询
    3. 1.1.3. 接口的优点和使用形式
  3. 1.2. 空接口