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® 为 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 包为如何编写这种工具类包代码提供了很好的范例。