GoLang 学习笔记(三)-- slice 二级指针,接口的应用等

3,291 阅读23分钟

0. 二级指针

不知为啥某种原因这个帖子会在 bing 和 google 搜索 "golang 二级指针" 出现在前几位(百度没有)。我觉得我有责任在这里解释一下 golang 二级指针的运作。

  1. 指针是值的地址。所有的变量都可以寻址,但不是所有的值都可以。像是 12 这种值是没有地址的,但是 struct 值是有地址的(其实准确来说不是,struct之所以能直接在值前面加&,是一个语法糖,具体见我的这篇文章)。
    • & 只是个取地址的运算符,后面的值可以寻址就可以,不可以就会报错
  2. 此外,指针本身也是值,且这个值和 12 一样,是不可以寻址的。这也是为什么 &(&a) 不行,但是 先b = &a,再&b 却可以。
    • 虽然指针本身没有地址,但是把这个值赋值给变量就有了,这也是为什么会有二级指针的说法,只有把地址赋值给一个变量,才能产生二级指针。
  3. 各种帖子里经常混用 指针,指针变量和指针类型,统一都叫指针。这里讲一下它们之间的区别,以 p := &x 为例。
    1. p 是指针变量,
    2. p 存储的那个值,也就是地址,是指针。
    3. *x 为指针类型,在你打印类型的时候才会看到。
    4. 以后看这种帖子的时候,就要注意帖子里想表达的是哪个指针。
  4. 这里写一下如何得到一个二级指针,假设有两种情况:
    • 原数据的值本身就是有地址的:
      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)
      
  5. 然后来看几个例题,例题永远是理解的最佳途径
    1. 第一个例题代码如下,请问输出什么?
      func main() {
          x := 0
          p := &x
          test(&p)
          fmt.Println(*p)
      }
      
      func test(p **int) {
          x := 100
          *p = &x
      }
      
      • 答案是:100
    2. 还是同样的代码,把输出改成 fmt.Pritln(x),答案是什么?
      • 答案是:0 这里可以用这么一张图来解释:
    3. 首先在运行 test 里的 *p = &x 前,内存里是这么一个结构(内,外,分别指 test 函数内和 test 函数外)。 image.png 需要注意的是这么几点:
      • 指针变量不会指向内存,只有地址会指向内存,指针变量只是储存了地址的值罢了。比如上图中的 0x0001 指向 test 函数外的 x 变量存放的地址。但是 p(外) 是一个指针变量,本身只是一个值,这个值就是 0x0001,本身并不指向 x(外)。
      • 也就是说,p(内) 的值是 0x0002*p(内) 的值是 0x0001**p(内) 的值是 0。
      • 注意,你永远无法改变地址的指向(即使是指针左值也不行),只能改变指针变量储存的是哪个地址(指针左值实际干的)。
    4. 然后再运行完 *p = &x 后,图变成这样了: image.png 需要注意的是:
      • 你可能以为 *p(内) = &x 改变的是 p(内)的指向,但是不是。首先左边 *p(内) 寻址操作,得到的是 p(外),而右边 &x 取出 x(内)的地址也就是 0x0004。那么整个操作相当于 p(外)= 0x0004
      • 总结下来就是,想要得到地址就只能使用 &,而 * 是直接把你带到内存,如果那个内存上有变量,就可以直接改变那个变量。
    5. 然后这里再加一个变体不改变 *p,但是引入一个中间变量,观察这个中间变量的改变:
    func main() {
        x := 0
        p := &x
        pp := &p // 把二级指针存入一个变量,当然结果不会变,但你能画出 pp 在哪个位置吗?
        test(pp)
        fmt.Println(**pp) // 请问 **pp 现在是什么,会被改变吗?
    }
    
    func test(p **int) {
        x := 100
        *p = &x
    }
    
    答案是:100,会被改变。画成图如下所示: image.png
    • 虽然 pp 是一个新的变量,但是使用 * 后,就回到了旧的变量。*pp 相当于 p**pp 相当于 *p 相当于 x

原文章从这里开始

今天的学习资料是 A Tour of Go

1. 基础语法

go 的一些特殊的语法:

1.1 变量

  1. 在函数外面只能声明变量,不能写其他代码。且在函数外就不能用 := 了。
  2. var 可以声明多个变量和初始化, 有初始化值就不用写类型了。
  3. 没有初始化的值都有一个 零值,数字为 0,布尔为 false,字符串为 "" ,其他为 nil。
  4. 常量不能用 :=,必须是 const x = xx 形式。

1.2 判断/循环

  1. for 和 if 不能被括号包裹,但这指的是最外层。里层有括号没问题。
  2. go 里没有 while,for 同时担任两种循环的工作。
  3. if/switch 里也可以和 for 一样初始化一个变量,作用域只在 if/switch/for 里。
  4. 无限循环不用写 for true {},直接一个 for {} 即可。
  5. 和其他语言相反,switch 不用写 break,会自动添加 break。相反,如果想要运行连续的 case,要加上 fallthrough 关键字。
  6. switch 可以直接用 switch {},然后可以把 case/default 当做多个 if/else 来用,可以优化多重 if/else 结构。

1.3 defer

  1. defer 关键字后接一个函数调用,虽然函数调用的参数还是会被立刻求值,但函数的调用会延迟到最外层的函数运行完毕才会执行。
  2. 如果有多个 defer,则会进入一个 的结构,也就是说先进后出。

1.4 指针

  1. go 里也有指针,语法和 c 一样。不过没有指针运算。

1.5 struct

  1. 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

  1. 中括号写在前面,如 var a = [<number>]<type> 表示有 <number> 个类型为 <type> 的元素构成的数组。
  2. 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 的同一个部分,也都会跟着改变。
  3. slice 拥有长度和容量。分别用 len() 和 cap() 来获取:
    1. 长度就是 slice 有几个元素
    2. 容量比较复杂,和原数组相关,是切片的第一个元素在原数组中的位置,到原数组最后一个元素的长度。 对切片再次切片的话,这里和 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 永远都是引用。
  4. 如前面所说,slice 的零值是 nil。要求长度,容量都为0,且没有底层数组
  5. 使用 make 创建切片:make([]int, 0, 5) 的意思是创建一个 slice,长度为 0,容量为 5。初始值都是零值,这里是 int,所以都是 0。
  6. 多重切片,就是两个空的中括号。
  7. 添加元素,前面说道切片是可变的,也包括增加元素,go 提供了一个 append 函数,第一个参数为切片,后面可以接多个参数,依次添加到切片后面,类型要一致。该函数返回新的切片。
    • 注意1: append 后如果容量不够,go 会自动扩充容量。这样底层数组就改变了,那么改变新 slice,旧的 slice 和底层数组并不会改变,因为引用的已经不是同一个数组了。
    • 注意2: 上面提到 append 会扩充容量,但并不一定和需要扩充的完全一致,有时候切片的容量会比长度要大一些,也就是说容量此时是不可控的。比如说,现在有一个长度为 1,容量为 1 的 slice。使用 append 加一个元素。容量不够,所以还需要加一个容量。但是 append 可能会增加两个容量。想要完全控制可以使用 AppendByte。
  8. range 遍历切片时,返回两个值,一个 index,一个 value
  9. make 只能创建一维 slice,二维只能用 for 循环写那种垃圾代码。
  10. slice 被封装过了,本身就是指针,不需要加 *。加了就是二级指针了。
    • 这里需要重点提一下,当 slice 作为函数参数的时候的几种情况:
      1. slice 由于本身就是地址,所以如果只是对 slice 内的元素进行改变的时候,完全可以直接传 slice。如:func MyFunction(numbers []int) {}
      2. 要注意:go 的函数是完全的值传递,numbers 传进来的时候,外面的变量记录了一个地址(假设为 0x11111),进入函数后,函数复制了这个地址,目前这两个地址相同(理解了吗?地址本身也是一个值,作为值传递进函数内部,只是这时候正好内外都是同一个地址)。对这个地址进行操作,自然内部也能改变外部。
      3. 但是!!! ,如果内部对 slice 进行了 append 操作,即使是类似 numbers = append(numbers, 1),这样是无法改变外部的值的。
      4. ???为什么
      5. 因为 append 新创建了一个地址(假设为 0x22222),然后把这个值赋值给 numbers。但是外部的变量记录的依旧是旧地址 0x11111。所以自然不会更改。
      6. 相比之下,一个普通的用户定义的结构体,由于没有 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)
        }
        

1.7 映射,也就是 map

  1. 使用 make 生成 map。map 的零值是 nil,nil 没有键值对,也不能添加键值对
  2. 键值对之间也是用的冒号。
  3. 基本用法和 python 一样。不过要注意的是,map 中查找不存在的键值对,会返回零值,而不是报错说不存在。
  4. 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 函数

  1. go 的函数也是值,可以当做值来用,可以当做参数,赋值等等。
  2. 拜此所赐,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 可是自称面向接口式编程。

接口是一组方法构成的集合。用于保存实现了实现这些方法的值(下一节讲这是什么意思)。
接口的使用过程如下:

  1. 首先定义一个接口,里面有一些方法。
  2. 然后我们用 type 定义一个类型。(记得吗?因为内置类型不在当前包里,所以没有方法)
  3. 然后对照接口,对这个类型实现所有接口上的方法。
  4. 这个时候,这个类型就隐式地实现了这个接口,无需专门声明这个类型实现了这个接口。(因为无需,所以 go 甚至没有做显式声明的关键字)
  5. 由于没有显式声明类型和接口之间的关系,接口和类型之间是解耦的。

接口可以看做是一个规则,比如一个洗衣机的接口,我们规定有洗衣和甩干两个方法,只要任何类型实现了这两个方法,我们就叫它洗衣机。这个类型是什么完全不用管。

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 接口

这里举个例子:

  1. 在 fmt 包里有一个接口叫做 Stringer,里面定义了一个方法 String,这个方法用于把自己用字符串的形式打印出来。
  2. ↑ 这是什么意思呢?
  3. 比如说 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。

  1. 这里需要重新提一下接口值,之前提到过接口可以当做函数返回值来用。这里就经常用到。
  2. 一个 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 进行一些操作,然后返回。这样就可以实现修改数据流的效果了。
具体见这里

这种方式是通过接口嵌套,实现了一种类似继承的效果。(想想看为什么类似继承)

3.6.4 Image 接口

这一节主要是教你看文档