登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go bytes.Reader.Seek 怎样回退到可重复读取位置

来源:17golang原创

时间:2026-10-26 16:17:49 372浏览 收藏

在解析同一段内存数据时,经常会遇到“先读一遍判断格式,再从头读一遍”的需求。bytes.Reader 已经读过一部分内容后,不需要重新复制字节切片,调用 Seek 就能把当前位置移回开头,或者回到之前保存的偏移。最常用的回退写法是 r.Seek(0, io.SeekStart)。

官方地址:https://pkg.go.dev/bytes#Reader.Seek

Seek 调整的是 bytes.Reader 的读取位置。把目标位置设为 0 并使用 io.SeekStart,就能让后续 Read 从原始数据开头重新开始;它不会修改底层的 []byte。

一、先看清 Reader 的当前位置

bytes.NewReader 接收一个字节切片并返回实现了 io.Reader、io.ReaderAt 和 io.Seeker 的 Reader。每次调用 Read,当前位置都会向后移动;下一次读取从新的位置继续。

package main

import (
	"bytes"
	"fmt"
)

func main() {
	r := bytes.NewReader([]byte("abcdef"))
	buf := make([]byte, 2)
	// Read 消费前两个字节,Reader 的当前位置随之向后移动。
	n, err := r.Read(buf)
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}
	// Len 返回当前位置之后仍可读取的字节数。
	fmt.Printf("读取=%q 字节数=%d 剩余=%d\\n", buf[:n], n, r.Len())
}

这里的 r.Len() 反映“未读部分”的长度,而不是原始切片总长度。要知道原始数据的固定长度,可以使用 r.Size();如果只是想重复读取,应该操作 Reader 的位置,不要把剩余切片当成新的输入。

原始字节切片、bytes.Reader、当前位置和 Read 返回区间的关系说明图
图1:bytes.Reader 的数据区间与当前位置关系说明图,不是运行截图。

二、用 Seek 回到开头再读一次

回到开头的关键是两个参数:偏移量传 0,基准传 io.SeekStart。返回值是新的绝对位置,第二个返回值必须检查;只有没有错误时,后续读取才是在预期位置开始。

package main

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

func main() {
	r := bytes.NewReader([]byte("header|payload"))
	first := make([]byte, 6)
	// 第一次读取只消费 header,留下后续内容。
	_, _ = r.Read(first)

	position, err := r.Seek(0, io.SeekStart)
	if err != nil {
		fmt.Println("回到开头失败:", err)
		return
	}
	// SeekStart 以数据开头为基准,返回值应为 0。
	if position != 0 {
		fmt.Println("位置异常:", position)
		return
	}
	all := make([]byte, 14)
	n, err := r.Read(all)
	if err != nil && err != io.EOF {
		fmt.Println("重复读取失败:", err)
		return
	}
	fmt.Printf("重复读取=%q\\n", all[:n])
}

这里第二次 Read 会从 h 开始,而不是从第一次读取结束的位置继续。Seek 不会清空或重建 Reader,已读的数据仍然由同一个底层字节切片提供。

三、三种 whence 怎样计算目标位置

Seek(offset, whence) 的 whence 决定偏移量从哪里算:io.SeekStart 从开头算,io.SeekCurrent 从当前位置算,io.SeekEnd 从数据末尾算。需要回退时,使用负偏移配合当前位置基准即可。

写法目标位置适用场景
Seek(0, io.SeekStart)0完整重读
Seek(-n, io.SeekCurrent)当前位置减 n撤回刚消费的前 n 个字节
Seek(-n, io.SeekEnd)总长度减 n从末尾回看一段内容
package main

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

func main() {
	r := bytes.NewReader([]byte("0123456789"))
	buf := make([]byte, 4)
	_, _ = r.Read(buf)

	// 保存当前绝对位置,之后可以精确回到这个位置。
	saved, err := r.Seek(0, io.SeekCurrent)
	if err != nil {
		fmt.Println("保存位置失败:", err)
		return
	}
	// 从当前位置回退两个字节,目标位置是 saved-2。
	position, err := r.Seek(-2, io.SeekCurrent)
	if err != nil {
		fmt.Println("相对回退失败:", err)
		return
	}
	part := make([]byte, 3)
	n, _ := r.Read(part)
	fmt.Printf("当前位置=%d 读取=%q\\n", position, part[:n])

	// 用保存的绝对位置恢复,而不是根据业务字符串长度猜位置。
	_, err = r.Seek(saved, io.SeekStart)
	if err != nil {
		fmt.Println("恢复位置失败:", err)
	}
}

保存位置时,Seek(0, io.SeekCurrent) 返回的是当前位置。恢复时把这个值作为 io.SeekStart 的绝对偏移,逻辑比“再向前移动多少字节”更稳定。

io.SeekStart、io.SeekCurrent、io.SeekEnd 和 Reader 位置边界的关系说明图
图2:Seek 三种基准与位置边界的静态说明图,不是运行截图。

四、负位置、越过末尾和非法 whence

Seek 的返回值不能只看新位置。目标位置小于 0 时会返回错误,Reader 的当前位置不会被设置成负数;传入未知的 whence 也会报错。相反,目标位置超过数据末尾并不等同于错误,后续 Read 会读不到数据,直到再次 Seek 回合法内容范围。

package main

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

func trySeek(r *bytes.Reader, offset int64, whence int) {
	position, err := r.Seek(offset, whence)
	// 同时打印位置和错误,避免把非法定位误当成成功。
	fmt.Printf("position=%d err=%v remaining=%d\\n", position, err, r.Len())
}

func main() {
	r := bytes.NewReader([]byte("abc"))
	// 负目标位置会失败,不应继续使用失败返回的 position。
	trySeek(r, -1, io.SeekStart)
	// 超过末尾可以定位,但当前位置之后没有可读字节。
	trySeek(r, 10, io.SeekStart)
	// 非法 whence 会失败,调用方应立即处理 error。
	trySeek(r, 0, 99)
}

工程代码里建议遵循一个简单规则:每次 Seek 都检查 error,只有成功时才把返回的偏移交给后续逻辑。若允许定位到末尾之外,也要把“后续读取得到 EOF”作为业务上的预期分支写清楚。

五、整理成可重复读取的小函数

如果一个流程需要“读取前几字节判断格式,再从头交给解析器”,可以把回退动作集中在函数边界里。这样调用方只关心返回内容和错误,不会遗漏位置恢复。

package main

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

func readTwice(data []byte) (string, string, error) {
	r := bytes.NewReader(data)
	probe := make([]byte, 4)
	// 先读取探测区,真实解析仍然要从原始开头开始。
	if _, err := r.Read(probe); err != nil && err != io.EOF {
		return "", "", err
	}
	// 明确回到 0,避免依赖探测区实际读了多少字节。
	if _, err := r.Seek(0, io.SeekStart); err != nil {
		return "", "", err
	}
	all, err := io.ReadAll(r)
	if err != nil {
		return "", "", err
	}
	return string(probe), string(all), nil
}

func main() {
	probe, all, err := readTwice([]byte("Go reader"))
	// 统一处理函数错误,避免把不完整结果当成成功。
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}
	fmt.Printf("探测=%q 完整=%q\\n", probe, all)
}

这个模板的关键不是“必须读取两次”,而是把位置当作显式状态管理:探测完成后回到固定基准,读取失败就停止传播。对于同一个 Reader 的多个阶段读取,保存绝对位置再恢复也遵循同一原则。

六、最后记住这几个判断

  • 完整重读使用 Seek(0, io.SeekStart),它不会修改原始字节切片。
  • 保存和恢复阶段位置时,用 Seek(0, io.SeekCurrent) 取绝对偏移,再用 io.SeekStart 恢复。
  • io.SeekCurrent 适合相对回退,io.SeekEnd 适合从末尾定位。
  • 负目标位置和非法 whence 要看 error;越过末尾可以定位,但后续读取通常得到 EOF。
  • 每次成功 Seek 后再使用返回的 offset,避免错误位置进入后续解析流程。

相关问题

  • bytes.Reader 的 Len 和 Size 有什么区别?
  • 为什么 Seek 越过末尾没有立即返回错误?
  • 什么时候应该保存当前位置,而不是直接回到 0?
  • ReadAt 和移动 Reader 当前位置的读取方式有什么区别?
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>