go学习记录——第十六天

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

下面是类型断言那一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 的可调用方法集包含接受者为 *TT 的所有方法集

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

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

例子1:使用sorter接口排序

这里将以 Sort 包为例子,对一组数字或字符串进行排序,只需要实现三个方法:反应元素个数的 Len() 方法、比较第 ij 个元素的 Less(i, j) 方法以及交换第 ij 个元素的 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 是待排序序列长度的类型,而不是要排序的对象一定要是一组 inti 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) 用来对此类对象进行排序,可以用它们来实现对其他类型的数据(非基本类型)进行排序。在上面的例子中,我们也是这么做的,不仅可以对 intstring 序列进行排序,也可以对用户自定义类型 dayArray 进行排序。

例子2:读和写

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

io 包提供了读和写的接口 io.Readerio,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)
}

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

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

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

空接口

概念

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

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

1
type Any interface {}

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

空接口类似 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, stringPerson 实例的值,然后使用 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
}

go学习记录——第十六天
https://www.lx02918.ltd/2025/03/06/go-study-sixteenth-day/
作者
Seth
发布于
2025年3月6日
许可协议