Golang中gzip.Reader与bytes.Reader在读取末尾字节时的差异行为解析

Golang中gzip.Reader与bytes.Reader在读取末尾字节时的差异行为解析 我发现,对于两种读取器:gzip.Readerbytes.Reader,它们在读取最后一个字节时的行为有所不同,也许我应该在 GitHub 上创建一个问题?

详情如下

对于 gzip.Reader,读取最后一个字节时返回 1 和一个 EOF 错误,但对于 bytes.Reader,它返回 1 和 nil(无错误)。我写了以下代码:Go Playground - The Go Programming Language

一个像这样的函数

func compareReader(r, b io.Reader) error {
	var bufa [1]byte
	var bufb [1]byte
	for {

		na, erra := r.Read(bufa[:])
		nb, errb := b.Read(bufb[:])

		if erra == nil && errb == nil && na == nb && bufa[0] == bufb[0] {
			continue
		}
		if erra == errb && erra == io.EOF {
			return nil
		}
		if erra != nil {
			if erra == io.EOF && errb != io.EOF {
				return fmt.Errorf("reader b has more data than a")
			}
			return fmt.Errorf("read on a error: %s", erra)
		}
		if errb != nil {
			if errb == io.EOF && erra != io.EOF {
				return fmt.Errorf("reader a has more data than b")
			}
			return fmt.Errorf("read on b error: %s", erra)
		}
		return nil
	}
}

我不关心任何诸如 POSIX、Linux 手册页或其他任何东西,我只是认为所有标准库都应该按照统一的标准来处理。

并且因为调用者只知道这是一个 io.Reader,他并不关心这个读取器的底层实现。


更多关于Golang中gzip.Reader与bytes.Reader在读取末尾字节时的差异行为解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您如此详尽的解答,阅读后,我成功理解了!

更多关于Golang中gzip.Reader与bytes.Reader在读取末尾字节时的差异行为解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


io.Reader 的文档指出,你所看到的行为是一些实现中的预期行为。具体来说,第三段提到:

当 Read 在成功读取 n > 0 个字节后遇到错误或文件结束条件时,它会返回已读取的字节数。它可能从同一次调用中返回(非 nil 的)错误,也可能在后续调用中返回错误(并且 n == 0)。这种一般情况的一个实例是,一个 Reader 在输入流末尾返回非零字节数时,可能返回 err == EOF 或 err == nil。下一次 Read 应该返回 0, EOF。

尽管可能不关心 POSIX 或其他任何操作系统接口标准,但这并不能改变运行你 Go 代码的底层操作系统确实遵循 POSIX 或其他(可能是内部的/专有的)标准这一事实,并且从文件、网络套接字等进行读取的底层操作系统级调用,可能会读取到文件末尾而不返回 EOF。例如,如果你正在从 TCP 套接字读取数据,你可能直到连接关闭才知道已经到达文件末尾。你的第一次读取可能读取了 n 个字节,然后你坐着等待,直到连接关闭,此时你再读取 0 个额外的字节并只得到一个 EOF。

或者,在读取文件时,同时返回文件的最后一个数据块和一个 EOF 可能是有利的,而不是一次调用读取最后一个数据块且没有错误,另一次调用读取 0 个字节并得到一个 EOF,因为这节省了一次向操作系统获取 EOF 的上下文切换。

所以,长话短说: 向 Go 团队提交问题不会解决你的问题。我建议改为:

  1. 修改你的代码,使其能处理读取零字节和非零字节时的 EOF 条件。
  2. 使用或创建一个抽象层,将一种场景转换为另一种:

有另一个名为 io.ByteReader 的标准接口,用于一次读取单个字节。你可以检查 br 是否实现了该接口,如果没有,则将它们包装进去:

b2, ok := b.(io.ByteReader)
if !ok {
    b2 = bufio.NewReader(b)
}

// 对 r 进行同样的处理

// 原来的:
//	na, erra := r.Read(bufa[:])
//	nb, errb := b.Read(bufb[:])

// 改为:
ba, erra := r2.ReadByte()
bb, errb := b2.ReadByte()

此接口的实现不会混合成功读取字节与错误;如果有错误,则没有读取任何字节;如果读取了任何字节,则没有错误。

这是标准库中不同读取器实现的正常行为差异,不需要在GitHub上创建问题。gzip.Reader遵循"读取到末尾时返回数据和EOF"的模式,而bytes.Reader采用"先返回数据,下次读取再返回EOF"的模式。两种模式在Go标准库中都存在,都是符合io.Reader接口规范的。

根据io.Reader接口文档:

  • 读取可能返回n > 0的同时返回非nil错误(包括io.EOF
  • 调用者应该处理返回的n > 0的数据,然后再处理错误

你的比较函数需要调整以正确处理这种情况:

func compareReader(r, b io.Reader) error {
    var bufa [1]byte
    var bufb [1]byte
    
    for {
        na, erra := r.Read(bufa[:])
        nb, errb := b.Read(bufb[:])
        
        // 先比较读取到的数据
        if na != nb {
            return fmt.Errorf("byte count mismatch: a=%d, b=%d", na, nb)
        }
        
        if na > 0 && bufa[0] != bufb[0] {
            return fmt.Errorf("data mismatch at position")
        }
        
        // 处理错误状态
        if erra != nil || errb != nil {
            // 如果都返回EOF,比较完成
            if erra == io.EOF && errb == io.EOF {
                return nil
            }
            
            // 如果一个返回EOF而另一个没有
            if (erra == io.EOF && errb == nil) || (erra == nil && errb == io.EOF) {
                // 检查是否还有数据要读取
                if na == 0 && nb == 0 {
                    return fmt.Errorf("one reader reached EOF before the other")
                }
                // 如果还有数据,继续下一次读取
                if erra == io.EOF && na == 0 {
                    return fmt.Errorf("reader a reached EOF but b has more data")
                }
                if errb == io.EOF && nb == 0 {
                    return fmt.Errorf("reader b reached EOF but a has more data")
                }
            }
            
            // 其他错误
            if erra != nil && erra != io.EOF {
                return fmt.Errorf("read on a error: %w", erra)
            }
            if errb != nil && errb != io.EOF {
                return fmt.Errorf("read on b error: %w", errb)
            }
        }
        
        // 如果都读取了0字节且没有错误,继续
        if na == 0 && nb == 0 && erra == nil && errb == nil {
            continue
        }
    }
}

更简洁的实现方式是使用io.ReadFull或缓冲读取:

func compareReaderBuffered(r1, r2 io.Reader) error {
    buf1 := make([]byte, 4096)
    buf2 := make([]byte, 4096)
    
    for {
        n1, err1 := r1.Read(buf1)
        n2, err2 := r2.Read(buf2)
        
        if n1 != n2 {
            return fmt.Errorf("byte count mismatch: %d vs %d", n1, n2)
        }
        
        if !bytes.Equal(buf1[:n1], buf2[:n2]) {
            return fmt.Errorf("data mismatch")
        }
        
        if err1 != err2 {
            // 处理不同的EOF行为
            if (err1 == io.EOF && n1 == 0) || (err2 == io.EOF && n2 == 0) {
                return fmt.Errorf("different EOF timing")
            }
        }
        
        if err1 == io.EOF || err2 == io.EOF {
            return nil
        }
    }
}

关键点是:io.Reader接口允许实现在最后一次成功读取时返回数据和EOF,这是完全有效的实现。调用者应该按照"先处理数据,再处理错误"的原则来编写代码。

回到顶部