Golang中bufio.NewReaderSize设置1M大小会导致内存损坏,除非同时调用scanner.Buffer

Golang中bufio.NewReaderSize设置1M大小会导致内存损坏,除非同时调用scanner.Buffer 我在使用以下等效代码时遇到了内存损坏问题:

	file, err := os.Open(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	fp := newFileParser()
	reader := bufio.NewReaderSize(file, 1024*1024)
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		line := scanner.Bytes()
		fp.parseLine(line)
	}

当我为扫描器创建了适当大小的缓冲区后,内存损坏问题就解决了:

	fp := newFileParser()
	const maxCapacity = 1024 * 1024
	buf := make([]byte, maxCapacity)
	reader := bufio.NewReaderSize(file, maxCapacity)
	scanner := bufio.NewScanner(reader)
	scanner.Buffer(buf, maxCapacity)
	for scanner.Scan() {
		line := scanner.Bytes()
		fp.parseLine(line)
	}

我发现默认缓冲区大小是64k。但我期望扫描器能够发现可用的缓冲区大小,例如应该panic而不是静默地损坏内存(用从文件读取的数据内容覆盖数据结构)——我的期望是否不合理?

致意 Robert


更多关于Golang中bufio.NewReaderSize设置1M大小会导致内存损坏,除非同时调用scanner.Buffer的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

感谢Johan - 就是那个愚蠢的问题!问题已解决。

更多关于Golang中bufio.NewReaderSize设置1M大小会导致内存损坏,除非同时调用scanner.Buffer的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


抱歉没有理解您的意思。您是遇到了Go错误信息还是数据不一致的问题?

在函数中

func (block *Block) addLine(line []byte, lineNo int64) {

您需要在追加行之前复制该行。否则在下一次读取时它将被新数据覆盖。

创建一个具有相同长度的新切片并使用复制命令。参见 https://golang.org/ref/spec#Appending_and_copying_slices

诚挚问候

行为与文档一致,文档在此处 https://golang.org/pkg/bufio/#pkg-variables 说明扫描器会无错误停止。还有一个关于 bufio 停止方式的相关问题 https://github.com/golang/go/issues/26431

也许我漏掉了什么 - 扫描器无论是否出错都没有停止,它只是继续运行但却覆盖了内存。

这让我心里有些担忧,如果在C/C++中出现这种情况我不会感到惊讶,但在Go中确实有点意外!

调试这个问题时,在某个特定文件上我追踪到内存损坏总是在扫描器循环固定次数后可靠地发生。

scanner.Scan()

Robaato:

它继续运行但覆盖了内存。

我很确定这种情况不应该发生。你能提供一个可复现的测试案例吗?

"等效代码"并不理想。细节决定成败——在你最初粘贴的代码中,我会仔细检查 fp.parseLine 具体做了什么。(也就是说,请确认你的操作符合文档中关于 scanner.Bytes 返回值的说明。)

func main() {
    fmt.Println("hello world")
}

Robaato:

scanner := bufio.NewScanner(reader)
scanner.Buffer(buf, maxCapacity)

这里的情况没有任何问题。在bufio的源代码中提到:

MaxScanTokenSize 是用于缓冲令牌的最大大小,除非用户使用Scan.Buffer提供显式缓冲区。实际的最大令牌大小可能更小,因为缓冲区可能需要包含例如换行符。

MaxScanTokenSize = 64 * 1024

你通过使用scanner.Buffer(buf, maxCapacity)扩展初始缓冲区做得非常好。

当然可以尝试一下。当前完整可运行的版本(这是一个匹配开始和结束记录的日志解析器 - 仍在开发中):

/*
Package p4dlog parses Perforce text logs (not structured logs).

It assumes you have set configurable server=3 (or greater)
You may also have decided to set track=1 to get more detailed usage of
access to different tables.

See p4dlog_test.go for examples of log entries.

*/
package p4dlog

import (
	"bufio"
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"

也许我还在做其他愚蠢的事情(第一个真正的Go程序)。需要清理日志文件。

在Go语言中,bufio.Scanner的默认缓冲区大小确实是64KB。当处理超过默认缓冲区大小的行时,如果没有显式设置缓冲区,会导致内存损坏。这是因为Scanner在内部使用一个固定大小的缓冲区来读取数据,当行长度超过缓冲区容量时,会重新分配更大的缓冲区,但这个过程可能不会正确处理所有边界情况。

你的观察是正确的:当使用bufio.NewReaderSize设置1MB的读取器缓冲区时,这并不会自动影响Scanner的缓冲区大小。Scanner有自己的内部缓冲区管理机制,与底层的Reader缓冲区是独立的。

以下是问题的技术分析:

  1. bufio.NewReaderSize只影响读取器的缓冲区大小,用于批量读取操作
  2. bufio.Scanner使用自己的内部缓冲区,默认64KB
  3. 当行长度超过当前缓冲区大小时,Scanner会尝试扩容,但这个过程可能导致内存问题

你的解决方案是正确的做法:显式设置Scanner的缓冲区大小和容量。

file, err := os.Open("largefile.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

const maxCapacity = 1024 * 1024
buf := make([]byte, maxCapacity)

reader := bufio.NewReaderSize(file, maxCapacity)
scanner := bufio.NewScanner(reader)
scanner.Buffer(buf, maxCapacity)

for scanner.Scan() {
    line := scanner.Bytes()
    // 处理行数据
    processLine(line)
}

if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

关于你的期望:确实,当行长度超过缓冲区时,Scanner应该返回错误而不是静默损坏内存。在实际使用中,如果遇到超长行,scanner.Scan()会返回false,并且scanner.Err()会返回bufio.ErrTooLong错误。

if err := scanner.Err(); err != nil {
    if err == bufio.ErrTooLong {
        log.Printf("行长度超过缓冲区大小: %v", err)
    } else {
        log.Fatal(err)
    }
}

这种设计确实可能导致困惑,因为内存损坏发生在错误检查之前。因此,在处理可能包含长行的文件时,总是应该预先设置足够大的缓冲区。

回到顶部