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 mainimport "bytes" func main () { hasUpper := func (r rune ) bool { return 'A' <= r && r <= 'Z' } println (bytes.ContainsFunc([]byte ("hello" ), hasUpper)) 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 mainimport "bytes" func main () { println (bytes.Count([]byte ("abab" ), []byte ("ab" ))) println (bytes.Count([]byte ("aaaa" ), []byte ("aa" ))) println (bytes.Count([]byte ("中国" ), nil )) 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 mainimport ( "bytes" "fmt" ) func main () { s := []byte ("abcdefg" ) fmt.Println(bytes.Cut(s, []byte ("cd" ))) 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 mainimport ( "bytes" "fmt" ) func main () { 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 mainimport ( "bytes" "fmt" ) func main () { s := []byte ("a,b,c" ) fmt.Printf("%q\n" , bytes.Split(s, []byte ("," ))) fmt.Printf("%q\n" , bytes.SplitN(s, []byte ("," ), -1 )) fmt.Printf("%q\n" , bytes.SplitAfter(s, []byte ("," ))) 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 mainimport ( "bytes" "fmt" ) func main () { 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 func TrimLeft (s []byte , cutset string ) []byte func TrimRight (s []byte , cutset string ) []byte func Trim (s []byte , cutset string ) []byte func TrimLeftFunc (s []byte , f func (r rune ) bool )func TrimRightFunc (s []byte , f func (r rune ) bool ) []byte func TrimFunc (s []byte , f func (r rune ) bool ) []byte func TrimPrefix (s, prefix []byte ) []byte func TrimSuffix (s, suffix []byte ) []byte func TrimSpace (s []byte ) []byte
虽然字节切片使用简单,借助上述实用函数应用程序可以轻松实现很多功能。但是由于切片的动态扩容特性,使得某些切片操作后会重新分配内存空间,这样新切片与原切片底层存储就会 分家
。如果 Gopher 没有特别注意这一点,容易写出隐藏较深的代码 bug。因此 bytes
包还提供了一些封装层级更高的类型,简化对 字节切片
的使用。
Buffer 类型 Buffer
类型用于表示一个长度可变的字节缓冲区,它实现了 io.Reader
、io.Writer
等接口,因此可以从 Buffer
中读取数据、向 Buffer
中写入数据。Buffer
的定义如下:
1 2 3 4 5 6 7 8 type Buffer struct { buf []byte off int lastRead readOp }
可以看到,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 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (b *Buffer) Read(p []byte ) (n int , err error )func (b *Buffer) ReadByte() (byte , error )func (b *Buffer) ReadBytes(delim byte ) (line []byte , err error )func (b *Buffer) ReadRune() (r rune , size int , err error )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
1 2 3 4 5 6 7 8 9 10 11 func (b *Buffer) Write(p []byte ) (n int , err error )func (b *Buffer) WriteByte(c byte ) error func (b *Buffer) WriteRune(r rune ) (n int , err error )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 { s []byte i int64 prevRune int }
Reader 类型实现了 io.Reader
、io.ReaderAt
、io.Seeker
等接口,当需要一个从字节切片中读取数据的 io.Reader
实现时,就可以使用 bytes.Reader
类型。
下面列举 bytes.Reader
支持的函数/方法:
NewReader
创建一个 Reader,通过该 Reader 可以从字节序列 b 中读取数据
1 func NewReader (b []byte ) *Reader
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) }
1 2 3 4 5 6 7 8 9 10 11 func (r *Reader) Read(b []byte ) (n int , err error )func (r *Reader) ReadAt(b []byte , off int64 ) (n int , err error )func (r *Reader) ReadByte() (byte , error )func (r *Reader) ReadRune() (ch rune , size int , err error )
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 )
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 包为如何编写这种工具类包代码提供了很好的范例。