golang中的interface

RenXin Lv3

Interface整理

接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。

编写参数是接口变量的函数,这使得它们更具有一般性。

使用接口使代码更具有普适性。

最近在学Go当中的接口,学的有点云里雾里 ,这个interface和Java的也太不像了,我们先来看看Java当中的接口是怎么用的:

首先我们先定义一个接口:

1
2
3
public interface Study {    //使用interface表示这是一个接口
void study(); //接口中只能定义访问权限为public抽象方法,其中public和abstract关键字可以省略
}

之后我们用关键字继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Student extends Person implements Study {   //使用implements关键字来实现接口
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}

@Override
public void study() { //实现接口时,同样需要将接口中所有的抽象方法全部实现
System.out.println("我会学习!");
}
}

public class Teacher extends Person implements Study {
protected Teacher(String name, int age, String sex) {
super(name, age, sex, "教师");
}

@Override
public void study() {
System.out.println("我会加倍学习!");
}

这样一个显示继承的方式非常清晰明了,接下来看看Go里面的接口:

1
2
3
4
5
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}

这样一看没有什么很大的区别,都需要先声明一个接口但是不使用,接下来看看接口的实现:

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
package main

import "fmt"

type Shaper interface {
Area() float32
}

type Square struct {
side float32
}

func (sq *Square) Area() float32 {
return sq.side * sq.side
}

func main() {
sq1 := new(Square)
sq1.side = 5

var areaIntf Shaper
areaIntf = sq1
// shorter,without separate declaration:
// areaIntf := Shaper(sq1)
// or even:
// areaIntf := sq1
fmt.Printf("The square has area: %f\n", areaIntf.Area())
}

这样就会发现如下几个区别:

  1. 并没有显式继承
  2. 接口能声明变量,并通过该变量指向方法
  3. 实现方法中的参数为自定义的结构体

一个接口类型的变量或一个 接口值 :

首先我们来看第一点,关于为什么不显示继承,这一点我在网上搜过,观点基本是Go强调的是组合而非继承,并没有一个很确切的理论,那暂且不议

第二点:areaIntf是一个多字(multiword)数据结构,它的值是 nil。接口变量里包含了接收者实例的值和指向对应方法表的指针。

在Go中,我们自定义的结构体就像Java中的类一样,可以实现接口中的方法。我们可以同一个接口被实现多次。当时就有了点疑问:不是不允许函数重载吗?后来发现方法和函数是完全不同的概念:

Go中不允许函数(function)重载是为了提高效率,而方法(method)的可多次实现则体现了Go的多态,也就是根据场景选择。


接下来,我们看一些进阶功能:

接口嵌套接口

在Java 和go当中,我们都倡导一个接口的简洁明了。比如说先定义一个结构体为综测,综测又是由考试成绩、竞赛、体育等等组成,考试成绩里面又有不同科,体育里面也有不同科,这个时候我们就应该分开定义,之后进行嵌套。我个人的理解的理解就是类似于树一样的存在,而一个结构体就是一个父节点。这里还是放一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
type ReadSeeker interface {
Reader
Seeker
}

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

type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

类型断言

我们通常会想知道一个接口变量里面是什么类型,这个时候我们就会用到类型断言,通用格式为:

1
typeA := var1.(T)

var1为接口变量,T是想知道的类型。如果转换合法,typeA 是 var1转换到类型 T 的值

如果在判断式中使用,则是这样的:

1
2
3
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of areaIntf is: %T\n", t)
}

如果转换合法,t 是 转换到类型的值,ok 会是 true;否则 t是类型的零值,ok 是 false,也没有运行时错误发生。

注意:如果忽略 areaIntf.(*Square) 中的 * 号,会导致编译错误:impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)

同理,我们也可以判断他是否属于该接口:

1
2
3
4
5
6
7
type Stringer interface {
String() string
}

if sv, ok := v.(Stringer); ok {
fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}

类型判断 type-switch

个人认为如果说类型断言是只想知道值是不是某个类型,那么此语句则是想知道究竟是哪个重要的类型或者不需要知道的类型,常见用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("Param #%d is a bool\n", i)
case float64:
fmt.Printf("Param #%d is a float64\n", i)
case int, int64:
fmt.Printf("Param #%d is a int\n", i)
case nil:
fmt.Printf("Param #%d is a nil\n", i)
case string:
fmt.Printf("Param #%d is a string\n", i)
default:
fmt.Printf("Param #%d is unknown\n", i)
}
}
}

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。

使用方法集与接口

作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,

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
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
)

type List []int

func (l List) Len() int {
return len(l)
}

func (l *List) Append(val int) {
*l = append(*l, val)
}

type Appender interface {
Append(int)
}

func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}

type Lener interface {
Len() int
}

func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}

func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)

CountInto(lst, 1, 10) //错误代码
if LongEnough(lst) { // VALID: Identical receiver type
fmt.Printf("- lst is long enough\n")
}

// A pointer value
plst := new(List)
CountInto(plst, 1, 10) // VALID: Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}

输出

讨论

在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。

在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。

总结

在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P 直接辨识的:

  • 指针方法可以通过指针调用
  • 值方法可以通过值调用
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。

译注

Go 语言规范定义了接口方法集的调用规则:

  • 类型 T 的可调用方法集包含接受者为 T或 T 的所有方法集
  • 类型 T 的可调用方法集包含接受者为 T的所有方法
  • 类型 T 的可调用方法集包含接受者为 T 的方法

接下来我们讨论下空接口

空接口

定义:不包含任何方法,对实现没有要求

空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。

可以给一个空接口类型的变量 var val interface {} 赋任何类型的值

每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。

这样光看似乎觉得没什么大不了的,我们举个例子,比如说创建树或者其他数据结构,如果我们要根据每个数据类型来定义不同的方法,那无疑是很浪费时间的,这时候就可以用到空接口,实现一键通用:

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
package main

import (
"fmt"
)

type Node struct {
le *Node
data interface{}
rl *Node
}

func NewNode(left, right *Node) *Node {
return &Node{left, nil, right}
}

func (n *Node) setData(data interface{}) {
n.data = data
}

func main() {
root := NewNode(nil, nil)
root.setData("root node")
a := NewNode(nil, nil)
a.setData("left node")
b := NewNode(nil, nil)
b.setData(1)
root.le = a
root.rl = b
fmt.Printf("%v\n", root)
}

接口赋值给接口

一个接口的值可以赋值给另一个接口变量,前提是底层类型实现了必要的方法,此转换是在运行时检查的,转换失败的时候会导致一个运行时错误,这也是GO的动态的一点

比如此代码

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
package main

import "fmt"

type Shaper interface {
Area() float64
}

type Square struct {
side float64
}

func (s Square) Area() float64 {
return s.side * s.side
}

type Circle struct {
radius float64
}

func main() {
var s Shaper
c := Circle{radius: 5.0}

// 错误的示例:将接口 Shaper 赋值给接口 Shaper,但底层类型 Circle 并没有实现 Area() 方法
s = c

fmt.Printf("Area of the shape: %f\n", s.Area())
}

错误显示:

实例

我们来看一些实际应用,在GORM框架中,我们创建对象可以使用map的数据结构导入,但是我们无法保证数据都是一个类型,所以就需要一个空接口来帮我们接住所有类型:

1
2
3
4
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
  • Title: golang中的interface
  • Author: RenXin
  • Created at : 2023-10-31 00:00:00
  • Updated at : 2023-12-29 19:31:04
  • Link: https://blog.renxin.space/golang/basis/interface/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments