0
点赞
收藏
分享

微信扫一扫

为什么要避免在 Go 中使用 io.ReadAll

ioutil包在go1.16版本已弃用。

io.ReadAll()实现:

// src/io/io.go

func ReadAll(r Reader) ([]byte, error) {
    // 创建一个 512 字节的 buf
	b := make([]byte, 0, 512)
	for {
		if len(b) == cap(b) {
			// 如果 buf 满了,则追加一个元素,使其重新分配内存
			b = append(b, 0)[:len(b)]
		}
		// 读取内容到 buf
		n, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+n]
		// 遇到结尾或者报错则返回
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}
	}
}

我给代码加上了必要的注释,这段代码的执行主要分三个步骤:

  1. 创建一个 512 字节的 buf
  2. 不断读取内容到 buf,当 buf 满的时候,会追加一个元素,促使其重新分配内存;
  3. 直到结尾或报错,则返回;

知道了执行步骤,但想要分析其性能问题,还需要了解 Go 切片的扩容策略,如下:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

也就是说,如果待拷贝数据的容量小于 512 字节的话,性能不受影响。但如果超过 512 字节,就会开始切片扩容。数据量越大,扩容越频繁,性能受影响越大。

如果数据量足够大的话,内存可能就直接撑爆了,这样的话影响就大了。

那有更好的替换方案吗?当然是有的,我们接着往下看。

io.Copy

可以使用 io.Copy 函数来代替

对比之后就会发现,io.Copy 函数不会一次性读取全部数据,也不会频繁进行切片扩容,显然在数据量大时是更好的选择。

实现方式:

package main
import (
	"fmt"
	"io"
	"net/http"
	"os"
)
func main() {
	fmt.Println("嗨客网()")
	for _, url := range os.Args[1:] {
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		b, err := io.Copy(os.Stdout, resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("%s", b)
	}
}

函数 调用 io.Copy(dst, src) 会从 src 中读取内容,并将读到的结果写入到 dst 中,使用这个函数替代掉例子中的 ioutil.ReadAll 来拷贝响应 结构体 到 os.Stdout,避免申请一个缓冲区(例子中的 b)来存储。记得处理 io.Copy 返回结果中的错误。

举报

相关推荐

0 条评论