← 返回全部文章

WRITINGS 学习记录

go学习记录——第十六天

学习记录第 15 / 61 篇

测试一个值是否实现了某个接口

下面是类型断言那一part里的一个特例:假定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口,可以用:

1
2
3
4
5
6
type Stringer interface {
String() string
}
if sv, ok := v.(Stringer); ok {
fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}

Print() 函数就是如此检测类型是否可以打印自身的。

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

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

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

而标准库中都是使用了这个原则(所以需要看标准库的源码)

接下来会用两个例子去深入理解并掌握它们

使用方法集和接口

在前文第 10.6.3 节及例子methodset1.go 中我们看到,作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型的值时,这会变得有点复杂,原因是接口变量中存储的具体值时不可寻址的,当然如果使用不当编译器会给出错误提示。

示例methodset2.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 (variable of type List) as type valuable in argument to LongEnough:
// List does not implement valuable (getValue method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) {
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")
}
}

输出结果:

1
- plst is long enough

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

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

总结:

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

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

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

PS:

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

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

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

  • 类型 T 的可调用方法集不包含接受者为 *T 的方法

例子1:使用sorter接口排序

这里将以 Sort 包为例子,对一组数字或字符串进行排序,只需要实现三个方法:反应元素个数的 Len() 方法、比较第 i 和 j 个元素的 Less(i, j) 方法以及交换第 i 和 j 个元素的 Swap(i, j) 方法。

排序函数的算法只会用到这三个方法(和Python不一样诶,这里使用的是冒泡排序)

1
2
3
4
5
6
7
8
9
func Sort(data Sorter) {
for pass := 1; pass < data.Len(); pass++ {
for i := 0; i < data.Len()-pass; i++ {
if data.Less(i+1, i) {
data.Swap(i, i+1)
}
}
}
}

两层循环的解释:

外层循环:控制轮数,轮数最多为长度减1,因为每层至少有一个元素会被放在正确的位置上。

内层循环:遍历未排序的元素,data.Len() - pass 保证经过一轮后,已排序的最大元素不会被再次比较。

Sort 函数接收一个接口类型的参数: Sorter ,它声明了如下方法:

1
2
3
4
5
type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

参数中的 int 是待排序序列长度的类型,而不是要排序的对象一定要是一组 int。i and j 表示元素的整型索引,长度也是整型的。

如果现在相对一个 int 数组进行排序,必须要做的事情就是:为数组顶一个类型并且在它上面实现 Sorter 接口的方法:

1
2
3
4
type IntArray []int
func (p IntArray) Len() int {return len(p)}
func (p IntArray) Less(i, j int) bool {return p[i] < p[j]}
func (p IntArray) Swap(i, j int) {p[i], p[j] = p[j], p[i]}

下面是一个调用排序函数的例子:

1
2
3
data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
a := sort.IntArray(data)
sort.sort(a)

接下来是完整的代码sort.go and sortmain.go

示例 sort.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package sort

type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

func Sort(data Interface) {
for i := 1; i < data.Len(); i++ {
for j := i; j > 0 && data.Less(j, j-1); j-- {
data.Swap(j, j-1)
}
}
}

func IsSorted(data Interface) bool {
n := data.Len()
for i := n - 1; i > 0; i-- {
if data.Less(i, i-1) {
return false
}
}
return true
}

type IntArray []int

func (p IntArray) Len() int { return len(p) }
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }
func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

type Float64Array []float64

func (p Float64Array) Len() int { return len(p) }
func (p Float64Array) Less(i, j int) bool { return p[i] < p[j] }
func (p Float64Array) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

type StringArray []string

func (p StringArray) Len() int { return len(p) }
func (p StringArray) Less(i, j int) bool { return p[i] < p[j] }
func (p StringArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

// Convenience wrappers for common cases
func SortInts(a []int) { Sort(IntArray(a)) }
func SortFloat64s(a []float64) { Sort(Float64Array(a)) }
func SortStrings(a []string) { Sort(StringArray(a)) }

func IntsAreSorted(a []int) bool { return IsSorted(IntArray(a)) }
func Float64sAreSorted(a []float64) bool { return IsSorted(Float64Array(a)) }
func StringsAreSorted(a []string) bool { return IsSorted(StringArray(a)) }

这里面要注意的就是 Sort 函数是j--和j-1,众所周知由于编写习惯都会写++。我最开始写完就是报错,查了之后才发现这里写错了。然后Sort记得放在Sort包下,别和sortmain在一个包里

示例sortmain.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
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
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"fmt"
"myproject/chapter11/sort"
)

func ints() {
data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
a := sort.IntArray(data)
sort.Sort(a)
if !sort.IsSorted(a) {
panic("fail")
}
fmt.Printf("The sorted array is: %v\n", a)
}

func strings() {
data := []string{"monday", "friday", "tuesday", "wednesday", "sunday", "thursday", "", "saturday"}
a := sort.StringArray(data)
sort.Sort(a)
if !sort.IsSorted(a) {
panic("fail")
}
fmt.Printf("The sorted array is: %v\n", a)
}

type day struct {
num int
shortName string
longName string
}

type dayArray struct {
data []*day
}

func (p *dayArray) Len() int { return len(p.data) }
func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }

func days() {
Sunday := day{0, "SUM", "Sunday"}
Monday := day{1, "MON", "Monday"}
Tuesday := day{2, "TUE", "Tuesday"}
Wednesday := day{3, "WED", "Wednesday"}
Thursday := day{4, "THU", "Thursday"}
Friday := day{5, "FRI", "Friday"}
Saturday := day{6, "SAT", "Saturday"}
data := []*day{&Sunday, &Monday, &Tuesday, &Wednesday, &Thursday, &Friday, &Saturday}
a := dayArray{data}
sort.Sort(&a)
if !sort.IsSorted(&a) {
panic("fail")
}
for _, d := range data {
fmt.Printf("%s", d.longName)
}
fmt.Printf("\n")
}
func main() {
ints()
strings()
days()
}

输出结果

1
2
3
The sorted array is: [-5467984 -784 0 0 42 59 74 238 905 959 7586 7586 9845]
The sorted array is: [ friday monday saturday sunday thursday tuesday wednesday]
SundayMondayTuesdayWednesdayThursdayFridaySaturday

PS:

panic('fail')用于停止处在非正常状态下的程序(详细参考第13章),当然也可以先打印一条信息,然后调用os.Exit(1)来停止程序。

当然,众所周知Sort都是标准库,所以正常调用直接用就行,不用自己写。对于一般性的排序,sort 包定义了一个接口:

1
2
3
4
5
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

这个接口主要是总结了用于排序的抽象方法,函数Sort(data Interface) 用来对此类对象进行排序,可以用它们来实现对其他类型的数据(非基本类型)进行排序。在上面的例子中,我们也是这么做的,不仅可以对 int 和 string 序列进行排序,也可以对用户自定义类型 dayArray 进行排序。

例子2:读和写

读和写作为程序中常见的操作,一提起就会想到读写文件、缓存、标准输入输出(万恶的ACM模式)、标准错误以及网络连接、管道等,或者读写自定义类型。为了让代码尽可能通用,Go采用的是一致的方式来读写。

io 包提供了读和写的接口 io.Reader 和 io,Writer:

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

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

只要类型实现了读写接口,提供 Read 和 Write 方法,就可以从它读取数据,或者向它写入数据。一个对象要是可读的,它必须实现 io.Reader 接口,这个接口只有一个签名是 Read(p []byte) (n int, err error) 的方法,它从调用它的对象上读取数据,并把读取到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 error 对象。如果没有错误发生返回 nil ,如果已经到达输入数据的尾端,会返回 io.EOF("EOF") ,如果读取的过程中发生了错误,就会返回具体的错误信息。类似的写操作也是一样的。

io 包中的 Reader 和 Writers 都是不带缓冲的,bufio 包里提供了对应的带缓冲操作,在读写 UTF-8 编码的文本时尤其有用。

所以在实际编程中,尽可能使用这些接口,能够使程序更加通用,可以在任何实现了接口的类型上使用读写方法。

空接口

概念

一个概念一出我就瞬间想到了那几步XXX的历史,XXX的由来,XXX的定义,XXX的现状,XXX的未来,XXX的应用

空接口或者最小接口不包含任何方法,它对实现不做任何要求:

1
type Any interface {}

任何其他类型都实现了空接口(它不仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。

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

示例empty_interface.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
30
31
32
33
34
35
36
37
38
package main

import "fmt"

var i = 5
var str = "ABC"

type Person struct {
name string
age int
}

type Any interface{}

func main() {
var val Any
val = 5
fmt.Printf("val has the value: %v\n", val)
val = str
fmt.Printf("val has the value: %v\n", val)
pers1 := new(Person)
pers1.name = "Rob Pike"
pers1.age = 42
val = pers1
fmt.Printf("Person 1: %v\n", val)
switch t := val.(type) {
case int:
fmt.Printf("Type int %T\n", t)
case string:
fmt.Printf("Type string %T\n", t)
case bool:
fmt.Printf("Type bool %T\n", t)
case *Person:
fmt.Printf("Type *Person %T\n", t)
default:
fmt.Printf("Type unknown %T", t)
}
}

输出结果

1
2
3
4
val has the value: 5
val has the value: ABC
Person 1: &{Rob Pike 42}
Type *Person *main.Person

在上述例子中,接口变量 val 被依次赋予一个 int, string 和 Person 实例的值,然后使用 type-switch 来测试它的实际类型。每个 interface() 变量在内存中占据两个字长:一个用来存储它所包含的类型,一个用来存储它所包含的数据或指向数据的指针。

示例emptyint_switch.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
30
31
32
33
package main

import "fmt"

type specialString string

var whatlsThis specialString = "hello"

func TypeSwitch() {
testFunc := func(any interface{}) {
switch v := any.(type) {
case bool:
fmt.Printf("any %v is a bool type", v)
case int:
fmt.Printf("any %v is an int type", v)
case float32:
fmt.Printf("any %v is a float32 type", v)
case float64:
fmt.Printf("any %v is a float64 type", v)
case string:
fmt.Printf("any %v is a string type", v)
case specialString:
fmt.Printf("any %v is a special String!", v)
default:
fmt.Printf("unknown type!")
}
}
testFunc(whatlsThis)
}

func main() {
TypeSwitch()
}

输出结果

1
any hello is a special String!

该示例中说明了空接口在 type-switch 中联合 lambda 函数的用法

构建通用类型或包含不同类型变量的数组

在 7.6.6 中我们看到了能被搜索和排序的 int数组、float 数组以及string 数组,那么对于其他类型的数组呢?(莫名想到leetcode)

具体实现就是通过一个空接口,让我们给空接口定一个别名类型 Element: type Elemet interface{}

然后定义一个容器类型的结构体 Verctor,它包含一个 Element 类型元素的切片:

1
2
3
type Vector struct{
a []Element
}

Vector 里能够放任何类型的元素,因为任何类型都是先了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回 第 i 个元素:

1
2
3
func (p *Vector) At(i int) Element {
return p.a[i]
}

再定一个 Set() 方法用于设置第 i 个元素的值:

1
2
3
func (p *Vertor) Set(i int, e Element) {
p.a[i] = e
}

Vector 中存储的元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。TODO: The compiler rejects assertions guaranteed to fail ,类型断言总是在运行时才执行,因此它会产生运行时错误。

复制数据到切片至空切片接口

假设有一个 myType 类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:

1
2
var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = dataSlice

可惜的是这样会直接报错 cannot use dataSlice (type []myType) as type []interface { } in assignment。

原因是他俩在内存中的布局是不一样的(参考go wiki)

必须使用 for-range 语句来一个一个显式地赋值:

1
2
3
4
5
var dataSlice []myType = FuncReturnSlice()
var interfaceSilce []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
END OF ARTICLE回到顶部 ↑
← 返回文章列表

CONVERSATION / 留言

留言与讨论

欢迎留下想法,评论由 Waline 提供。

评论正在加载…