Go语言常用接口简介

Go 语言内置了不少接口定义,掌握这些接口用法可以写出更有 Go 范儿的程序!本节就带领大家,饱览最常用的几个:Stringererror 以及 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.Colorcolor.Model 这两个也是接口类型,可以使用预先实现的 color.RGBAcolor.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语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: