Golang中io包的ErrShortWrite

先说结论,在Golang中遇到io.ErrShortWrite错误时,也就是short write时,说明你写入的数据大小要比期望的要小,一般是结合bufio包一起使用时会碰到这个问题

原始代码不方便贴出来,这里给出一个简单的复现代码

 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
29
30
31
32
33
package bufio_short_write

import (
	"bufio"
	"os"
	"testing"
	
	"github.com/stretchr/testify/require"
	"golang.org/x/sync/errgroup"
)

func fooWrite(w *bufio.Writer, content []byte) error {
	_, err := w.Write(content)
	if err != nil {
		return err
	}
	err = w.Flush()
	return err
}

func TestReCurrentIOShortWrite(t *testing.T) {
	var eg errgroup.Group
	write := bufio.NewWriter(os.Stderr)
	for i := 0; i < 1<<10; i++ {

		eg.Go(func() error {
			return fooWrite(write, []byte("hello world\n"))
		})
	}
	err := eg.Wait()
	require.Error(t, err)
	require.EqualError(t, err, "short write")
}

就有一个全局的临界区资源write,对他进行并发写入没有同步机制,然后就会出现short write的错误,而这就是io.ErrShortWrite。为什么会出现了? 我们可以看看标准库中的bufioWriterWriteFlush方法的实现

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// bufio.go Writer部分代码

// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.

type Writer struct {
	err error     // 内部error,如果该error不为nil,后续的Write,Flush都会返回该error
	buf []byte    // 内部缓存
	n   int       // 缓存了多少字节的数据
	wr  io.Writer // 底层的io.Writer
}

// Write writes the contents of p into the buffer.
// It returns the number of bytes written.
// If nn < len(p), it also returns an error explaining
// why the write is short.
func (b *Writer) Write(p []byte) (nn int, err error) {
	// 内部的buffer不够容纳p,因此需要"多次"处理
	for len(p) > b.Available() && b.err == nil {
		var n int
		if b.Buffered() == 0 {
			// Large write, empty buffer.
			// Write directly from p to avoid copy.
			// 内部的buffer没有数据,直接将p的数据写入到底层的io.Writer中
			n, b.err = b.wr.Write(p)
		} else {
			// 注意copy(dst, src)最多拷贝min(len(dst),len(src))数据
			// 内部的buffer有数据,先将p的一部分数据拷贝到内部的buffer中。
			n = copy(b.buf[b.n:], p)
			b.n += n
			// 将内部的buffer的数据写入到底层的io.Writer中,最终是期望b.n = 0(也就是b.Buffered() = 0),
			// 因此这个for循环最多会执行两次(第一次走else分支,将内部的buffer充满,然后flush。第二次走if分支,将p中剩余的数据写入到底层的io.Writer中
			b.Flush()
		}
		nn += n
		// 从p中去掉已经处理的数据
		p = p[n:]
	}
	// Write和Flush都可能会出错,因此需要判断b.err是否为nil,不为nil就返回,这是基本保障
	if b.err != nil {
		return nn, b.err
	}
	// 说明内部的buffer子够容纳p的数据,直接将p的数据拷贝到内部的buffer中
	// 如果是进入过for循环的,那么走到这里时p的长度一定是0
	n := copy(b.buf[b.n:], p)
	b.n += n
	nn += n
	return nn, nil
}


// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
	if b.err != nil {
		// 基本的保证,如果b.err不为nil,后续的Write,Flush都会返回该error
		return b.err
	}
	if b.n == 0 {
		// 没有缓存的数据,直接返回
		return nil
	}
	// 将缓存的数据写入到底层的io.Writer
	n, err := b.wr.Write(b.buf[0:b.n])
	if n < b.n && err == nil {
		err = io.ErrShortWrite
	}
	if err != nil {
		if n > 0 && n < b.n {
			copy(b.buf[0:b.n-n], b.buf[n:b.n])
		}
		b.n -= n
		b.err = err
		return err
	}
	b.n = 0
	return nil
}

在代码中,当执行Flush操作时,可能会出现报错的情况。这是因为Flush时写往底层的Writer的数据少于bufio.Writer缓存的。

具体来说,考虑这样一个场景:fooWrite1fooWrite2都传递参数"hello world\n"fooWrite1调用Flush时,执行到n, err := b.wr.Write(b.buf[0:b.n])时,b.n=12。然而,在b.wr.Write调用时, fooWrite2调用了Write,导致b.n更新为24。因此,在fooWrite1b.wr.Write调用完后,此时n=12,b.n=24,就会报io.ErrShortWrite, 说明内部的缓存并没有被完全写往底层的Writer。那这里不报错行不行?答案是不行!因为Flush最后都会将b.n置为0,这就会导致fooWrite2写的”hello world\n”永远写不出去。

为了解决这个问题,最简单的方法是用互斥锁保护WriteFlush操作。不过,如果只是为了保证数据的一致性,直接使用底层的io.Writer也是可以的,不必使用bufio.Writer。因为每次都执行Write再接着Flush也不是标准库bufio作者的初衷。