0. 二级指针
不知为啥某种原因这个帖子会在 bing 和 google 搜索 "golang 二级指针" 出现在前几位(百度没有)。我觉得我有责任在这里解释一下 golang 二级指针的运作。
- 指针是值的地址。所有的变量都可以寻址,但不是所有的值都可以。像是
1
,2
这种值是没有地址的,但是 struct 值是有地址的(其实准确来说不是,struct之所以能直接在值前面加&,是一个语法糖,具体见我的这篇文章)。&
只是个取地址的运算符,后面的值可以寻址就可以,不可以就会报错
- 此外,指针本身也是值,且这个值和
1
,2
一样,是不可以寻址的。这也是为什么&(&a)
不行,但是 先b = &a
,再&b
却可以。- 虽然指针本身没有地址,但是把这个值赋值给变量就有了,这也是为什么会有二级指针的说法,只有把地址赋值给一个变量,才能产生二级指针。
- 各种帖子里经常混用 指针,指针变量和指针类型,统一都叫指针。这里讲一下它们之间的区别,以
p := &x
为例。- p 是指针变量,
- p 存储的那个值,也就是地址,是指针。
- *x 为指针类型,在你打印类型的时候才会看到。
- 以后看这种帖子的时候,就要注意帖子里想表达的是哪个指针。
- 这里写一下如何得到一个二级指针,假设有两种情况:
- 原数据的值本身就是有地址的:
var a = &MyStruct{"hi"} // 把结构体的地址取出来,赋值给一级指针(a) var b = &a // 把一级指针(a)的地址取出来,赋值给二级指针(c)
- 原数据的值本身是没有地址的:
var a = 1 // int 类型没有地址,只能先赋值给一个变量(a) var b = &a // 把变量 a 的地址取出来,赋值给一级指针(b) var c = &b // 把一级指针(b)的地址取出来,赋值给二级指针(c)
- 原数据的值本身就是有地址的:
- 然后来看几个例题,例题永远是理解的最佳途径:
- 第一个例题代码如下,请问输出什么?
func main() { x := 0 p := &x test(&p) fmt.Println(*p) } func test(p **int) { x := 100 *p = &x }
- 答案是:100
- 还是同样的代码,把输出改成
fmt.Pritln(x)
,答案是什么?- 答案是:0 这里可以用这么一张图来解释:
- 首先在运行 test 里的
*p = &x
前,内存里是这么一个结构(内,外,分别指 test 函数内和 test 函数外)。需要注意的是这么几点:
- 指针变量不会指向内存,只有地址会指向内存,指针变量只是储存了地址的值罢了。比如上图中的 0x0001 指向 test 函数外的 x 变量存放的地址。但是 p(外) 是一个指针变量,本身只是一个值,这个值就是 0x0001,本身并不指向 x(外)。
- 也就是说,
p(内)
的值是0x0002
,*p(内)
的值是0x0001
,**p(内)
的值是 0。 - 注意,你永远无法改变地址的指向(即使是指针左值也不行),只能改变指针变量储存的是哪个地址(指针左值实际干的)。
- 然后再运行完
*p = &x
后,图变成这样了:需要注意的是:
- 你可能以为
*p(内) = &x
改变的是 p(内)的指向,但是不是。首先左边*p(内)
寻址操作,得到的是 p(外),而右边&x
取出x(内)
的地址也就是0x0004
。那么整个操作相当于p(外)= 0x0004
。 - 总结下来就是,想要得到地址就只能使用 &,而 * 是直接把你带到内存,如果那个内存上有变量,就可以直接改变那个变量。
- 你可能以为
- 然后这里再加一个变体不改变 *p,但是引入一个中间变量,观察这个中间变量的改变:
答案是:100,会被改变。画成图如下所示:func main() { x := 0 p := &x pp := &p // 把二级指针存入一个变量,当然结果不会变,但你能画出 pp 在哪个位置吗? test(pp) fmt.Println(**pp) // 请问 **pp 现在是什么,会被改变吗? } func test(p **int) { x := 100 *p = &x }
- 虽然 pp 是一个新的变量,但是使用 * 后,就回到了旧的变量。
*pp
相当于p
。**pp
相当于*p
相当于x
。
- 第一个例题代码如下,请问输出什么?
原文章从这里开始
今天的学习资料是 A Tour of Go。
1. 基础语法
go 的一些特殊的语法:
1.1 变量
- 在函数外面只能声明变量,不能写其他代码。且在函数外就不能用
:=
了。 - var 可以声明多个变量和初始化, 有初始化值就不用写类型了。
- 没有初始化的值都有一个 零值,数字为 0,布尔为 false,字符串为 "" ,其他为 nil。
- 常量不能用
:=
,必须是const x = xx
形式。
1.2 判断/循环
- for 和 if 不能被括号包裹,但这指的是最外层。里层有括号没问题。
- go 里没有 while,for 同时担任两种循环的工作。
- if/switch 里也可以和 for 一样初始化一个变量,作用域只在 if/switch/for 里。
- 无限循环不用写
for true {}
,直接一个for {}
即可。 - 和其他语言相反,switch 不用写 break,会自动添加 break。相反,如果想要运行连续的 case,要加上
fallthrough
关键字。 - switch 可以直接用
switch {}
,然后可以把 case/default 当做多个 if/else 来用,可以优化多重 if/else 结构。
1.3 defer
defer
关键字后接一个函数调用,虽然函数调用的参数还是会被立刻求值,但函数的调用会延迟到最外层的函数运行完毕才会执行。- 如果有多个 defer,则会进入一个 栈 的结构,也就是说先进后出。
1.4 指针
- go 里也有指针,语法和 c 一样。不过没有指针运算。
1.5 struct
- go 里也有 struct。
type struct MyStruct { int A int B } func main() { m := MyStruct{1, 2} // 或者 || var m MyStruct // || m.A = 1 // || m.B = 2 pm := &m // 以下三个结果都一样, 都是 1,需要注意第二个 // 第二个按逻辑来说是不对的,但是 go 简化了,使用 pm.A 就相当于使用 (*pm).A m.A pm.A (*pm).A }
// 多种声明方式 m1 = MyStruct{1, 2} m2 = MyStruct{A: 1} // B 为零值,该情况为 0 m3 = MyStruct{} // 全部为零值 pm4 = &MyStruct{1, 2} // 一个 struct 指针
1.6 array/slice
- 中括号写在前面,如
var a = [<number>]<type>
表示有<number>
个类型为<type>
的元素构成的数组。 - slice 在某种程度上可以看做是可变数组,不写
<number>
即可。- 但是 slice 本身还是类似于 python 上的 slice,是对数组的一部分的引用,比如说
// 先初始化一个不可变的 array,类型为 [5]int var a = [5]int{1,2,3,4,5} // 切片后,a[1:3] 的类型立刻变成 []int,变成可变的了 // 如果此时你在 []int 前面的括号里写一个数字,反而会报错 // 因为 a[1:3] 是切片,即使你知道他有 3 个元素,它的类型就是 []int // []int 和 [3]int 是不同的类型。所以不能 assign var b []int = a[1:3] // 用法和 python 完全一样,也是 [1,3) // 如果直接声明一个 slice 而不用旧有数组,其实背后还是新建了一个数组 var c = []int{1,2,3,4,5} // 你以为是直接创建了一个 slice?不是 // 背后其实相当于: var c1 = [5]int{1,2,3,4,5} var c2 =c1[:]
- 以上代码中,其实 b 仍然是对 a 的一部分的引用,如果 a 改变了,b 也会改变,反之亦然。并且如果有多个切片引用了 a 的同一个部分,也都会跟着改变。
- 但是 slice 本身还是类似于 python 上的 slice,是对数组的一部分的引用,比如说
- slice 拥有长度和容量。分别用 len() 和 cap() 来获取:
- 长度就是 slice 有几个元素
- 容量比较复杂,和原数组相关,是切片的第一个元素在原数组中的位置,到原数组最后一个元素的长度。 对切片再次切片的话,这里和 python 不一样,需要用到容量:
好,你以为你懂了,但其实你没懂,再看个例子:// 注意:本例从头到尾只有一个变量 s var s = []int{0,1,2,3,4} s = s[0:4] // 这里很正常,现在 s 为 []int{0,1,2,3} s = s[2:5] // 想不到吧,这一步 s 的输出居然为 [int]{2,3,4},仿佛上一步没有改变 slice 一样 // 你是不是认为后面的 index 会越界? // 不,并不会,这一步 s 的输出为 [int]{2,3,4} // ? // 但是这并不是说 s 没有变,s 确实变了,只是此时切片切的是底层数组
为什么会这样呢?// 前面一样 var s = []int{0,1,2,3,4} s = s[0:4] // 这一步也一样 s = [2:5] // 现在呢? s = [0:3] // 你说我懂了,这一步输出是 []int{0,1,2},因为是对原数组进行 slice // 不,你没懂,这一步输出是 []int{2,3,4} // ??
- slice 其实是一个包左不包右的概念,当进行切片后,左边的元素不再包含在该切片中了,但是右边的元素依然包含在切片中,只是不被打印出来,也不能访问,只是能再次切片。这就是长度和容量的区别。
- 不过无论是长度还是容量,都是指针。改变切片的切片,底层数组,以及所有的相关的 slice 都会跟着改变。slice 永远都是引用。
- 如前面所说,slice 的零值是 nil。要求长度,容量都为0,且没有底层数组。
- 使用 make 创建切片:
make([]int, 0, 5)
的意思是创建一个 slice,长度为 0,容量为 5。初始值都是零值,这里是 int,所以都是 0。 - 多重切片,就是两个空的中括号。
- 添加元素,前面说道切片是可变的,也包括增加元素,go 提供了一个 append 函数,第一个参数为切片,后面可以接多个参数,依次添加到切片后面,类型要一致。该函数返回新的切片。
- 注意1: append 后如果容量不够,go 会自动扩充容量。这样底层数组就改变了,那么改变新 slice,旧的 slice 和底层数组并不会改变,因为引用的已经不是同一个数组了。
- 注意2: 上面提到 append 会扩充容量,但并不一定和需要扩充的完全一致,有时候切片的容量会比长度要大一些,也就是说容量此时是不可控的。比如说,现在有一个长度为 1,容量为 1 的 slice。使用 append 加一个元素。容量不够,所以还需要加一个容量。但是 append 可能会增加两个容量。想要完全控制可以使用 AppendByte。
- range 遍历切片时,返回两个值,一个 index,一个 value
- make 只能创建一维 slice,二维只能用 for 循环写那种垃圾代码。
- slice 被封装过了,本身就是指针,不需要加 *。加了就是二级指针了。
- 这里需要重点提一下,当 slice 作为函数参数的时候的几种情况:
- slice 由于本身就是地址,所以如果只是对 slice 内的元素进行改变的时候,完全可以直接传 slice。如:
func MyFunction(numbers []int) {}
。 - 要注意:go 的函数是完全的值传递,numbers 传进来的时候,外面的变量记录了一个地址(假设为 0x11111),进入函数后,函数复制了这个地址,目前这两个地址相同(理解了吗?地址本身也是一个值,作为值传递进函数内部,只是这时候正好内外都是同一个地址)。对这个地址进行操作,自然内部也能改变外部。
- 但是!!! ,如果内部对 slice 进行了 append 操作,即使是类似
numbers = append(numbers, 1)
,这样是无法改变外部的值的。 - ???为什么
- 因为 append 新创建了一个地址(假设为 0x22222),然后把这个值赋值给 numbers。但是外部的变量记录的依旧是旧地址 0x11111。所以自然不会更改。
- 相比之下,一个普通的用户定义的结构体,由于没有 slice 的语法糖,所以即使要改变自身的属性,也需要加一个指针变成一级指针。如果想要新建一个结构体代替原结构体,则需要加两个星号 **MyStruct 变成二级指针。例子如下,好好看看:
package main import "fmt" type Person struct { name string age int } func (p *Person) changeAge() { p.age = 22 } // 一级指针方法 func (p *Person) changePersonFirstLevelPointer() { p = &Person{"shabi", 25} } // 二级指针方法 func (p *Person) changePersonSecondLevelPointer() { *p = Person{"shabi", 25} } func main() { var person = Person{"zouli", 17} fmt.Println("初始化为:", person) // 一级指针,可以更换属性 // 所以这里年龄变成 22 // (直接用 person 而无需 &person,这是 go 的语法糖,你想加也可以) person.changeAge() // 也可以是 (&person).changeAge() fmt.Println("一级指针可以改变属性:", person) // 一级指针不能变换自身 // 所以这里依旧是 22 person.changePersonFirstLevelPointer() fmt.Println("一级指针无法改变自身:", person) // 但是二级指针可以改变自身 // 这里可以看到 "zouli" 变成了 "shabi",17 变成了 25 person.changePersonSecondLevelPointer() fmt.Println("二级指针可以改变自身:", person) }
- slice 由于本身就是地址,所以如果只是对 slice 内的元素进行改变的时候,完全可以直接传 slice。如:
- 这里需要重点提一下,当 slice 作为函数参数的时候的几种情况:
1.7 映射,也就是 map
- 使用 make 生成 map。map 的零值是 nil,nil 没有键值对,也不能添加键值对。
- 键值对之间也是用的冒号。
- 基本用法和 python 一样。不过要注意的是,map 中查找不存在的键值对,会返回零值,而不是报错说不存在。
- map 的值可以被双赋值读取,如下:
var map = make(map[int]int) map[1] = 2 var res1 := map[1] // map[1] 是 int,所以赋值给 res1 也是 int var res2, ok := map[1] // map[1] 依然是 int // 但这个特殊语法,会给 ok 赋值,如果值不存在则 ok 为 false // 但这并不是说 map[1] 是什么特殊类型,不是,这就只是一种语法 // 如果先把 map[1] 赋值给其他变量,再双赋值,就会报错说不能把一个值赋给两个值 var temp = map[1] var res, ok = temp // 报错,因为 temp 和 map[1] 只是一个 int 罢了
1.8 函数
- go 的函数也是值,可以当做值来用,可以当做参数,赋值等等。
- 拜此所赐,go 的函数可以闭包,对,就是和 javascript 的闭包一模一样。
// 斐波那契闭包
package main
import "fmt"
func fibonacci() func() int {
var first, second = 0, 1
fmt.Println(0)
fmt.Println(1)
return func() int {
var sum = first + second
first = second
second = sum
return sum
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
2. 方法
2.1 使用
go 不是面型对象语言,没有类。
但是可以给结构体定义方法。
go 中的方法就是一类特殊的函数,依然用 func 来定义。只是相比普通的函数,多了接受者参数,存在于 func 关键字和方法名之间。
type MyStruct struct {
}
func (m MyStruct) Method(<方法的参数>) <return type> {
// do sth
}
这里给 MyStruct 结构体定义了一个 Method 方法。
方法除了多了一个接受者参数以外,和函数没有任何区别。
可以设置多个同名方法,只要接受者类型不一样。
2.2 可以给谁定义方法?
可以给所有的在同一个包内的类型定义方法。
不能给其他包内的类型定义方法。
这也是为什么不能给 int 这种内置类型建立方法,因为这种内置类型在 go 自己的包里,不在你的包里。
但是如果使用 type 建立一个内置类型的 type,居然是可以建立方法的,比如:
type MyFloat64 float64
func (mf MyFloat64) Minus() MyFloat64 {
// do sth
}
2.3 指针接受者方法
接受者参数可以以指针形式传入。
指针接受者方法的好处在于,可以在方法里直接修改接受者的属性。由于这个特性,指针接受者方法比值接受者方法更常用。
使用指针方法和使用值方法一样,无需主动给结构体加指针,go 会自动识别并加上指针。
type MyStruct struct {
X int,
Y int
}
// 值接受者
func (m MyStruct) Method1() int {
return m.X + m.Y
}
// 指针接受者
// 这里更改结构体的属性值
// 如果这里没有加 * ,则不会改变结构体的属性
func (m *MyStruct) Method2 {
m.X = 100
m.Y = 200
}
var ms = MyStruct(1,2)
fmt.Println(ms.Method1()) // 3
ms.Method2()
fmt.Println(ms.Method1()) // 300
2.4 指针接受者和指针作为参数的函数的区别:
比如:
func (m *MyStruct) Method1 {
// do sth
}
func Method2(m *MyStruct) {
// do sth
}
// 想要使用 Method2,必须传入一个指针参数
var ms = MyStruct{1, 2}
var pms = &ms
Method2(ms) // 报错
Method2(pms) // 正常
// 想要使用 Method1,则无论是不是指针都可以调用
ms.Method1() // 正常
pms.Method1() // 正常
/*
* 因为 go 发现 Method1 是个指针接受者方法之后,会自动把 ms 转成 &ms
* 所以以上两句是相同的效果
*/
2.5 值接受者和值作为参数的函数的区别
其实是一样的感觉,值接受者方法,无论是值还是指针都可以用,指针会被解释为 *ms 取值。而函数则必须对应。
2.6 使用值接受者还是指针接受者
肯定是指针接受者。
不仅比值接受者多一个可以改变自身的功能以外。对于大型结构体来说,还能节省复制的空间。(指针直接引用就行了,而值则要复制一个新的出来)
3. 接口
接口很重要,go 可是自称面向接口式编程。
接口是一组方法构成的集合。用于保存实现了实现这些方法的值(下一节讲这是什么意思)。
接口的使用过程如下:
- 首先定义一个接口,里面有一些方法。
- 然后我们用 type 定义一个类型。(记得吗?因为内置类型不在当前包里,所以没有方法)
- 然后对照接口,对这个类型实现所有接口上的方法。
- 这个时候,这个类型就隐式地实现了这个接口,无需专门声明这个类型实现了这个接口。(因为无需,所以 go 甚至没有做显式声明的关键字)
- 由于没有显式声明类型和接口之间的关系,接口和类型之间是解耦的。
接口可以看做是一个规则,比如一个洗衣机的接口,我们规定有洗衣和甩干两个方法,只要任何类型实现了这两个方法,我们就叫它洗衣机。这个类型是什么完全不用管。
3.1 接口的使用
上面说过接口用于保存实现了这些方法的值,这是什么意思呢?不是说接口是一个规则吗?
- 实际上看接口的语法就会发现,interface 的使用和 struct 一样,使用 type 新建一个类型。这个新类型可以看做一个规则。
- 但是实际使用的时候,我们使用该类型创建一个变量,这个变量则用于储存一个值,这个值一定要实现接口规则所规定的方法,不然会报错。
// 接口
type MyInterface interface {
M()
}
// 没有实现 M 方法的类型,所以不属于 MyInterface
type MyStruct1 struct { A int }
// 实现了 M 方法,所以属于 MyInterface
// 只要实现 M 方法就属于,不需要(也没法)刻意去说 MyStruct2 属于 MyInterface
type MyStruct2 struct { A int }
func (ms *MyStruct2) M() {
ms.A = 99999
}
func main() {
var i MyInterface
fmt.Printf("%v, %T\n", i, i) // "<nil>, <nil>"
i = &MyStruct1{1} // 报错
i = &MyStruct2{1} // ok
fmt.Printf("%v, %T\n", i, i) // "&{1}, *main.MyStruct2"
i.M()
fmt.Printf("%v, %T\n", i, i) // "&{99999}, *main.MyStruct2"
}
3.2 接口值
接口本身也是值,可以作为参数传入函数和作为返回值。
- 接口刚被初始化的时候,值为 nil,type 也为 nil,不能调用方法。
只有给接口保存一个值后,接口才有值和type,值和 type 和保存的值一模一样。 - 此外,当传入接口的值是 nil 的时候,仍然可以调用方法。
3.3 空接口
当接口里不声明任何方法的时候,叫做空接口。type MyEmptyInterface interface{}
按照定义,空接口自然属于所有的类型。因为所有类型都实现了 0 个方法。
所有类型,也就是说,包括内置类型。
空接口的作用:
由于任意类型都实现了接口,所以接口可以储存任意值。
那么我们可以把这个特性用于 map 储存任意值,比如 make(map[string]interface{})
就可以摆脱 map 只能储存固定值的限制了。
3.4 类型断言
类型断言 提供了访问接口值的底层具体值的方式。
3.4.1 一个返回值
v = i.(<type>) // <type> 是一个具体的类型
以上语句的意思是,断言接口值 i 的底层值得类型是 <type>
。
- 如果不是的话,这条语句会触发 panic。
- 如果是的话,当然就正常运行了。返回值 v 为 i 的底层值,类型为
<type>
3.4.2 两个返回值
此外,如果不想触发 panic 的话,可以使用如下语句:
v, ok := i.(<type>)
注意
- 这样的话,即使 i 不是
<type>
类型,也不会触发 panic。但是 ok 会被赋值为 false。
但是 v 仍然会变成<type>
类型,值为零值。 - 如果是的话,ok 为 true。
3.5 类型选择
- 如果类型断言是为了判断一个接口值是不是属于某个类型。
- 那么类型选择就是根据接口值的类型进行不同的处理。
语法和 switch 很像,只是 case 为类型罢了:
注意:这里是 type 不是 <type>
,这里 type 是个关键字。
注意2:类型选择不是多个类型断言,是用来对不同类型进行不同处理的,不是用来判断类型是不是想要的类型的
switch v := i.(type){
case A:
// i 的类型为 A
case B:
// i 的类型为 B
default:
// i 不为以上任何类型
}
其实就是在对 i 的类型进行监测,不同的类型作不同的处理。检测完顺便还给 v 赋个值。
3.6 interface 的不同应用方式
虽然上面说的 interface 很有用的样子,但确实光是这么说确实不好看出来。解耦在哪里?我怎么看不出来?
3.6.1 Stringer 接口
这里举个例子:
- 在 fmt 包里有一个接口叫做 Stringer,里面定义了一个方法 String,这个方法用于把自己用字符串的形式打印出来。
- ↑ 这是什么意思呢?
- 比如说 fmt 里的 Println 函数,假设你想要打印一个 struct,但是又不想 fmt 打印出来
{xx, xx}
这种形式,想弄个更好看的,怎么办?
Stringer 接口里的 String 方法就是起这个作用的,你只需要给你定义的 struct 定义一个叫做 string 的方法,返回你想要打印出来的字符串就行了。无需专门提到这是为了 fmt 的这个 Stringer 接口写的方法。
fmt.Println() 会自动调用 String 方法。
/*
* 不定义 String 方法
*/
type Person {
name string,
age int
}
func main() {
fmt.Println(Person{"zouli", "17"}) // 默认打印 "{zouli, 17}"
// 不好看,没意思
}
/*
* 定义 String 方法
*/
func (p *Person) String() string { // 注意名字一定要是 String
return fmt.Sprintf("我不是 %v, 你才是,你全家都是。", p.name)
}
func main() {
fmt.Println(Person{"zouli", "17"}) // 打印 "我不是 zouli, 你才是,你全家都是。"
// 很有趣
}
有没有发现,完全没有出现 Stringer 的字眼,只有 String。接口由 fmt 定义,而我们只需要实现方法即可,这就是解耦。题外话:除了 fmt,还有很多包都用这个 String 来打印,可以记一下这个名字。
想象一下,你可以写一个包,里面定义了一个接口,但你完全可以不用自己定义里面的方法,而只写一些规则,比如说一定要返回一个字符串。然后你在你的包里的一些 exported 的函数里对调用这个方法,即使现在还没有这个方法。然后别人用你的包,直接调用你的函数就会报错没有这个方法,但是一旦别人定义好自己的方法,符合你定义的接口规则,就可以调用这个方法了。
当然,最好还是在包里定义一个默认的方法。不然用个包还要自己定义方法就太烦了。
通过这种方式,fmt 无需理会值到底是什么结构,直接使用用户定义的方法即可。
3.6.2 error 接口
error 也是一个内置接口,不过不在 fmt 包中,就是在 go 中。实现 error 接口需要实现 Error 方法,该方法同样返回一个 string。
- 这里需要重新提一下接口值,之前提到过接口可以当做函数返回值来用。这里就经常用到。
- 一个 error 常用的方式就是,定义一个函数,这个函数返回多个值,最后一个值是 error 类型。那么怎么得到这个 error 类型呢?当然就是自定义一个类型,然后给这个类型实现一个 Error 方法即可。
- 当然,如果不想自己定义和实现,也可以用 errors 包里的 errors.New() 方法得到 可以在该页面上看看。
以及,fmt.Println 在打印 error 接口值的时候,会先寻找有没有实现 Error 方法,如果没有的话才会去寻找 String 方法。
这种方式是把接口当类型用,当然这也是因为 error 是一个内置接口,而不是其他包(fmt是标准库,所以 Stringer 不是内置接口)中。
3.6.3 Read 接口
go 中有许多的 io.Reader 接口,这些接口都有一个 Read 方法,如下:
func (r Reader) Read(b []byte) (int, error)
作用是,从 r 中尽量读取字节直到 EOF,全部存入 b 中,如果 b 放不下了,那么就等下一次调用该函数。返回一个 int,这表示这次读取了多少字节,这个要精确,不然输出会混乱。error 就不用解释了。
这里有点绕的地方就是,使用该方法后,改变的是 b 这个参数,不是调用的 r,要从面向对象思考模式里走出来。在进来之前,b 可能是 1024 大小的空切片,但是 r 里只有 3 个字节,使用完之后 b 的大小就只有 3 了。
可以在 Reader 里包裹一个 Reader,然后先调用外部的 Reader.Read() 方法,然后使用内部的 Reader.Read() 方法改写 b,然后对 b 进行一些操作,然后返回。这样就可以实现修改数据流的效果了。
具体见这里。
这种方式是通过接口嵌套,实现了一种类似继承的效果。(想想看为什么类似继承)