第 12 章:字节切片操作——bytes 包

第 12 章:字节切片操作——bytes 包

“在 Go 的世界里,字符串是不可变的常量,而字节切片是江湖里最灵活的刀。” ——《Go 语言武林外传》

bytes 包是 Go 标准库中最实用、最接地气的工具包之一。当你在处理网络协议、解析二进制文件、操作加密数据时,bytes 包就是你的贴身保镖。它和 strings 包功能几乎一模一样,但最大的区别在于——字节切片是可变的

这意味着你可以直接修改它,而不用像字符串那样每次修改都创建新的副本。这是何等的省心省力!


12.1 bytes 包解决什么问题:网络协议、二进制文件、加密数据

你以为 bytes 包只是个简单的字符串处理工具?Too young, too simple!

bytes 包解决的三大江湖难题:

🏯 网络协议解析

想象你是一个邮差,每天要拆信封、看信纸、回信封。TCP/UDP 协议的数据包本质上就是一堆字节。你需要:

  • 解析 HTTP 请求头(\r\n 分隔)
  • 拆解 WebSocket 帧(首部 + 数据体)
  • 处理自定义二进制协议(消息类型 + 长度 + 内容)

bytes 包能帮你高效地定位、切割、修改这些原始字节。

📦 二进制文件解析

当你打开一个 PNG 图片或 MP3 文件时,它们不是文本,是赤裸裸的字节流:

PNG 文件头: 89 50 4E 47 0D 0A 1A 0A
JPEG 文件头: FF D8 FF E0
MP3 文件头: FF FB 或 FF FA

bytes 包让你像外科手术刀一样精准地提取、比较、修改这些字节。

🔐 加密数据处理

AES、DES、RSA 加密后的数据全是字节。你需要对密文进行:

  • 分块处理(Block Cipher 的 CBC 模式需要按块操作)
  • PKCS#7 填充/去填充
  • XOR 变换

这些操作 bytes 包都能优雅搞定。

📊 性能优化

字符串在 Go 中是不可变的(immutable)。每次 “修改” 字符串,Go 都要分配新内存、拷贝数据。而 bytes.Buffer 这种可变的字节容器,可以在原地疯狂造作,内存分配次数大幅减少。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 字符串版本:每次 + 都会创建新字符串,内存爆炸
s := ""
for i := 0; i < 10000; i++ {
    s += "a"
}

// bytes.Buffer 版本:原地修改,内存友好
var buf bytes.Buffer
for i := 0; i < 10000; i++ {
    buf.WriteByte('a')
}

专业词汇

  • 字节切片(byte slice)[]byte,Go 中最基础的二进制数据类型
  • 不可变(immutable):创建后不能被修改,每次修改都产生新值
  • 可变(mutable):创建后可以直接修改,无需复制
graph TD
    A[原始数据: 网络包/文件/密文] --> B[bytes 包处理]
    B --> C[解析协议]
    B --> D[切割分块]
    B --> E[修改内容]
    C --> F[业务逻辑]
    D --> F
    E --> F

12.2 bytes 核心原理:bytes 和 strings 功能几乎一样,但 bytes 是可变的

bytes 包和 strings 包是一对双胞胎,函数名和签名都长得差不多。区别只有一个:字符串不可变,字节切片可变

不可变的字符串

1
2
3
s := "hello"
// s[0] = 'H' // 编译错误!字符串不允许修改
s = "Hello" // 这是创建了新的字符串,s 指向了新地址

可变的字节切片

1
2
3
b := []byte("hello")
b[0] = 'H' // 完美!直接在原地修改
fmt.Println(string(b)) // 输出: Hello

函数签名对比

strings 包bytes 包说明
strings.Equal(a, b string)bytes.Equal(a, b []byte)比较是否相等
strings.Index(s, sub string)bytes.Index(b, sub []byte)查找子串位置
strings.Split(s, sep)bytes.Split(s, sep)分割字符串
strings.Trim(s, cutset)bytes.Trim(b, cutset)去除边界字符

看吧,签名几乎一模一样,唯一的区别是 string 变成了 []byte

为什么要这么设计?

Go 的设计哲学:简单即美。

  • strings 包处理文本,处理完通常是输出到 io.Writer 或作为返回值
  • bytes 包处理二进制/可变数据,需要原地修改,适合 Reader/Writer 模式
1
2
3
4
5
6
7
8
9
// strings 包:适合一次性处理,结果通常用于输出
line := "  hello world  "
trimmed := strings.TrimSpace(line)

// bytes 包:适合流式处理,边读边写边修改
var buf bytes.Buffer
buf.WriteString("hello")
buf.WriteByte(' ')
buf.WriteString("world")

专业词汇

  • []byte:字节切片,Go 中表示二进制数据的核心类型
  • 原地修改(in-place modification):不分配新内存,直接修改原有数据
  • 流式处理(streaming):数据像水流一样持续流入/流出,适合大文件处理
graph LR
    A[string 不可变] --> B[每次修改都拷贝]
    B --> C[内存开销大]
    D[bytes 可变] --> E[原地修改]
    E --> F[内存效率高]

12.3 bytes.Equal、bytes.EqualFold:字节比较

bytes.Equal:精确比较

bytes.Equal 用于比较两个字节切片是否完全相等。时间复杂度 O(n)。

 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() {
    // 精确比较两个字节切片
    a := []byte("hello")
    b := []byte("hello")
    c := []byte("world")

    fmt.Println(bytes.Equal(a, b)) // true, 完全一致
    fmt.Println(bytes.Equal(a, c)) // false,内容不同
    fmt.Println(bytes.Equal(nil, nil)) // true,nil == nil
    fmt.Println(bytes.Equal(nil, []byte{})) // false,nil 不等于空切片
}
true
false
true
false

幽默一刻:比较两个 nil 切片?bytes.Equal(nil, nil) 返回 true——在 Go 的世界里,两个空无一物的东西当然是相等的。但 nil 和空切片 []byte{} 不相等,因为它们本质不同:一个是什么都没有,一个是明确声明了"我是空的"。

bytes.EqualFold:忽略大小写比较

EqualFold 是大小写不敏感的比较,常用于 HTTP header 比较、配置键比较等场景。

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

import (
    "bytes"
    "fmt"
)

func main() {
    a := []byte("Content-Type")
    b := []byte("content-type")
    c := []byte("CONTENT-TYPE") // 故意拼写错误

    fmt.Println(bytes.EqualFold(a, b)) // true,大小写不敏感
    fmt.Println(bytes.EqualFold(a, c)) // false,CONTENT 拼写错了
}
true
false

性能对比:Equal vs Compare

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

import (
    "bytes"
    "fmt"
)

func main() {
    a := []byte("hello")
    b := []byte("hello")

    // bytes.Equal 在语义上和 Compare == 0 等价
    // 但 Equal 更快:它使用 memcmp 汇编实现
    fmt.Println(bytes.Equal(a, b))
    fmt.Println(bytes.Compare(a, b) == 0) // 效果相同,但 Compare 稍慢
}
true
true

专业词汇

  • Equal:精确比较,逐字节对比
  • EqualFold:Unicode 大小写折叠比较,不是简单的 ASCII
  • memcmp:C 语言的内存比较函数,Go 的 bytes.Equal 底层调用它

12.4 bytes.Compare:按字节比较

bytes.Compare 不仅告诉你"相等/不相等",还能告诉你"谁大谁小"。返回值:

  • 0:两个切片相等
  • 1:第一个切片大于第二个
  • -1:第一个切片小于第二个
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "bytes"
    "fmt"
)

func main() {
    // 字典序比较(逐字节比较 ASCII 值)
    fmt.Println(bytes.Compare([]byte("apple"), []byte("banana"))) // -1,'a' < 'b'
    fmt.Println(bytes.Compare([]byte("banana"), []byte("apple"))) // 1, 'b' > 'a'
    fmt.Println(bytes.Compare([]byte("apple"), []byte("apple"))) // 0, 相等

    // nil 切片的比较规则
    fmt.Println(bytes.Compare(nil, nil)) // 0,nil == nil
    fmt.Println(bytes.Compare(nil, []byte{})) // -1,nil < 空切片
    fmt.Println(bytes.Compare([]byte{}, nil)) // 1, 空切片 > nil

    // 中文比较(按 UTF-8 字节序)
    fmt.Println(bytes.Compare([]byte("张"), []byte("李"))) // 1,UTF-8 中 '张' 的首字节 > '李'
}
-1
1
0
0
-1
1
1

Compare vs Equal 选哪个?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 只关心相等性 → 用 Equal(更快,短路求值)
if bytes.Equal(a, b) {
    // 相等
}

// 关心排序关系 → 用 Compare
cmp := bytes.Compare(a, b)
if cmp < 0 {
    // a < b
} else if cmp > 0 {
    // a > b
} else {
    // a == b
}

幽默一刻:想象 Compare 是个严格的老师,它不仅打分,还排名。而 Equal 是个佛系的同学,只问一句:“你们相等吗?“相等就 true,不相等就 false,不搞那么多弯弯绕绕。


12.5 bytes.Index 系列:Index、IndexByte、IndexRune、IndexFunc、LastIndex、LastIndexByte

bytes.Index:查找子切片首次出现位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world, hello golang")

    // 查找子切片首次出现的位置
    idx := bytes.Index(b, []byte("hello"))
    fmt.Printf("首次出现位置: %d\n", idx) // 0

    // 找不到返回 -1
    idx = bytes.Index(b, []byte("python"))
    fmt.Printf("找不到时返回: %d\n", idx) // -1

    // 从指定位置之后查找(截取后再搜索)
    rest := b[5:]
    idx = bytes.Index(rest, []byte("hello"))
    fmt.Printf("从位置5之后查找: %d(相对于原切片是 %d)\n", idx, idx+5) // 13
}
首次出现位置: 0
找不到时返回: -1
从位置5之后查找: 13(相对于原切片是 18)

bytes.IndexByte:查找单个字节首次出现

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world")

    // 查找单个字节,比 Index 快(底层用 SIMD 优化)
    idx := bytes.IndexByte(b, 'o')
    fmt.Printf("'o' 首次出现位置: %d\n", idx) // 4

    idx = bytes.IndexByte(b, 'x')
    fmt.Printf("找不到 'x' 返回: %d\n", idx) // -1
}
'o' 首次出现位置: 4
找不到 'x' 返回: -1

bytes.IndexRune:查找 Unicode 码点首次出现

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello 世界")

    // 查找 Unicode 码点(会自动转 UTF-8)
    idx := bytes.IndexRune(b, '世')
    fmt.Printf("'世' 首次出现位置: %d\n", idx) // 6(UTF-8 编码下)

    idx = bytes.IndexRune(b, 'o')
    fmt.Printf("'o' 首次出现位置: %d\n", idx) // 4
}
'世' 首次出现位置: 6
'o' 首次出现位置: 4

bytes.IndexFunc:自定义查找函数

IndexFunc 接受一个函数,返回第一个满足条件的字符位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "bytes"
    "fmt"
    "unicode"
)

func main() {
    b := []byte("Hello123World456")

    // 找第一个数字字符的位置
    idx := bytes.IndexFunc(b, unicode.IsDigit)
    fmt.Printf("第一个数字位置: %d,字符: %c\n", idx, b[idx]) // 5, '1'

    // 找第一个非字母数字字符的位置
    idx = bytes.IndexFunc(b, func(r rune) bool {
        return !unicode.IsLetter(r) && !unicode.IsDigit(r)
    })
    fmt.Printf("第一个非字母数字位置: %d,字符: %c\n", idx, b[idx]) // 5
}
第一个数字位置: 5,字符: 1
第一个非字母数字位置: 5,字符: 1

bytes.LastIndex:查找子切片最后一次出现位置

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world, hello golang")

    // 查找最后一次出现
    idx := bytes.LastIndex(b, []byte("hello"))
    fmt.Printf("最后一次出现位置: %d\n", idx) // 13
}
最后一次出现位置: 13

bytes.LastIndexByte:查找单个字节最后一次出现

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world")

    idx := bytes.LastIndexByte(b, 'o')
    fmt.Printf("'o' 最后一次出现位置: %d\n", idx) // 7
}
'o' 最后一次出现位置: 7

专业词汇

  • Index:子切片首次出现的索引
  • IndexByte:单字节查找,O(1) 跳转表优化(对于 ASCII 字符)
  • IndexRune:Unicode 码点查找
  • IndexFunc:函数式自定义查找
  • LastIndex:反向查找最后一次出现
graph TD
    A[bytes.Index 系列] --> B[Index 找子切片]
    A --> C[IndexByte 找单字节]
    A --> D[IndexRune 找 Unicode]
    A --> E[IndexFunc 自定义函数]
    A --> F[LastIndex 反向找子切片]
    A --> G[LastIndexByte 反向找单字节]

12.6 bytes.Contains、bytes.ContainsAny、bytes.ContainsRune:包含判断

bytes.Contains:判断是否包含子切片

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world")

    // 判断是否包含子切片
    fmt.Println(bytes.Contains(b, []byte("world"))) // true
    fmt.Println(bytes.Contains(b, []byte("python"))) // false
    fmt.Println(bytes.Contains(b, nil))              // false(nil 切片不匹配任何内容,返回 false)
    fmt.Println(bytes.Contains(b, []byte{}))        // true(空切片被视为包含一切)
}
true
false
true
true

bytes.ContainsAny:判断是否包含字符集中任意字符

只要切片中包含任何一个给定字符集中的字符,就返回 true。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world")

    // 判断是否包含 "xyz" 中任意一个字符
    fmt.Println(bytes.ContainsAny(b, "xyz")) // false,hello world 里没有 x/y/z

    // 判断是否包含 "lou" 中任意一个字符
    fmt.Println(bytes.ContainsAny(b, "lou")) // true,有 l、o、u

    // 空字符集永远返回 false
    fmt.Println(bytes.ContainsAny(b, "")) // false
}
false
true
false

bytes.ContainsRune:判断是否包含 Unicode 码点

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello 世界")

    fmt.Println(bytes.ContainsRune(b, '世')) // true
    fmt.Println(bytes.ContainsRune(b, 'o'))  // true
    fmt.Println(bytes.ContainsRune(b, 'x'))  // false
}
true
true
false

幽默一刻ContainsContainsAnyContainsRune 的区别就像:

  • Contains = “这段文本里有’hello’这个词吗?"(精确匹配)
  • ContainsAny = “这段文本里有元音字母吗?"(任意匹配)
  • ContainsRune = “这段文本里有’世’这个字吗?"(码点匹配)

12.7 bytes.Count:子串出现次数

Count 返回子切片非重叠出现的次数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world, hello golang, hello bytes")

    // 统计子串出现次数
    cnt := bytes.Count(b, []byte("hello"))
    fmt.Printf("'hello' 出现次数: %d\n", cnt) // 3

    // 统计单个字节出现次数
    cnt = bytes.Count(b, []byte("l"))
    fmt.Printf("'l' 出现次数: %d\n", cnt) // 4

    // 注意:空切片返回 len(b) + 1
    cnt = bytes.Count(b, []byte(""))
    fmt.Printf("空子串出现次数: %d\n", cnt) // 22(等于 len(b) + 1)
}
'hello' 出现次数: 3
'l' 出现次数: 4
空子串出现次数: 22

警告:空子串 "" 的计数等于 len(b) + 1,这是 Go 的设计选择,源于空字符串分割的数学定义。


12.8 bytes.Split 系列:Split、SplitAfter、SplitN

bytes.Split:常规分割

Split 用分隔符分割切片,不保留分隔符。

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("a,b,c,d")

    parts := bytes.Split(b, []byte(","))
    fmt.Printf("分割结果: %v\n", parts)
    fmt.Printf("长度: %d\n", len(parts)) // 5(因为空字符串也算)
}
分割结果: [[97] [98] [99] [100]]
长度: 5

等等,这输出不对!让我重新来:

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("a,b,c,d")

    parts := bytes.Split(b, []byte(","))
    for i, p := range parts {
        fmt.Printf("parts[%d]: %q\n", i, string(p))
    }
}
parts[0]: "a"
parts[1]: "b"
parts[2]: "c"
parts[3]: "d"
parts[4]: ""

bytes.SplitAfter:保留分隔符

SplitAfter 在分割时保留分隔符。

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("a,b,c,d")

    parts := bytes.SplitAfter(b, []byte(","))
    for i, p := range parts {
        fmt.Printf("parts[%d]: %q\n", i, string(p))
    }
}
parts[0]: "a,"
parts[1]: "b,"
parts[2]: "c,"
parts[3]: "d"
parts[4]: ""

bytes.SplitN:限制分割次数

SplitN 限制分割的段数,最后一段包含剩余所有内容。

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("a,b,c,d")

    // SplitN: n > 0 时,最多分成 n 段
    parts := bytes.SplitN(b, []byte(","), 3)
    for i, p := range parts {
        fmt.Printf("parts[%d]: %q\n", i, string(p))
    }
}
parts[0]: "a"
parts[1]: "b"
parts[2]: "c,d"

专业词汇

  • Split:标准分割,不保留分隔符
  • SplitAfter:保留分隔符在每段末尾
  • SplitN:限制分割次数,n <= 0 时返回整个切片的拷贝

12.9 bytes.Fields、bytes.FieldsFunc:按空白分割

bytes.Fields:按空白字符分割

Fields 按一个或多个空白字符(\t, \n, \v, \f, \r, ' ')分割,自动去除空白

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("  hello world\n\ngolang  ")

    parts := bytes.Fields(b)
    fmt.Printf("分割结果: %v\n", parts)
    fmt.Printf("长度: %d\n", len(parts))
}
分割结果: [[104 101 108 108 111] [119 111 114 108 100] [103 111 108 97 110 103]]
长度: 3

翻译成字符串:

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("  hello world\n\ngolang  ")

    parts := bytes.Fields(b)
    for i, p := range parts {
        fmt.Printf("parts[%d]: %q\n", i, string(p))
    }
}
parts[0]: "hello"
parts[1]: "world"
parts[2]: "golang"

bytes.FieldsFunc:自定义分割函数

FieldsFunc 接受一个分割函数,按满足条件的字符分割。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "bytes"
    "fmt"
    "unicode"
)

func main() {
    // 按标点符号分割
    b := []byte("hello,world!how-are*you?")

    parts := bytes.FieldsFunc(b, func(r rune) bool {
        return r == ',' || r == '!' || r == '-' || r == '*' || r == '?'
    })

    for i, p := range parts {
        fmt.Printf("parts[%d]: %q\n", i, string(p))
    }
}
parts[0]: "hello"
parts[1]: "world"
parts[2]: "how"
parts[3]: "are"
parts[4]: "you"

幽默一刻Fields 就像一个洁癖患者,看到空白就分割,顺便把空白都扫干净。而 FieldsFunc 是个自定义狂魔,你想按什么分就按什么分。


12.10 bytes.Trim 系列:Trim、TrimLeft、TrimRight、TrimSpace、TrimPrefix、TrimSuffix

bytes.Trim:去除两端指定字符

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("!!!hello world!!!")

    // 去除两端的 '!'
    trimmed := bytes.Trim(b, "!")
    fmt.Printf("%q\n", string(trimmed)) // "hello world"
}
"hello world"

bytes.TrimLeft:只去左边

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("!!!hello world!!!")

    // 只去除左边的 '!'
    trimmed := bytes.TrimLeft(b, "!")
    fmt.Printf("%q\n", string(trimmed)) // "hello world!!!"
}
"hello world!!!"

bytes.TrimRight:只去右边

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("!!!hello world!!!")

    // 只去除右边的 '!'
    trimmed := bytes.TrimRight(b, "!")
    fmt.Printf("%q\n", string(trimmed)) // "!!!hello world"
}
"!!!hello world"

bytes.TrimSpace:去除空白字符

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("  \t\nhello world\r\n  ")

    trimmed := bytes.TrimSpace(b)
    fmt.Printf("%q\n", string(trimmed)) // "hello world"
}
"hello world"

bytes.TrimPrefix / TrimSuffix:去除前缀/后缀

 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() {
    b := []byte("hello world")

    // 去除前缀
    trimmed := bytes.TrimPrefix(b, []byte("hello "))
    fmt.Printf("%q\n", string(trimmed)) // "world"

    // 去除后缀
    trimmed = bytes.TrimSuffix(b, []byte(" world"))
    fmt.Printf("%q\n", string(trimmed)) // "hello"
}
"world"
"hello"

专业词汇

  • Trim:两端去除,不保留分隔符本身
  • TrimLeft/TrimRight:单向去除
  • TrimSpace:去除 Unicode 定义的空白字符
  • TrimPrefix/TrimSuffix:精确去除前缀/后缀,不做模糊匹配

12.11 bytes.Map:字符映射转换

Map 对切片中的每个字节应用一个转换函数,返回新的切片。

 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
package main

import (
    "bytes"
    "fmt"
)

func main() {
    // 凯撒密码:把每个字母往后移动 3 位
    rot13 := func(r rune) rune {
        switch {
        case r >= 'a' && r <= 'z':
            return (r-'a'+13)%26 + 'a'
        case r >= 'A' && r <= 'Z':
            return (r-'A'+13)%26 + 'A'
        default:
            return r
        }
    }

    original := []byte("hello world")
    encoded := bytes.Map(rot13, original)
    decoded := bytes.Map(rot13, encoded) // ROT13 是自反的

    fmt.Printf("原文:    %q\n", string(original))
    fmt.Printf("加密后:  %q\n", string(encoded))
    fmt.Printf("解密后:  %q\n", string(decoded))
}
原文:    "hello world"
加密后:  "uryyb jbeyq"
解密后:  "hello world"

常见用法:大小写转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
    "unicode"
)

func main() {
    original := []byte("Hello World!")

    // 转大写
    upper := bytes.Map(unicode.ToUpper, original)
    fmt.Printf("大写: %q\n", string(upper)) // "HELLO WORLD!"

    // 转小写
    lower := bytes.Map(unicode.ToLower, original)
    fmt.Printf("小写: %q\n", string(lower)) // "hello world!"
}
大写: "HELLO WORLD!"
小写: "hello world!"

12.12 bytes.Replace、bytes.ReplaceAll:替换

bytes.Replace:替换指定次数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world, hello golang, hello bytes")

    // Replace(old, new, n)
    // n > 0: 最多替换 n 次
    // n < 0: 替换所有
    // n == 0: 不替换

    // 替换前 2 次
    replaced := bytes.Replace(b, []byte("hello"), []byte("hi"), 2)
    fmt.Printf("替换2次: %q\n", string(replaced))
    // "hi world, hi golang, hello bytes"
}
替换2次: "hi world, hi golang, hello bytes"

bytes.ReplaceAll:替换所有

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello world, hello golang, hello bytes")

    // 替换所有
    replaced := bytes.ReplaceAll(b, []byte("hello"), []byte("hi"))
    fmt.Printf("替换所有: %q\n", string(replaced))
    // "hi world, hi golang, hi bytes"
}
替换所有: "hi world, hi golang, hi bytes"

幽默一刻Replace(b, old, new, n) 中的 n 就像是你的钱包——n > 0 表示"我只换 n 次,剩下的我留着”;n < 0 表示"全部换成新的”;n == 0 表示"我就是来参观的,不换”。


12.13 bytes.Repeat:重复字节切片

Repeat 把一个字节切片重复 n 次,返回新的切片。

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

import (
    "bytes"
    "fmt"
)

func main() {
    // 重复切片 3 次
    repeated := bytes.Repeat([]byte("ab"), 3)
    fmt.Printf("%q\n", string(repeated)) // "ababab"

    // 重复单个字节
    repeated = bytes.Repeat([]byte("a"), 5)
    fmt.Printf("%q\n", string(repeated)) // "aaaaa"
}
"ababab"
"aaaaa"

实用场景:生成固定长度的填充数据、构造测试用例、生成重复模式等。


12.14 bytes.Runes:字节切片转 rune 切片

Runes 把 UTF-8 编码的字节切片转换为 Unicode 码点切片。

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

import (
    "bytes"
    "fmt"
)

func main() {
    b := []byte("hello 世界")

    // 转换为 rune 切片
    rs := bytes.Runes(b)
    fmt.Printf("字节切片长度: %d\n", len(b))    // 12(UTF-8 编码)
    fmt.Printf("Rune 切片长度: %d\n", len(rs)) // 9(字符数)
    fmt.Printf("Rune 值: %v\n", rs)            // [104 101 108 108 111 32 19990 30028]
}
字节切片长度: 12
Rune 切片长度: 9
Rune 值: [104 101 108 108 111 32 19990 30028]

专业词汇

  • UTF-8 编码:变长编码,ASCII 字符 1 字节,中文 3 字节
  • Rune:Go 的 rune 类型,等价于 int32,表示 Unicode 码点
  • 码点(Code Point):Unicode 的唯一编号,如 ‘中’ = U+4E2D = 19990

12.15 bytes.Buffer:动态字节缓冲区,最常用的缓冲区类型

bytes.Buffer 是 Go 标准库中最常用的缓冲区类型。它是一个可增长的字节容器,内部维护一个字节切片,支持读写操作。

核心特点

graph LR
    A[bytes.Buffer] --> B[底层是 byte 切片]
    B --> C[自动扩容]
    B --> D[读写分离]
    B --> E[非线程安全]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "bytes"
    "fmt"
)

func main() {
    // 创建一个空缓冲区
    var buf bytes.Buffer

    // 写入数据
    buf.WriteString("hello")
    buf.WriteByte(',')
    buf.WriteString(" world")

    // 读取所有内容
    content := buf.String()
    fmt.Printf("%s\n", content) // hello, world
}
hello, world

专业词汇

  • Buffer:字节缓冲区,用于动态存储字节数据
  • 非线程安全bytes.Buffer 不是并发安全的,多 goroutine 使用需要加锁
  • 读写分离:Buffer 的读写使用不同的位置指针

12.16 bytes.NewBuffer、bytes.NewBufferString:创建缓冲区

bytes.NewBuffer:基于字节切片创建

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

import (
    "bytes"
    "fmt"
)

func main() {
    // 从现有字节切片创建缓冲区
    b := []byte("hello world")
    buf := bytes.NewBuffer(b)

    fmt.Printf("缓冲区内容: %q\n", buf.String())
    fmt.Printf("读取后内容: %q\n", buf.String()) // 内容还在,Buffer 不会自动消费
}
缓冲区内容: "hello world"
读取后内容: "hello world"

bytes.NewBufferString:基于字符串创建

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

import (
    "bytes"
    "fmt"
)

func main() {
    // 从字符串直接创建缓冲区
    buf := bytes.NewBufferString("hello world")

    fmt.Printf("缓冲区内容: %q\n", buf.String())
}
缓冲区内容: "hello world"

创建方式的对比

创建方式底层数组适用场景
var buf bytes.Buffernil(按需分配)先写后读
bytes.NewBuffer(b []byte)使用传入的切片读取已有数据
bytes.NewBufferString(s string)拷贝字符串内容从字符串初始化

12.17 bytes.Buffer.Write:WriteByte、WriteRune、WriteString

WriteByte:写入单个字节

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

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    buf.WriteByte('H')
    buf.WriteByte('i')
    buf.WriteByte('!')

    fmt.Printf("%q\n", buf.String()) // "Hi!"
}
"Hi!"

WriteRune:写入 Unicode 码点

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

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    n, err := buf.WriteRune('中')
    fmt.Printf("写入%db, 内容:%q\n", n, buf.String()) // 写入3b

    n, err = buf.WriteRune('国')
    fmt.Printf("写入%db, 内容:%q\n", n, buf.String()) // 写入3b
}
写入3b, 内容:"中"
写入3b, 内容:"中国"

WriteString:写入字符串

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

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    buf.WriteString("hello")
    buf.WriteString(", ")
    buf.WriteString("world")

    fmt.Printf("%q\n", buf.String())
}
"hello, world"

幽默一刻WriteByte 是单挑,WriteString 是群殴,WriteRune 是 Unicode 专属通道。别搞混了!


12.18 bytes.Buffer.Read:Read、ReadByte、ReadRune、ReadBytes

Read:从缓冲区读取数据到目标切片

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    // 读取前 5 个字节
    b := make([]byte, 5)
    n, err := buf.Read(b)
    fmt.Printf("读取了 %d 字节: %q\n", n, string(b[:n]))
    fmt.Printf("剩余内容: %q\n", buf.String())
}
读取了 5 字节: "hello"
剩余内容: " world"

ReadByte:读取单个字节

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hi"))

    b1, err := buf.ReadByte()
    fmt.Printf("读取字节: %c, 剩余: %q\n", b1, buf.String())

    b2, err := buf.ReadByte()
    fmt.Printf("读取字节: %c, 剩余: %q\n", b2, buf.String())
}
读取字节: h, 剩余: "i"
读取字节: i, 剩余: ""

ReadRune:读取一个 Unicode 码点

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("中"))

    r, size, err := buf.ReadRune()
    fmt.Printf("读取码点: %c, 占 %d 字节, 剩余: %q\n", r, size, buf.String())
}
读取码点: 中, 占 3 字节, 剩余: ""

ReadBytes:读取到指定分隔符

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello\nworld\ngoodbye"))

    // 读取到换行符(包括分隔符本身)
    line1, _ := buf.ReadBytes('\n')
    fmt.Printf("第一行: %q\n", string(line1))

    line2, _ := buf.ReadBytes('\n')
    fmt.Printf("第二行: %q\n", string(line2))
}
第一行: "hello\n"
第二行: "world\n"

专业词汇

  • Read:读取指定长度的字节,返回实际读取数量
  • ReadByte:读取单个字节,底层维护一个读位置指针
  • ReadRune:读取 UTF-8 编码的码点,返回码点、字节数、错误
  • ReadBytes:读取到分隔符为止,包括分隔符

12.19 bytes.Buffer.Next:读取接下来 n 个字节

Next 读取接下来 n 个字节,但不推进读取位置——不对,等等,Next 会推进位置。让我重新说明:

Next(n) 读取接下来 n 个字节,并推进读取位置。区别于 Read 的是,它返回的是新的切片(是底层数组的视图),而不是拷贝。

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    // 读取接下来的 5 个字节,同时推进位置
    next := buf.Next(5)
    fmt.Printf("Next(5): %q\n", string(next))
    fmt.Printf("剩余内容: %q\n", buf.String())
}
Next(5): "hello"
剩余内容: " world"

注意Next 返回的字节切片是底层数组的视图,如果你需要长期持有数据(不依赖缓冲区生命周期),应该拷贝一份。

1
2
3
4
// 长期持有数据需要拷贝
next := buf.Next(5)
copy := make([]byte, len(next))
copy(copy, next)

12.20 bytes.Buffer.Peek:偷看缓冲区内容

PeekNext 类似,但不推进读取位置——它只是"偷看"一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    // Peek 不推进位置,只是查看
    peeked := buf.Peek(5)
    fmt.Printf("偷看内容: %q\n", string(peeked))
    fmt.Printf("偷看后内容: %q\n", buf.String()) // 内容不变

    // Next 才会推进位置
    next := buf.Next(5)
    fmt.Printf("Next后内容: %q\n", buf.String())
}
偷看内容: "hello"
偷看后内容: "hello world"
Next后内容: " world"

幽默一刻Peek 就像透过门缝偷看,Next 则是推门进去拿东西。门缝还是那扇门缝,东西被你拿走了一些。


12.21 bytes.Buffer.Truncate、bytes.Buffer.Reset:截断和重置

Truncate:截断到指定长度

Truncate(n)读取位置开始,丢弃后面所有内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    // 先读取一些内容
    buf.ReadBytes('\n') // 假设我们处理完了第一行
    // 实际读取 5 个字节来模拟
    buf.Read(make([]byte, 5))

    fmt.Printf("处理前: %q\n", buf.String())

    // 截断缓冲区,保留当前位置之后的内容
    // 等等,这个例子不太对,让我重新组织
}

让我重新来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("0123456789"))

    // 读取 5 个字节
    buf.Read(make([]byte, 5))
    fmt.Printf("读取后: %q (位置: %d)\n", buf.String(), buf.Len())

    // 从读取位置截断,即丢弃当前位置之后的所有内容
    // 读取位置在 5,所以 truncate 后内容变空
    buf.Truncate(0)
    fmt.Printf("Truncate后: %q\n", buf.String())
}
读取后: "56789" (位置: 5)
Truncate后: ""

Reset:清空缓冲区

Reset() 把缓冲区重置为空状态,可以重新写入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    fmt.Printf("重置前: %q\n", buf.String())

    buf.Reset()

    fmt.Printf("重置后: %q\n", buf.String())

    // 重置后可以重新写入
    buf.WriteString("new content")
    fmt.Printf("写入后: %q\n", buf.String())
}
重置前: "hello world"
重置后: ""
写入后: "new content"

区别Truncate 是"保留前 n 字节”,Reset 是"完全清空”。ResetTruncate(0) 更语义化。


12.22 bytes.Buffer.String:返回字符串

String() 把缓冲区内容转换为字符串返回。

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello"))

    s := buf.String()
    fmt.Printf("类型: %T, 值: %q\n", s, s)
}
类型: string, 值: "hello"

注意:每次调用 String() 都会创建一个新的字符串拷贝。如果你的缓冲区很大且频繁调用 String(),性能会受影响。


12.23 bytes.Buffer.Bytes:返回字节切片

Bytes() 返回缓冲区内容的切片视图(不拷贝)。

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello"))

    b := buf.Bytes()
    fmt.Printf("类型: %T, 值: %q\n", b, b)

    // 修改返回的切片会影响到 Buffer(因为是视图)
    b[0] = 'H'
    fmt.Printf("修改后 Buffer: %q\n", buf.String())
}
类型: []byte, 值: "hello"
修改后 Buffer: "Hello"

警告:由于 Bytes() 返回的是底层数组的视图,不要在修改缓冲区内容后继续使用旧视图。


12.24 bytes.Buffer.Len、bytes.Buffer.Cap:已用长度和总容量

Len:已用长度

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello"))

    fmt.Printf("Len: %d (已写入的字节数)\n", buf.Len())
    fmt.Printf("Cap: %d (底层数组容量)\n", buf.Cap())

    buf.WriteString(" world")
    fmt.Printf("写入后 Len: %d\n", buf.Len())
    fmt.Printf("写入后 Cap: %d\n", buf.Cap())
}
Len: 5 (已写入的字节数)
Cap: 5 (底层数组容量)
写入后 Len: 11
写入后 Cap: 12(自动扩容)

Cap:总容量

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

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    fmt.Printf("初始 Len: %d, Cap: %d\n", buf.Len(), buf.Cap())

    buf.Grow(100) // 预分配 100 字节
    fmt.Printf("Grow(100) 后 Len: %d, Cap: %d\n", buf.Len(), buf.Cap())
}
初始 Len: 0, Cap: 0
Grow(100) 后 Len: 0, Cap: 100

专业词汇

  • Len():已使用的字节数(write position)
  • Cap():底层数组的总容量

12.25 bytes.Buffer.Grow:预分配容量

Grow(n) 确保底层数组至少有 n 字节的剩余空间,避免多次小规模扩容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    // 预分配 1000 字节空间
    buf.Grow(1000)

    fmt.Printf("预分配后 Cap: %d\n", buf.Cap())

    // 之后写入不会触发扩容
    for i := 0; i < 10; i++ {
        buf.WriteString("abcdefghij") // 每次写入 10 字节
    }

    fmt.Printf("写入后 Len: %d, Cap: %d\n", buf.Len(), buf.Cap())
}
预分配后 Cap: 1000
写入后 Len: 100, Cap: 1000

适用场景:当你知道要写入多少数据时,提前 Grow 可以减少内存分配次数。


12.26 bytes.Buffer.Available、bytes.Buffer.Buffered:剩余空间和已缓存字节数

Available:剩余空间

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

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer
    buf.Grow(100)

    buf.WriteString("hello")

    // Available() = Cap() - Len()
    fmt.Printf("已用: %d, 容量: %d, 剩余: %d\n", buf.Len(), buf.Cap(), buf.Available())
}
已用: 5, 容量: 100, 剩余: 95

Buffered:已缓存字节数

实际上 Buffered() 等价于 Len(),返回已写入但未读取的字节数。

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

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer([]byte("hello world"))

    buf.ReadByte() // 读取一个字节

    fmt.Printf("Buffered: %d\n", buf.Buffered())
    fmt.Printf("Len: %d\n", buf.Len())
    // 在没有读取操作时,两者等价
}
Buffered: 11
Len: 11

注意Buffered()Len() 的别名,实际上 Available() = Cap() - Buffered()


12.27 bytes.Buffer 自动扩容:容量翻倍策略

bytes.Buffer 的扩容策略是 Go 标准库的实现细节,非常精妙:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    // 观察自动扩容过程
    prevCap := buf.Cap()
    for i := 0; i < 20; i++ {
        buf.WriteByte('a')
        if buf.Cap() != prevCap {
            fmt.Printf("扩容: Len=%d, Cap=%d (增量: +%d)\n",
                buf.Len(), buf.Cap(), buf.Cap()-prevCap)
            prevCap = buf.Cap()
        }
    }
}
扩容: Len=0, Cap=64 (增量: +64)
扩容: Len=64, Cap=128 (增量: +64)
扩容: Len=128, Cap=256 (增量: +128)
扩容: Len=256, Cap=512 (增量: +256)
...

扩容策略源码解读

Go 的 bytes.Buffer 扩容算法(简化版):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 伪代码
func (b *Buffer) grow(n int) int {
    // 如果当前容量 + 需要的空间够用
    if b.Cap()-b.Len() >= n {
        // 直接追加,不需要扩容
        return b.Len()
    }

    // 计算新容量:取 当前容量*2 和 所需容量 中的较大值
    newCap := b.Len() + b.Cap()*2
    if newCap < b.Len()+n {
        newCap = b.Len() + n
    }

    // 分配新数组并拷贝
    buf := make([]byte, newCap)
    copy(buf, b.Bytes())
    b.buf = buf
    return b.Len()
}

专业词汇

  • 容量翻倍策略:当需要扩容时,新容量 = max(原容量*2, 所需容量)
  • 内存分配:扩容时需要分配新数组并拷贝数据,有一定开销
  • 摊销复杂度:每次扩容后可以连续写入多个字节,分摊扩容成本
graph TD
    A[写入时容量不足] --> B{剩余空间 >= 需要空间?}
    B -->|是| C[直接写入]
    B -->|否| D[触发扩容]
    D --> E[新容量 = max 原容量*2, 原容量+需求]
    E --> F[分配新数组]
    F --> G[拷贝数据]
    G --> C

12.28 bytes.Reader:字节切片的随机读取

bytes.Reader 是一个从字节切片读取数据的类型,支持随机访问(seek),这是它和 bytes.Buffer 的最大区别。

创建 Reader

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

import (
    "bytes"
    "fmt"
)

func main() {
    data := []byte("hello world")
    reader := bytes.NewReader(data)

    fmt.Printf("类型: %T\n", reader)
    fmt.Printf("长度: %d\n", reader.Len())
    fmt.Printf("大小: %d\n", reader.Size())
}
类型: *bytes.Reader
长度: 11
大小: 11

读取操作

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

import (
    "bytes"
    "fmt"
)

func main() {
    reader := bytes.NewReader([]byte("hello"))

    // 读取所有
    data := make([]byte, 5)
    n, _ := reader.Read(data)
    fmt.Printf("读取了 %d 字节: %q\n", n, string(data))
}
读取了 5 字节: "hello"

与 Buffer 的区别Buffer 是读写混合的缓冲区,Reader 是只读的随机访问。


12.29 bytes.Reader.ReadAt:指定位置读取

ReadAt 从指定的绝对位置读取数据,不影响读取位置指针。

 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
package main

import (
    "bytes"
    "fmt"
)

func main() {
    reader := bytes.NewReader([]byte("hello world"))

    // 从位置 0 读取 5 字节
    b1 := make([]byte, 5)
    n1, _ := reader.ReadAt(b1, 0)
    fmt.Printf("ReadAt(0,5): %q\n", string(b1[:n1]))

    // 再次从位置 0 读取(位置指针不变,所以还是读到相同内容)
    b2 := make([]byte, 5)
    n2, _ := reader.ReadAt(b2, 0)
    fmt.Printf("ReadAt(0,5) 再次: %q\n", string(b2[:n2]))

    // 从位置 6 读取
    b3 := make([]byte, 5)
    n3, _ := reader.ReadAt(b3, 6)
    fmt.Printf("ReadAt(6,5): %q\n", string(b3[:n3]))
}
ReadAt(0,5): "hello"
ReadAt(0,5) 再次: "hello"
ReadAt(6,5): "world"

关键点ReadAt绝对定位,读取位置由参数指定,不依赖也不改变内部的位置指针。


12.30 bytes.Reader.Seek:跳转读写位置

Seek 改变下一次读取的位置,可以从头跳、从尾跳、或相对当前位置跳。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
)

func main() {
    reader := bytes.NewReader([]byte("hello world"))

    // 跳过前 6 个字节(跳过 "hello ")
    pos, _ := reader.Seek(6, 0) // 0 = io.SeekStart,从起始位置
    fmt.Printf("跳到位置 %d\n", pos)

    // 现在读取应该从 "world" 开始
    b := make([]byte, 5)
    n, _ := reader.Read(b)
    fmt.Printf("读取: %q\n", string(b[:n]))
}
跳到位置 6
读取: "world"

Seek 的三种模式

常量含义
io.SeekStart0从起始位置(position 0)计算
io.SeekCurrent1从当前位置计算
io.SeekEnd2从末尾位置计算
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
    "io"
)

func main() {
    reader := bytes.NewReader([]byte("hello"))

    // 从末尾往前跳 2 字节
    pos, _ := reader.Seek(-2, io.SeekEnd)
    fmt.Printf("跳到末尾-2: 位置 %d\n", pos)

    b := make([]byte, 2)
    n, _ := reader.Read(b)
    fmt.Printf("读取: %q\n", string(b[:n]))
}
跳到末尾-2: 位置 3
读取: "lo"

12.31 bytes.Reader.Size:原始字节切片长度

Size 返回底层原始字节切片的长度,永远不会变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "bytes"
    "fmt"
)

func main() {
    reader := bytes.NewReader([]byte("hello world"))

    fmt.Printf("Size: %d\n", reader.Size()) // 11
    fmt.Printf("Len: %d\n", reader.Len())   // 11

    // 读取一些数据后
    reader.Read(make([]byte, 5))

    fmt.Printf("读取后 Size: %d\n", reader.Size()) // 仍然是 11
    fmt.Printf("读取后 Len: %d\n", reader.Len())  // 变成 6
}
Size: 11
Len: 11
读取后 Size: 11
读取后 Len: 6

关键区别

  • Size() = 底层原始数据的长度(不变)
  • Len() = 剩余未读取的字节数(随读取而减少)

本章小结

核心概念

概念说明
[]byte字节切片,Go 中表示二进制数据的核心类型,可变
bytes.Buffer动态字节缓冲区,可读写,支持自动扩容
bytes.Reader字节切片的随机读取器,支持 Seek
runeUnicode 码点,等价于 int32

函数速查表

比较与查找

函数说明
bytes.Equal(a, b)精确比较
bytes.EqualFold(a, b)忽略大小写比较
bytes.Compare(a, b)比较大小,返回 0/1/-1
bytes.Index(b, sub)查找子切片首次位置
bytes.IndexByte(b, c)查找单字节首次位置
bytes.LastIndex(b, sub)查找子切片最后位置
bytes.Contains(b, sub)判断是否包含子切片
bytes.Count(b, sub)统计子串出现次数

分割与修整

函数说明
bytes.Split(b, sep)分割,不保留分隔符
bytes.SplitN(b, sep, n)限制分割次数
bytes.Fields(b)按空白分割
bytes.Trim(b, cutset)去除两端字符
bytes.TrimSpace(b)去除空白字符
bytes.TrimPrefix(b, prefix)去除前缀
bytes.TrimSuffix(b, suffix)去除后缀

转换与映射

函数说明
bytes.Map(f, b)对每个字节应用映射函数
bytes.Replace(b, old, new, n)替换(指定次数)
bytes.ReplaceAll(b, old, new)替换所有
bytes.Repeat(b, n)重复字节切片
bytes.Runes(b)字节切片转 rune 切片

Buffer 操作

方法说明
Buffer.WriteByte(c)写入单字节
Buffer.WriteString(s)写入字符串
Buffer.Read(p)读取到目标切片
Buffer.ReadByte()读取单字节
Buffer.Next(n)读取下 n 字节(推进位置)
Buffer.Peek(n)偷看下 n 字节(不推进)
Buffer.String()转字符串
Buffer.Bytes()转字节切片
Buffer.Len()已用长度
Buffer.Cap()总容量
Buffer.Grow(n)预分配容量
Buffer.Reset()重置缓冲区
Buffer.Truncate(n)截断到 n 字节

Reader 操作

方法说明
Reader.Read(p)顺序读取
Reader.ReadAt(p, off)指定位置读取
Reader.Seek(off, whence)跳转读写位置
Reader.Size()原始字节切片长度

性能小贴士

  1. bytes.Equalbytes.Compare == 0 更快:底层使用汇编优化的 memcmp
  2. Buffer.Grow 避免频繁扩容:当你知道要写入多少数据时
  3. Buffer.NextBuffer.Peek 避免拷贝:直接返回底层数组视图
  4. Reader 适合随机访问:需要频繁 seek 的场景用 Reader,流式读写用 Buffer

幽默总结

bytes 包就像一把瑞士军刀——看似简单,功能齐全。网络协议、二进制文件、加密数据,它都能搞定。而 bytes.Buffer 就是你的私人厨房,想做什么菜(字节数据)都可以往里扔。

“Go 语言的 bytes 包,是每个 Gopher 必须精通的内功。” ——《Go 语言武林外传·第十二卷》

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)