golang学习笔记七:interface

接口的基本介绍和使用在这里就不写了,作为 go 最重要的组成部分之一,多态等都是通过它来实现的。

使用

接口嵌套

想要使用该子接口的话,必须将父接口和子接口的所有方法都实现。

1
2
3
4
5
6
7
8
type stringer interface{ 
string()string
}

type tester interface{
stringer // 嵌入其他接口
test()
}

插句题外话,如果structure想要像interface一样进行嵌套的话需要进行初始化。初始化方式仍旧是自己初始化and依赖注入两种。

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Sequence []int
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}

虽然我没搞懂为什么是 $O(N^2)$,但是loop浪费时间是真的,而利用类型转换就可以:

1
2
3
4
5
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}

value.(type) 算是interface类型判断和转换的方法,利用switch可以进行类型判断

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

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}

类型转换则是在type里输入想转换的值,失败时在上面的情况下会抛出panic,所以也提供了第二个断言值来判断转换是否成功:

1
2
3
4
5
6
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}

接口继承

OO的说法摆在这里有点奇怪。。。所有的struct只要实现了interface里定义的方法,就可以被认为是这个interface的继承,这也是GO语言实现多态的方法。

如果一个类型只是用来实现接口,并且除了该接口以外没有其它被导出的方法,那就不需要导出这个类型。只导出接口,清楚地表明了其重要的是行为,而不是实现,并且其它具有不同属性的实现可以反映原始类型的行为。这也避免了对每个公共方法实例进行重复的文档介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}

type Stream interface {
XORKeyStream(dst, src []byte)
}

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream

在上面的加解密的例子里,无论我们传入的是哪种加密算法都无所谓,只要实现方法是只有interface内的,就可以利用NewCTR生成对应的stream。

利用接口实现多态,而实现OO的继承需要使用到反射(明天再说)。

接口详解

可以先回忆一下OO编程中的 is-a 和 has-a。

is-a就是继承,简单来说就和前面的接口继承一样,有一个基类,可以派生出非常多种子类来。再简单一点解释就是基类鸟:有翅膀,有脚,会飞,吃虫子。那么我创建的所有鸟都必须有翅膀,有脚,会飞,吃虫子。

继承是强耦合的,如果哪天定义变了,我们认为鸟不吃虫子改吃鱼了(当然实际上鸟两个都吃忘记这个失败的比方),那么所有的子类鸟都必须吃鱼。

has-a就是组合,我们有翅膀,脚,心脏,血液等等类,鸟就是由这些类组合出来的,has-a是低耦合关系,哪天血液类发生了改变,血的颜色不是红的变成绿的了,对鸟这个类本身没有什么影响。

duck typing

那么我们回到 GO 语言,现在先不讲has-a,而是is-a的问题,传统OO编程的is-a是由基类产生派生类,派生类直接继承基类的所有元素,可以在基类的基础上改进or覆盖(说的就是虚函数这种面试官的最爱)。GO语言的继承和多态的概念和前面就有点微妙的不太一样,在 GO 语言中当定义了一个鸟类接口后,我们认为,所有有翅膀,有脚,会飞吃虫子的,都是鸟。

这个is-a就变得不再是严格的属于关系,而是通过特征来判定一个东西的归属情况,这种想法我们叫它 duck typing

鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。

duck typing 在C++中的实现我已经不熟悉了,在GO语言中当然就是通过接口+方法的方式来实现,倒不如说GO语言本身已经没有继承这种强耦合的关系形式了。

一个抄来的 duck typing 的例子:

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
type Reader interface {
Read(p []byte) (n int, err error)
}

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

type Closer interface {
Close() error
}

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

type File struct { // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

var file1 Reader = new(File)
var file2 Writer = new(File)
var file3 Closer = new(File)
var file4 Seeker = new(File)

因为在File实现了4个接口,因此可以将File对象赋值给任何一个接口。

接口值接收(Receiver)

一个类型可以实现任意数量的接口,每个类型都实现了一个空接口interface{}。
接口是一系列接口的集合,是一种抽象数据类型,接口变量可以引用任何实现了接口的全部方法的具体数据类型的值。

接口变量存储了两部分信息,一个是分配给接口变量的具体值(接口实现者的值),一个是值的类型的描述器(接口实现者的类型),形式是(value, concrete type),而不是(value, interface type)。

下面这是一个定义了GET方法的接口,函数 f 也就是调用了接口内的 GET 方法并输出。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

type Retriever interface {
Get(url string) string
}

func f(r Retriever) {
s := r.Get('\args')
fmt.Println(s)
}

pointer receiver

首先来看一下指针接收者的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type RealRetriever struct {
Contents string
}

// 指针接收者
func (r *RealRetriever) Get(url string) string {
return r.Contents
}

func main(){
retriever := RealRetriever{}
f(&retriever) //pointer
f(retriever) //value
}

f(&retriever) 执行完全没有问题,而 f(retriever) 会报错,简单来说就是 f(retriever) 是个call by value,调用的时候会对 retriever 进行复制,拷贝之后的r并不支持GET的接收者是一个指针的概念。

value receiver

1
2
3
4
5
6
7
8
type MockRetriever struct {
Contents string
}

// 值接收者
func (r MockRetriever) Get(url string) string {
return r.Contents
}

同样执行上面的代码,发现两个都是可以的,当传入的是一个引用的时候,go内部的隐式转换可以通过地址找到Get方法,而之前call by value的情况下,通过值是无法找到地址在哪儿的。

空接口

空接口类型interface{}一个方法签名也不包含,所以所有的数据类型都实现了空接口。
空接口类型可以用于存储任意数据类型的实例。

空接口给了我一种指针的感觉😂,可以指向任何一个interface。

如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数。但是对于函数内部来讲,传入的永远是一个interface类型。

数组的空接口则不能简单地通过等号来赋值,打个比方:

1
2
t := []int{1, 2, 3, 4}
var s []interface{} = t

同理,当使用 []interface{} 作为参数类型的时候,像下面这样直接传进去也是没有用的。

1
2
3
4
5
6
7
8
9
10
func printAll(vals []interface{}) { //1
for _, val := range vals {
fmt.Println(val)
}
}

func main(){
names := []string{"stanley", "david", "oscar"}
printAll(names)
}

具体为什么不自动转换有个博客提到了wiki上占用存储空间的解释(我本人的怀疑就是转换代价太高)https://github.com/golang/go/wiki/InterfaceSlice

当然可以通过手动添加地址空间来做:

1
2
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))

判断interface的类型可以用之前类型转换里面的类型断言来做。