Golang-4-方法和接口

Parts of A Tour Of Go

方法

Go没有类。
Go可以为结构体类型定义方法。
方法就是一类带特殊的接收者参数的函数。
方法接收者在它自己的参数列表内,位于func关键字和方法名之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

// Abs 方法拥有一个名为 v 类型为 Vertex 的接收者
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // 5
}

方法即函数

方法只是个带接收者参数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

// 这个 Abs 就是个正常的函数
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v)) // 5
}

为非结构体类型声明方法

接收者的类型定义和方法声明必须在同一个包内。
不能为内建类型声明方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"math"
)

type MyFloat float64

// Abs 方法的接收者类型为 MyFloat
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}

为指针接收者声明方法

对于某类型T,接收者的类型可以使用*T文法。
*T不能是像*int这样的指针(不能为内建类型声明方法)。
指针接收者的方法可以修改接收者指向的值。
由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
若使用值接收者,那么方法会对原始值的副本进行操作。
修改原始值的方法要使用指针接收者。

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

// Abs 方法 接收者类型为 Vertex
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 为 *Vertex 定义了 Scale 方法
// 以指针为接收者的方法
// Scale 方法 接收者为 指针 *Vertex
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
// 初始化 X, Y = 3, 4
v := Vertex{3, 4}
// 通过方法直接修改了 v.X, v.Y = 30, 40
v.Scale(10)
// 输出 math.Sqrt(30 * 30 + 40 * 40)
fmt.Println(v.Abs())
}

指针与函数

以指针作为接收者的方法 可以修改原值;
以非指针为接收者的方法 修改的是原值的副本。

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
// 初始化 x, y = 3, 4
v := Vertex{3, 4}
// 这里只是操作了 v.X 和 v.Y 的副本,实际上 v.X v.Y 并没有被改变
Scale(v, 10)
// 输出 math.Sqrt(3 * 3 + 4 * 4)
fmt.Println(Abs(v))
}

方法与指针重定向

含有指针参数的函数必须接受一个指针。
而以指针为接收者的方法被调用时,接收者既能是值又能是指针。

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"

type Vertex struct {
X, Y float64
}

// Scale 方法 接收者为指针类型 *Vertex
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

// 含有指针参数的 ScaleFunc 函数
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
// v 为值类型
v := Vertex{3, 4}
// 值调用方法 Go会将 v.Scale(2) 解释成 (&v).Scale(2)
v.Scale(2)
// 必须传递指针参数
ScaleFunc(&v, 10)

// p 为指针类型
p := &Vertex{4, 3}
// 指针调用方法
p.Scale(3)
// 以指针为参数调用函数
ScaleFunc(p, 8)

// {60 80} &{96 72}
fmt.Println(v, p)
}

接受一个值作为参数的函数必须接受一个指定类型的值。
而以值为接收者的方法被调用时,接收者既能为值又能为指针。

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 以值为参数的函数
func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
// 值类型
v := Vertex{3, 4}
// 直接调用方法
fmt.Println(v.Abs())
// 直接调用以值类型为参数的函数
fmt.Println(AbsFunc(v))

// 指针类型
p := &Vertex{4, 3}
// 直接调用方法
fmt.Println(p.Abs())
// 用值才可以调用以值类型为参数的函数
fmt.Println(AbsFunc(*p))
}

选择值或指针作为方法的接收者

使用指针接收者的原因:

  • 方法能够修改其接收者指向的值(原值)。
  • 可以避免每次调用方法时复制该值(若值较大时,可避免额外的内存开销)。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

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
package main

import (
"fmt"
"math"
)

// 结构体声明 Vertex
type Vertex struct {
X, Y float64
}

// Scale 方法 接收者为指针类型 *Vertex
// 含有一个 float64 参数
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

// Abs 方法 接收者为指针类型 *Vertex
// 返回值为一个 float64
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
// 声明一个指针 v 指向一个 Vertex 结构体
v := &Vertex{3, 4}
// Before scaling: &{X:3 Y:4}, Abs: 5
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
// 指针类型 v 调用方法,直接修改原值
v.Scale(5)
// After scaling: &{X:15 Y:20}, Abs: 25
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

接口

接口类型是由一组方法签名定义的集合。
接口类型的值可以是任何实现了这些方法的值。

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
package main

import (
"fmt"
"math"
)

// 接口声明
// 无参 Abs 方法 返回一个 float64
type Abser interface {
Abs() float64
}

type MyFloat float64

// Abs 方法 接收者为 MyFloat 类型
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

type Vertex struct {
X, Y float64
}

// Abs 方法 接收者为 *Vertex 指针类型
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}

// a MyFloat 实现了 Abser
a = f
// a *MyFloat 实现了 Abser
// *MyFloat 可隐式转换为 MyFloat
a = &f
// a *Vertex 实现了 Abser
a = &v
// a Vertex 未实现 Abser (报错)
// Vertex 不可隐式转换为 *Vertex (不是所有的值都可以取址)
a = v

fmt.Println(a.Abs())
}

接口与隐式实现

类型通过实现一个接口的所有方法来实现该接口。
隐式接口从接口的实现中解耦了定义,这样的接口可以出现在任何包中,无需提前准备。
也就无需在每个实现上增加新的接口名称,同时也鼓励粒度明确的接口定义。

接口值

接口值可以看做包含值和具体类型的元组。
接口值保存了一个具体底层类型的具体值。
接口值调用方法时会执行其底层类型的同名方法。

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
package main

import (
"fmt"
"math"
)

// 接口声明
type I interface {
// 无参 M 方法
M()
}

type T struct {
S string
}

// *T 类型的 M 方法
func (t *T) M() {
fmt.Println(t.S)
}

type F float64

// F 类型的 M 方法
func (f F) M() {
fmt.Println(f)
}

func main() {
// 接口实现
var i I

// *T 类型实现了 M
i = &T{"Hello"}
// 指针类型 => main包T类型
// (&{Hello}, *main.T)
describe(i)
// 调用 *T类型的M方法
// Hello
i.M()

// F 类型实现了 M
i = F(math.Pi)
// main包F类型
// (3.141592653589793, main.F)
describe(i)
// 调用 F类型的M方法
// 3.141592653589793
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

底层值为nil的接口值

即便接口内的具体值为nil,方法仍然会被nil接收者调用。
在一些语言中,这会触发一个空指针异常,Go中通常会写一些方法来优雅的处理它。
注意:保存了nil具体值的接口其自身并不为nil

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
package main

import (
"fmt"
)

type I interface {
M()
}

type T struct {
S string
}

func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}

func main() {
var i I

var t *T
// 保存了nil具体值的接口
i = t
// (<nil>, *main.T)
describe(i)
// <nil>
i.M()

i = &T{"Hello"}
// (&{Hello}, *main.T)
describe(i)
// Hello
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

nil接口值

nil接口值既不保存值也不保存具体的类型。
nil接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个具体方法的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// build通过,run报错
package main

import (
"fmt"
)

type I interface {
M()
}

func main() {
var i I
// (<nil>, <nil>)
describe(i)
// panic: runtime error: invalid memory address or nil pointer dereference
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}

空接口

指定了零个方法的接口值被称为空接口:interface{}
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法)
空接口被用来处理未知类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func main() {
var i interface{}
// (<nil>, <nil>)
describe(i)

i = 18
// (18, int)
describe(i)

i = "Hello"
// (Hell0, string)
describe(i)
}

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

类型断言

类型断言提供了访问接口值底层具体值的方式。

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"

func main() {
var i interface{} = "Hello"

// 该语句断言接口值 i 保存了具体类型 string
// 并将其底层类型为 string 的值赋予变量 s
s := i.(string)
// Hello
fmt.Println(s)

// 为了判断一个接口值是否保存了一个特定的类型
// 类型断言可以返回两个值
// 其底层值以及一个报告断言是否成功的布尔值
// 这种语法与读取一个映射时有相同之处
s, ok := i.(string)
// Hello true
fmt.Println(s, ok)

// i 未保存 float64 类型的值
// f 是 float64 类型的零值
// ok 是 false
f, ok := i.(float64)
// 0 false
fmt.Println(f, ok)

// i 未保存 float64 类型的值 报错
// panic: interface conversion: interface {} is string, not float64
f = i.(float64)
fmt.Println(f)
}

类型选择

类型选择是一种顺序从几个类型断言中选择分支的结构。
类型选择与一般的switch语句相似,不过类型选择中的case为为类型(而非值),它们针对给定接口值所存储的值的类型进行比较。

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
package main

import "fmt"

func do(i interface{}) {
// 类型选择中的声明与类型断言 i.(T) 的语法相同
// 只是具体类型 T 被替换成了关键字 type
switch v := i.(type) {
// v 的类型为 int
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
// v 的类型为 string
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
// 没有匹配!!!
// v 的类型与 i 的类型相同
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

func main() {
// Twice 19 is 38
do(19)
// "Hello" is 5 bytes long
do("Hello")
// I don't know about type bool!
do(true)
}

Stringer

fmt包中定义的Stringer是最普遍的接口之一。

1
2
3
type Stringer interface {
String() string
}

Stringer是一个可以用字符串描述自己的类型。
fmt包(还有很多包)都通过此接口来打印值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"SB_NO1", 18}
b := Person{"SB_NO2", 17}
// SB_NO1 (18 years) SB_NO2 (17 years)
fmt.Println(a, b)
}

练习: Stringer

通过让IPAddr类型实现fmt.Stringer来打印点号分隔地址。
例如,IPAddr{127, 0, 0, 1}应当打印为127.0.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (a IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", a[0], a[1], a[2], a[3])
}

func main() {
hosts := map[string]IPAddr{
"localhost": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}

错误

Go使用error值来表示错误状态。
fmt.Stringer类似,error类型是一个内建接口。

1
2
3
type error interface {
Error() string
}

fmt.Stringer类型,fmt包在打印值时也会满足error
通常函数会返回一个error值,调用该函数的代码应当判断这个错误是否等于nil来进行错误处理。
error等于nil时表示成功,非nilerror表示失败。

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
package main

import (
"fmt"
"time"
)

type MyError struct {
When time.Time
What string
}

func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}

func main() {
if err := run(); err != nil {
// at 2016-10-18 11:30:14.002074 +0800 DST, it didn't work
fmt.Println(err)
}
}

练习: 错误

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
package main

import (
"fmt"
)

type ErrNegativeSqrt struct {
Num float64
}

func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v\n", e.Num)
}

func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, &ErrNegativeSqrt{x}
}
var temp float64 = 10
for i := 0; i < 10; i++ {
temp = (temp + x/temp) / 2
}
return temp, nil
}

func main() {
// 1.414213562373095 <nil>
fmt.Println(Sqrt(2))
// 0 cannot Sqrt negative number: -2
fmt.Println(Sqrt(-2))
}

Reader

io包指定了io.Reader接口,它表示从数据流的末尾进行读取。
Go标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader接口有一个Read方法:

1
func (T) Read(b []byte) (n int, err error)

Read用数据填充给定的字节切片并返回填充的字节数和错误值。
在遇到数据流的结尾时,它会返回一个io.EOF错误。

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
// 示例代码创建了一个 strings.Reader
// 并以每次 8 字节的速度读取它的输出
package main

import (
"fmt"
"io"
"strings"
)

// n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
// b[:n] = "Hello, R"
// n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
// b[:n] = "eader!"
// n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
// b[:n] = ""
func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

练习: Reader

实现一个Reader类型,它产生一个ASCII字符A的无限流。

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
package main

import "github.com/Go-zh/tour/reader"

type MyReader struct {}

// Add a Read([]byte) (int, error) method to MyReader.
func (r MyReader) Read(b []byte) (n int, err error) {
b[0] = 'A'
return 1, nil
}

func main() {
reader.Validate(MyReader{})
}

// Source
// func Validate(r io.Reader) {
// b := make([]byte, 1024, 2048)
// i, o := 0, 0
// for ; i < 1<<20 && o < 1<<20; i++ { // test 1mb
// n, err := r.Read(b)
// for i, v := range b[:n] {
// if v != 'A' {
// fmt.Fprintf(os.Stderr, "got byte %x at offset %v, want 'A'\n", v, o+i)
// return
// }
// }
// o += n
// if err != nil {
// fmt.Fprintf(os.Stderr, "read error: %v\n", err)
// return
// }
// }
// if o == 0 {
// fmt.Fprintf(os.Stderr, "read zero bytes after %d Read calls\n", i)
// return
// }
// fmt.Println("OK!")
// }

练习: rot13Reader

有种常见的模式是一个io.Reader包装另一个io.Reader,然后通过某种方式修改其数据流。
例如,gzip.NewReader函数接受一个io.Reader(已压缩的数据流)并返回一个同样实现了io.Reader*gzip.Reader(解压后的数据流)。
编写一个实现了io.Reader并从另一个io.Reader中读取数据的rot13Reader,通过应用rot13代换密码对数据流进行修改。
rot13Reader类型已经提供。
实现Read方法以满足io.Reader

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
package main

import (
"io"
"os"
"strings"
)

type rot13Reader struct {
r io.Reader
}

func (rot *rot13Reader) Read(b []byte) (n int, err error) {
n, err = rot.r.Read(b)
// ROT-13 回转13位
// 一种简单的加密解密方式
// a-m 一一对应 n-z
// n-z 一一对应 a-m
// 加密和解密使用同一种方式
for i := 0; i < len(b); i++ {
if (b[i] >= 'A' && b[i] <= 'M') || (b[i] >= 'a' && b[i] <= 'm') {
b[i] += 13
} else if (b[i] >= 'N' && b[i] <= 'Z') || (b[i] >= 'n' && b[i] <= 'z') {
b[i] -= 13
}
}
return
}

func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
// You cracked the code!
io.Copy(os.Stdout, &r)
}

图像

image包定义了Image接口:

1
2
3
4
5
6
7
package image

type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}

注意: Bounds方法的返回值Rectangle实际上是一个image.Rectangle,它在image包中声明。

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

import (
"fmt"
"image"
)

func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
// (0,0)-(100,100)
fmt.Println(m.Bounds())
// 0 0 0 0
fmt.Println(m.At(0, 0).RGBA())
}

练习: 图像

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
package main

import (
"golang.org/x/tour/pic"
"image"
"image/color"
)

type Image struct {
Width, Height int
color uint8
}

func (i *Image) Bounds() image.Rectangle {
return image.Rect(0, 0, i.Width, i.Height)
}

func (i *Image) ColorModel() color.Model {
return color.RGBAModel
}

func (i *Image) At(x, y int) color.Color {
return color.RGBA{i.color + uint8(x), i.color + uint8(y), 255, 255}
}

func main() {
m := Image{100, 100, 128}
pic.ShowImage(&m)
}