0%

go 库学习之 bytes

Go 标准库中的 bytes 包提供了一系列工具来实现对 字节切片([]byte)的操作。bytes 包和 string 包提供了类似的工具类 API,只不过 bytes 包操作的字节切片,而 strings 包操作的是字符串类型。

这篇文章将学习 bytes 包的基本使用方法。

实用函数

bytes 包的一大核心功能就是为字节切片类型 []byte 提供一系列实用函数,旨在简化应用程序对字节切片类型的操作。下面将逐一列举这些实用函数。如果某个函数涉及 Unicode 码点(rune)特性时,都认为该 Unicode 字符是通过 UTF-8 编码的。

比较

  • Equal 函数用于判断两个字节切片是否完全相同。在内部实现中,bytes 包是将其转换为字符串后进行比较
1
2
3
func Equal(a, b []byte) bool {
return string(a) == string(b)
}
  • EqualFold 将字节切片按照 UTF-8 编码格式解码为字符串,然后按照 Unicode case-folding 规则进行比较,这种比较方式是一种更通用的大小写不敏感比较
1
func EqualFold(s, t []byte) bool
  • Compare 函数用于比较两个字节切片。如果 a 等于 b,则返回 0;如果 a 比 b 大,则返回 +1;如果 a 比 b 小,则返回 -1
1
func Compare(a, b []byte) int
  • HasPrefix 用于判断字节切片 s 是否以 prefix 切片为前缀
1
func HasPrefix(s, prefix []byte) bool
  • HasSuffix 用于判断字节切片 s 是否以 suffix 切片为后缀
1
func HasSuffix(s, suffix []byte) bool

查找

  • Contains 函数用于判断字节切片 b 中是否包含子字节切片 subslice
1
func Contains(b, subslice []byte) bool
  • ContainsAny 函数用于判断字节切片 b 中是否包含字符串 chars 中的任意字符(即 Unicode 码点)
1
func ContainsAny(b []byte, chars string) bool
  • ContainsFunc 函数用于判断字节切片中是否存在满足使 f(r) 为 true 的 Unicode 码点
1
func ContainsFunc(b []byte, f func(rune) bool) bool

如下是一个示例:

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

import "bytes"

func main() {
hasUpper := func(r rune) bool {
return 'A' <= r && r <= 'Z'
}

// false
println(bytes.ContainsFunc([]byte("hello"), hasUpper))
// true
println(bytes.ContainsFunc([]byte("helLo"), hasUpper))
}
  • ContainsRune 函数用于判断字节切片 b 中是否包含 Unicode 码点 r
1
func ContainsRune(b []byte, r rune) bool
  • Count() 函数用于统计某个子字节切片 sep 在字节切片 s 中出现的次数
    • 注意这个统计是按照不重叠(non-overlapping)的方式进行统计的
    • 当 sep 传入 nil 或空切片时,该函数返回字节切片 s 中 Unicode 码点(即 Rune 类型)个数 + 1
1
func Count(s, sep []byte) int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "bytes"

func main() {
// 2
println(bytes.Count([]byte("abab"), []byte("ab")))
// 2, 非重叠统计
println(bytes.Count([]byte("aaaa"), []byte("aa")))
// 3
println(bytes.Count([]byte("中国"), nil))
// 4
println(bytes.Count([]byte("中国人"), []byte{}))
}
  • Index 函数在字节 s 中查找 sep 子切片第一次出现的位置,如果不存在,则返回 -1
1
func Index(s, sep []byte) int
  • IndexAny 函数用在字节切片 s 中查找字符串 chars 中的任意字符(即 Unicode 码点)第一次出现的位置
1
func IndexAny(s []byte, chars string) int
  • IndexByte 函数用于获取字节切片 b 中字节 c 第一次出现的位置
1
func IndexByte(b []byte, c byte) int
  • IndexFunc 函数从字节切片 s 中查找第一个使 f(r) 为 true 的 Unicode 码点的字节索引
1
func IndexFunc(s []byte, f func(r rune) bool) int
  • IndexRune 函数用于获取字节切片 b 中 Unicode 码点 r 第一次出现的位置
1
func IndexRune(s []byte, r rune) int
  • LastIndex 函数用于获取字节切片 b 中子切片 sep 最后一次出现的位置
1
func LastIndex(s, sep []byte) int
  • LastIndexAny 函数用从字节切片 s 中查找字符串 chars 中的任意字符(即 Unicode 码点)最后一次出现的位置
1
func LastIndexAny(s []byte, chars string) int
  • LastIndexByte 函数用于获取字节切片 b 中字节 c 最后一次出现的位置
1
func LastIndexByte(s []byte, c byte) int
  • LastIndexFunc 函数从字节切片 s 中查找最后一个使 f(r) 为 true 的 Unicode 码点的字节索引
1
func LastIndexFunc(s []byte, f func(r rune) bool)

分割

  • Cut 函数用于以 sep 作为分割串,将 s 分隔为两部分,其中,before 为 sep 之前的字节切片,after 为 sep 之后的字节切片,返回值 found 表示 sep 是否在 s 中出现。注意返回的切片是原始切片 s 的子切片,而不是副本
1
func Cut(s, sep []byte) (before, after []byte, found bool)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"bytes"
"fmt"
)

func main() {
s := []byte("abcdefg")
// []byte("ab"), []byte("efg"), true
fmt.Println(bytes.Cut(s, []byte("cd")))
// []byte("abcdefg"), nil, false
fmt.Println(bytes.Cut(s, []byte("xyz")))
}
  • CutPrefix 尝试从字节切片 s 中删除 prefix 前缀。如果 s 不是以 prefix 作为前缀,则返回 s, false,否则返回 删除前缀后的字节切片,true
1
func CutPrefix(s, prefix []byte) (after []byte, found bool) {
  • CutSuffix 尝试从字节切片 s 中删除 suffix 后缀。如果 s 不是以 suffix 作为后缀,则返回 s, false,否则返回 删除后缀后的字节切片,true
1
func CutPrefix(s, suffix []byte) (before []byte, found bool) {
  • Fields 将字节切片当成 UTF-8 编码的字符串,并以 Unicode 空白符作为分隔字符,返回分隔后的结果
1
func Fields(s []byte) [][]byte
1
2
3
4
5
6
7
8
9
10
11
package main

import (
"bytes"
"fmt"
)

func main() {
// fields: ["hello" "world"]
fmt.Printf("fields: %q\n", bytes.Fields([]byte(" hello world ")))
}
  • FieldsFunc 类似于 Field 函数,但是接收一个自定义函数用来判断当前 rune 是否为分割符
1
func FieldsFunc(s []byte, f func(rune) bool) [][]byte
  • Runes 将字节切片 s 按照 UTF-8 格式解码为 Unicode 码点,并返回 rune 的切片
1
func Runes(s []byte) []rune
  • SplitN 对字节切片 s 进行分割,分隔串为 sep,其中 N 参数指定了分隔后的总串数
    • 如果 N > 0,则代表需要返回最多 N 个子串,最后一个字符串可能是未分割状态
    • 如果 N 是 0,返回 nil
    • 如果 N 小于 0,返回所有子串
    • 如果 sep 为空,SplitN 将其分割为每个 UTF-8 字节序列
1
func SplitN(s, sep []byte, n int) [][]byte
  • Split 函数类似于 SpintN 用于对 s 进行分割,但总是返回所有子串
1
func Split(s, sep []byte) [][]byte
  • SplitAfterN 函数类似于 SplitN,但分隔后的子串会包括 sep
1
func SplitAfterN(s, sep []byte, n int) [][]byte
  • SplitAfter 函数类似于 SplitAferN,但总是返回所有子串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"bytes"
"fmt"
)

func main() {
s := []byte("a,b,c")
// ["a" "b" "c"]
fmt.Printf("%q\n", bytes.Split(s, []byte(",")))
// ["a" "b" "c"]
fmt.Printf("%q\n", bytes.SplitN(s, []byte(","), -1))
// ["a," "b," "c"]
fmt.Printf("%q\n", bytes.SplitAfter(s, []byte(",")))
// ["a," "b," "c"]
fmt.Printf("%q\n", bytes.SplitAfterN(s, []byte(","), -1))
}

修改/转换

  • Clone 函数用于返回一个字节切片的副本,新返回的切片和原切片只是数据内容相同,但指向两块不同的底层存储区域
1
func Clone(b []byte) []byte)

Clone 函数的实现非常简单,它其实就是通过 return append([]byte(nil), b...) 来返回 b 的副本。

  • Map 对将字节切片 s 中的每个 Unicode 码点应用转换函数 mapping,并将转换后的 Unicode 码点填入结果字节切片(以 UTF-8 进行编码)。注意该转换并不会修改原始字节切片 s,也就是说结果字节切片是一个新创建的切片
1
func Map(mapping func(r rune) rune, s []byte) []byte
  • Repeat 函数返回一个新的 byte 字节序列,它包含 b 字节切片的 n 个副本
1
func Repeat(b []byte, count int) []byte
  • Replace 函数可以将字节切片 s 中的 old 子串替换为 new,替换操作最多执行 n 次。该函数返回一个全新的字节切片
    • 如果 n 小于 0,不限制替换次数
    • 如果 old 为空,则在 rune 之间(包括起始和结尾),添加 replacement
1
func Replace(s, old, new []byte, n int) []byte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"bytes"
"fmt"
)

func main() {
// "He is Her friend"
fmt.Printf("%q\n", bytes.Replace([]byte("he is her friend"), []byte("he"), []byte("He"), -1))
// ",中,国,人,"
fmt.Printf("%q\n", bytes.Replace([]byte("中国人"), nil, []byte(","), -1))
// ",中,国人"
fmt.Printf("%q\n", bytes.Replace([]byte("中国人"), nil, []byte(","), 2))
}
  • ReplaceAll(s, old, new) 函数等价于 Replace(s, old, new, -1),即执行所有替换
1
func ReplaceAll(s, old, new []byte) []byte {
  • 以下函数将 s 字节切片中的所有 Unicode 字符转换为对应的形式,并返回一个新的字节切片副本
1
2
3
4
5
6
func ToLower(s []byte) []byte
func ToLowerSpecial(c unicode.SpecialCase, s []byte) []byte
func ToTitle(s []byte) []byte
func ToTitleSpecial(c unicode.SpecialCase, s []byte) []byte
func ToUpper(s []byte) []byte
func ToUpperSpecial(c unicode.SpecialCase, s []byte) []byte
  • ToValidUTF8 将 s 字节切片中的所有非法 UTF-8 编码序列进行替换,并返回一个新的字节切片副本
1
func ToValidUTF8(s, replacement []byte) []byte
  • 以下函数对按照不同的方式删除字节切片 s 的相应内容,并返回删除操作完成后的字节切片。返回的结果切片是 s 的子切片,而不是副本
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
// 该函数从字节切片 s 左边开始,删除 cutset 字符串所包含的所有 Unicode 码点
func TrimLeft(s []byte, cutset string) []byte

// 该函数从字节切片 s 右边开始,删除 cutset 字符串所包含的所有 Unicode 码点
func TrimRight(s []byte, cutset string) []byte

// 该函数从字节切片 s 两边,删除 cutset 中所包含的所有 Unicode 码点
func Trim(s []byte, cutset string) []byte

// 该函数从字节切片 s 左边开始,删除所有满足 f(r) 为 true 的 Unicode 字符
func TrimLeftFunc(s []byte, f func(r rune) bool)

// 该函数从字节切片 s 右边开始,删除所有满足 f(r) 为 true 的 Unicode 字符
func TrimRightFunc(s []byte, f func(r rune) bool) []byte

// 该函数从字节切片 s 的两边,删除所有满足 f(r) 为 true 的 Unicode 字符
func TrimFunc(s []byte, f func(r rune) bool) []byte

// /该函数从字节切片 s 的左边删除 prefix 前缀字节切片
func TrimPrefix(s, prefix []byte) []byte

// 该函数从字节切片 s 的右边删除 suffix 后缀字节切片
func TrimSuffix(s, suffix []byte) []byte

// 该函数从字节切片 s 的两边,删除所有空白符
func TrimSpace(s []byte) []byte

虽然字节切片使用简单,借助上述实用函数应用程序可以轻松实现很多功能。但是由于切片的动态扩容特性,使得某些切片操作后会重新分配内存空间,这样新切片与原切片底层存储就会 分家。如果 Gopher 没有特别注意这一点,容易写出隐藏较深的代码 bug。因此 bytes 包还提供了一些封装层级更高的类型,简化对 字节切片 的使用。

Buffer 类型

Buffer 类型用于表示一个长度可变的字节缓冲区,它实现了 io.Readerio.Writer 等接口,因此可以从 Buffer 中读取数据、向 Buffer 中写入数据。Buffer 的定义如下:

1
2
3
4
5
6
7
8
type Buffer struct {
// 表示底层所使用的字节切片
buf []byte // contents are the bytes buf[off : len(buf)]
// 当前待读取数据在 buf 中的偏移位置
off int // read at &buf[off], write at &buf[len(buf)]
// 记录上次读取操作
lastRead readOp // last read operation, so that Unread* can work correctly.
}

可以看到,Buffer 使用了字节切片作为底层存储区域。其 off 字段记录了当前读取位置的偏移,每次从 Buffer 中读取数据时,都会从该偏移开始读取,而 buf[off:len(buf)] 之间的数据就是所有待读取的数据。每次写入数据时则会从 len(buf) 处写入。由于 Buffer 支持 unread 操作,即回退到上一次读取时的位置,因此其需要通过 lastRead 来记录上次读取的字节数。

Buffer 在对底层的字节切片进行数据写入时,会处理字节切片的动态扩容场景,将 buf 指向新的内存区域,同时将原有字节切片中尚未读取的数据也拷贝新的存储区域。这些操作都封装在 Buffer 方法的内部,不需要程序员关心。这样就简化了字节切片的使用,也降低了犯错的概率。

下面简单列举 Buffer 类型支持的函数/方法:

  • NewBuffer(buf []byte) 函数用于创建一个 Buffer创建,其使用的底层字节切片就是参数 buf(不是 buf 的副本),因此调用该函数后,调用者不应该再操作原始的字节切片 buf 了
1
func NewBuffer(buf []byte) *Buffer
  • NewBufferString 创建一个 Buffer,其初始内容为字符串 s 的内容。
1
func NewBufferString(s string) *Buffer
  • Availabel 返回当前 buffer 的剩余可使用空间
1
func (b *Buffer) Available() int { return cap(b.buf) - len(b.buf) }
  • Availabel 则以字节切片的形式返回 buffer 中当前可使用的空间
1
func (b *Buffer) AvailableBuffer() []byte { return b.buf[len(b.buf):] }
  • Bytes 方法以字节切片的形式返回该 buffer 中还未被读取的数据
1
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }
  • Cap 方法该 buffer 底层字节切片的总容量
1
func (b *Buffer) Cap() int { return cap(b.buf) }
  • Grow 增长 Buffer 的容量,以确保该 Buffer 至少可以写入 n 字节数据。调用该函数后,可以确保 n 字节的写入操作不会触发内存分配
1
func (b *Buffer) Grow(n int)
  • Len 方法返回 buffer 中尚未读取数据的长度
1
func (b *Buffer) Len() int { return len(b.buf) - b.off }
  • 以下方法尝试从 Buffer 中读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从 Buffer 中读取尝试读取 `len(p)` 长度的数据到字节切片 p 中。它返回真实读取的字节数(因为 Buffer 中可能没有这么多的数据)
// 如果当前 Buffer 已经没有数据可读,则返回 0, io.EOF
func (b *Buffer) Read(p []byte) (n int, err error)

// 从 Buffer 中读取一个字节
func (b *Buffer) ReadByte() (byte, error)

// 从 Buffer 中读取数据,直到遇到指定的 delim 字节
// 以字节切片的形式返回所有读到的内容(包括 delim),返回的字节切片是 Buffer 底层字节切片的副本
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error)

// 从 Buffer 中读取一个 rune
func (b *Buffer) ReadRune() (r rune, size int, err error)

// 从 Buffer 中读取数据,直到遇到指定的 delim 字节
// 此时以字符串的形式返回所有读到的内容(包括 delim)
func (b *Buffer) ReadString(delim byte) (line string, err error)
  • ReadFrom 从一个 io.Reader 中读入数据并写入 Buffer
1
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error)
  • Next 方法以切片的形式返回该 Buffer 中未读取数据的前 n 个字节,同时调整 Buffer 的偏移,如同这些数据被 Read 一样。需要注意该函数返回的切片和 Buffer 底层的切片指向的是同一块内存
1
func (b *Buffer) Next(n int) []byte
  • Reset 方法将 Buffer 重新清空,但是底层的存储空间还保留,不会释放
1
func (b *Buffer) Reset()
  • String 方法以字符串形式返回该 buffer 中未读取的数据
1
func (b *Buffer) String() string
  • Truncate 只保留 buffer 未读取数据中的前 n 个字节,剩余的数据则全部丢弃。如果 n 为 0,则整个 buffer 都被清空,等同于 b.Reset()
1
func (b *Buffer) Truncate(n int)
  • UnreadByte 将最近一次读取操作的最后一个字节回退到 Buffer 中。如果从上次读取操作后调用了 Write 相关操作,则该函数会调用失败
1
func (b *Buffer) UnreadByte() error
  • UnreadRune 函数将最近一次 ReadRune 读取的 rune 回退到 Buffer 中。如果从上次读取操作后调用了 Write 相关操作,则该函数会调用失败
1
func (b *Buffer) UnreadRune() error
  • 以下方法尝试向 Buffer 中写入数据
1
2
3
4
5
6
7
8
9
10
11
// 往 Buffer 中写入字节切片 p 中的数据
func (b *Buffer) Write(p []byte) (n int, err error)

// 往 Buffer 中写入一个 byte
func (b *Buffer) WriteByte(c byte) error

// 往 Buffer 中写入一个 Rune
func (b *Buffer) WriteRune(r rune) (n int, err error)

// 往 Buffer 中写入一个字符串
func (b *Buffer) WriteString(s string) (n int, err error)
  • WriteTo 函数将 Buffer 中的数据写入 w 中,直到 Buffer 中的数据全部消耗完,或者出现错误
1
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error)

Reader 类型

相比于 Buffer 类型,Reader 类型则更简单了,因为它只实现了从字节切片中读取数据,由于不涉及写入操作,因此也不需要考虑动态扩容问题。Reader 类型的定义如下:

1
2
3
4
5
6
7
8
9
10
11
type Reader struct {
// Reader 内部保存的数据,当调用读取操作时就是从该字节切片中返回数据
s []byte
// 当前读取的索引
i int64 // current reading index
// 上一个 rune 所在的偏移
// 如果是按照非 rune 读取,需要将该字段重置为 -1
// 该字段主要用于 UnreadRune,即是 ReadRune 的回退操作
// 注意,不能连续回退
prevRune int // index of previous rune; or < 0
}

Reader 类型实现了 io.Readerio.ReaderAtio.Seeker 等接口,当需要一个从字节切片中读取数据的 io.Reader 实现时,就可以使用 bytes.Reader 类型。

下面列举 bytes.Reader 支持的函数/方法:

  • NewReader 创建一个 Reader,通过该 Reader 可以从字节序列 b 中读取数据
1
func NewReader(b []byte) *Reader
  • Len 返回剩余的、未读取的数据长度
1
2
3
4
5
6
7
8
func (r *Reader) Len() int
{
// 读取完成
if r.i >= int64(len(r.s)) {
return 0
}
return int(int64(len(r.s)) - r.i)
}
  • 以下方法从 Reader 中读取数据
1
2
3
4
5
6
7
8
9
10
11
// 从 Reader 中读取数据到字节切片 b 中,实现了 io.Reader 接口
func (r *Reader) Read(b []byte) (n int, err error)

// 从 Reader 的指定位置读取数据,实现了 io.ReaderAt 接口
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)

// 从 Reader 中读取一个字节,实现 io.ByteReader 接口
func (r *Reader) ReadByte() (byte, error)

// 从 Reader 中读取一个 Unicode 码点
func (r *Reader) ReadRune() (ch rune, size int, err error)
  • Reset 方法重置 Reader 对象
1
func (r *Reader) Reset(b []byte) { *r = Reader{b, 0, -1} }
  • Seek 方法实现了io.Seeker 接口,用于重置偏移量,whence 表示偏移量 offset 以哪个位置作为相对位置
1
func (r *Reader) Seek(offset int64, whence int) (int64, error)
  • Size 方法返回 Reader 中数据的总长度
1
func (r *Reader) Size() int64 { return int64(len(r.s)) }
  • UnreadByte 从 Reader 中 unread 一个字节。在加上 ReadByte 方法,Reader 类型就实现了 io.ByteScanner 接口
1
func (r *Reader) UnreadByte() error
  • UnreadRune 从 Reader 中 unread 一个 rune。在加上 ReadRune 方法,Reader 类型就实现了 io.RuneScanner 接口
1
func (r *Reader) UnreadRune() error
  • WriteTo 方法将 Reader 中的数据写入 w 中,直到 Reader 中的数据全部消耗完,或者出现错误。实现了 io.WriterTo 接口
1
func (r *Reader) WriteTo(w io.Writer) (n int64, err error)

小结

标准库的 bytes 功能并不算复杂,但是其实现却非常精巧,尤其是其工具函数的实现。它通过编写一系列简短的工具函数,并通过组合这些工具函数来提供相对复杂的 API。bytes 包为如何编写这种工具类包代码提供了很好的范例。