Go 语言内置了不少接口定义,掌握这些接口用法可以写出更有 Go 范儿的程序!本节就带领大家,饱览最常用的几个:Stringer 、 error 以及 Reader 等等。
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
|
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{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
|
练习
为 IP 地址类型 IPAddr 实现 fmt.Stringer 接口,将地址以点分十进制表示法打印出来。举个例子,IPAddr{1, 2, 3, 4}
打印出来应该是 1.2.3.4
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
type IPAddr [4]byte
// TODO: Add a "String() string" method to IPAddr.
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
|
答案
1
2
3
|
func (ip IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}
|
error
Go 程序通过 error 值返回错误状态,error 也是一个内置的接口类型,跟 fmt.Stringer 类似:
1
2
3
|
type error interface {
Error() string
}
|
跟 fmt.Stringer 一样,fmt 包在打印数据时,也会检查 error 接口看是否为错误信息。
函数通常在出错时返回 error 值,调用者需要检查该值是否为 nil
,以此判断是否需要处理错误:
1
2
3
4
5
6
|
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
|
- error 值为
nil
代表成功;
- error 值不为
nil
代表出错;
我们可以设计自己的错误类型,只要满足 error 接口契约,实现 Error 方法即可:
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 {
fmt.Println(err)
}
}
|
这个例子自定义了一个错误类型 MyError ,它实现了 Error 方法。注意到,当 fmt 打印 error 值时,它根据 error 接口找到 Error 方法。
练习
我们将 if判断语句 中的开方函数改造一下,让它可以通过 error 值返回错误。
- 当指定参数为负数时,返回非空 error 值(假设它不支持复数);
- 当指定参数为非负数时,返回空的 error 值;
定义一个新的错误类型:
1
|
type ErrNegativeSqrt float64
|
实现 Error 方法使它满足 error 接口:
1
|
func (e ErrNegativeSqrt) Error() string
|
举个例子,ErrNegativeSqrt(-2).Error()
返回 cannot Sqrt negative number: -2
。
开始动手改造 Sqrt 函数吧,当指定参数为负数时返回 ErrNegativeSqrt 错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
)
func Sqrt(x float64) (float64, error) {
return 0, nil
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
|
答案
1
2
3
4
5
6
7
8
9
10
11
12
|
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
return math.Sqrt(x), nil
}
|
特别注意,在 Error 函数内调用 fmt.Sprintf 函数,必须先将错误值转换成 float64 。否则 fmt 包会根据 error 接口继续调用 Error 方法,陷入死循环。
Reader
io 包提供的 io.Reader 接口,代表一个数据流的读端。Go 标准库提供的很多功能,都是按这个接口实现的,包括文件、网络连接、数据压缩、数据加解密等。
io.Reader 接口只声明了一个 Read 方法:
1
|
func (T) Read(b []byte) (n int, err error)
|
Read 方法负责将数据读到指定的字节切片 []byte ,返回成功读取的字节数以及一个 error 值。当数据流读完时,它将返回一个 io.EOF 错误。
这个例子创建了一个 strings.Reader ,然后每次读 8 字节,直到将它读完:
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"
"io"
"strings"
)
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 接口实现无穷数据流,不断返回 ASCII 字符 A
。
答案
1
2
3
4
5
6
7
8
9
|
type MyReader struct{}
func (r MyReader) Read(b []byte) (int, error) {
n := len(b)
for i := 0; i < n; i++ {
b[i] = 'A'
}
return n, nil
}
|
练习二:二次包装
有一种设计模式很常见:对另一个 io.Reader 类型进行包装,形成新的 io.Reader,以便修改数据流行为。
举个例子,gzip.NewReader 接受一个实现 io.Reader 接口的压缩数据流,返回的解压数据流 gzip.Reader ,它同样实现 了 io.Reader 接口。
现在,我们以相同的方式设计 rot13Reader :它同样实现了 io.Reader 接口,从 io.Reader 类型读取数据,然后对所有字母字符应用 ROT13 替换加密算法,返回加密后的数据。
ROT 算法将字母字符按固定的映射关系进行替换,映射表如下:
1
2
|
输入:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
输出:NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
|
答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func rot13(b byte) byte {
if b >= 'a' && b <= 'z' {
return 'a' + (b - 'a' + 13) % 26
} else if b >= 'A' && b <= 'Z' {
return 'A' + (b - 'A' + 13) % 26
} else {
return b
}
}
func (r rot13Reader) Read(b []byte) (int, error) {
n, err := r.r.Read(b)
if err != nil {
return n, err
}
for i := 0; i < n; i++ {
b[i] = rot13(b[i])
}
return n, nil
}
|
为了 rot13Reader 实现了 Read 方法,通过内部的 io.Reader 读取数据,然后调用 rot13 函数对字符进行替换。rot13 判断给定字节是否为小写字母或大写字母,若是将其替换成其后第 13 个。
Image
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 包里面。它代表一个矩形,由坐标最小的顶点 Min 和坐标最大的顶点 Max 界定。
color.Color 和 color.Model 这两个也是接口类型,可以使用预先实现的 color.RGBA 和 color.RGBAModel 类型,而不用理会接口的存在。想了解更多细节,可以围观 image/color 这个包。
1
2
3
4
5
6
7
8
9
10
11
12
|
package main
import (
"fmt"
"image"
)
func main() {
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
}
|
练习
还记得我们在 切片练习 中实现的图片生成器吗?学过接口后,我们尝试写一个新的版本,返回实现 image.Image 接口的数据类型,以替代原先的切片。
定义自己的 Image 类型,然后实现必要的方法,最后调用 pic.ShowImage
:
1
2
3
4
5
6
7
8
9
10
|
package main
import "golang.org/x/tour/pic"
type Image struct{}
func main() {
m := Image{}
pic.ShowImage(m)
}
|
答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func (i Image) ColorModel() color.Model {
return color.RGBAModel
}
func (i Image) Bounds() image.Rectangle {
return image.Rectangle{
Min: image.Point{
X: -100,
Y: -100,
},
Max: image.Point{
X: 100,
Y: 100,
},
}
}
func (i Image) At(x, y int) color.Color {
v := uint8(x*y)
return color.RGBA{v, v, 255, 255}
}
|
- ColorModel 直接返回 color.RGBAModel ;
- Bounds 返回一个矩形描述图片的范围,矩形由
(-100, -100)
和 (100, 100)
两点确定;
- At 根据坐标返回对应的位置的 RGBA 值,数值由一个数学公式 $x \times y$ 计算而来;
【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: