目录
现象
读取文件或者网络请求时,我们经常会遇到ioutil.ReadAll方法,但是这个方法虽然方便有时候却会导致一些性能问题。
我们往一个名为”test“的测试文件里简单写入两行字符串:
test
test
然后用我们熟知的ioutil.ReadAll来读取文件:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
file, err := os.Open("test")
checkErr(err)
b, err := ioutil.ReadAll(file)
checkErr(err)
fmt.Println(string(b))
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
输出:
test
test
符合我们的预期,这里我们在最后加两行输出:
fmt.Println(len(b))
fmt.Println(cap(b))
再看看输出:可以看到len只有9但是cap却有1536,我们只读取很少的内容却使用了这么多的内存,这在平时不会有问题,但是比如在网络应用当有大量请求过来时就容易导致内存严重浪费,严重时还会内存泄漏。
test
test
9
1536
溯源
我们来看看它底层到底如何读取的:ReadAll调用了内部方法readAll
func ReadAll(r io.Reader) ([]byte, error) {
return readAll(r, bytes.MinRead)
}
调用buf.Grow给buf申请内存空间,capacity=512为const,如果buf过大会导致ErrTooLarge异常
// readAll reads from r until an error or EOF and returns the data it read
// from the internal buffer allocated with a specified capacity.
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
var buf bytes.Buffer //
// If the buffer overflows, we will get bytes.ErrTooLarge.
// Return that as an error. Any other panic remains.
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
}
}()
if int64(int(capacity)) == capacity {
buf.Grow(int(capacity)) //这里
}
_, err = buf.ReadFrom(r)
return buf.Bytes(), err
}
func (b *Buffer) grow(n int) int {
m := b.Len()
// If buffer is empty, reset to recover space.
if m == 0 && b.off != 0 {
b.Reset()
}
// Try to grow by means of a reslice.
if i, ok := b.tryGrowByReslice(n); ok {
return i
}
// Check if we can make use of bootstrap array.
if b.buf == nil && n <= len(b.bootstrap) { //申请内存空间
b.buf = b.bootstrap[:n]
return 0
}
c := cap(b.buf)
if n <= c/2-m {// 如果n(512)<cap(b.buf)/2-len(b.buf)那就copy
// We can slide things down instead of allocating a new
// slice. We only need m+n <= c to slide, but
// we instead let capacity get twice as large so we
// don't spend all our time copying.
copy(b.buf, b.buf[b.off:])
} else if c > maxInt-c-n {
panic(ErrTooLarge) // 这里
} else { //如果空间不够,就申请两倍的cap(b.buf)。我们上面代码会走到这个逻辑
// Not enough space anywhere, we need to allocate.
buf := makeSlice(2*c + n)
copy(buf, b.buf[b.off:])
b.buf = buf
}
// Restore b.off and len(b.buf).
b.off = 0
b.buf = b.buf[:m+n]
return m
}
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
if l := len(b.buf); n <= cap(b.buf)-l { // 这里有个判断条件判断是否n(512)<cap(b.buf)-len(b.buf)
b.buf = b.buf[:l+n]
return l, true
}
return 0, false
}
申请完了512byte内存空间会调用buf.ReadFrom读取。
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
b.lastRead = opInvalid
for {
i := b.grow(MinRead)//这里还会尝试申请内存,每次都会判断是否需要申请内存
b.buf = b.buf[:i]
m, e := r.Read(b.buf[i:cap(b.buf)])
if m < 0 {
panic(errNegativeRead)
}
b.buf = b.buf[:i+m]
n += int64(m)
if e == io.EOF {
return n, nil // e is EOF, so return nil explicitly
}
if e != nil {
return n, e
}
}
}
我们看到了,如果用ioutil.ReadAll来读取即使只有1byte也会申请512byte,如果数据量大的话浪费的更多每次都会申请512+2*cap(b.buf),假如我们有1025byte那么它就会申请3584byte,浪费了2倍还多。
这在比如http大量请求时轻则导致内存浪费严重,重则导致内存泄漏影响业务。
解决办法bufio.Reader.Read
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("test")
checkErr(err)
data := make([]byte, 1025)
r := bufio.NewReader(file)
r.Read(data)
fmt.Println(len(data))
fmt.Println(cap(data))
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
1025
1025
缺点是得自己估量数据大小。