如果需要更多字节,则扩展临时切片

Expanding a temporary slice if more bytes are needed

我在目录中以编程方式生成随机文件,至少 temporaryFilesTotalSize 的随机数据(多一点,谁在乎)。

这是我的代码:

var files []string

    for size := int64(0); size < temporaryFilesTotalSize; {
        fileName := random.HexString(12)
        filePath := dir + "/" + fileName
        file, err := os.Create(filePath)
        if err != nil {
            return nil, err
        }

        size += rand.Int63n(1 << 32) // random dimension up to 4GB
        raw := make([]byte, size)
        _, err := rand.Read(raw)
        if err != nil {
            panic(err)
        }

        file.Write(raw)
        file.Close()
        files = append(files, filePath)
    }

有什么方法可以避免在 for 循环中进行 raw := make([]byte, size) 分配? 理想情况下,我想在堆上保留一个切片,只有在需要更大的 size 时才增长。有什么方法可以有效地做到这一点?

首先你应该知道生成随机数据并将其写入磁盘至少比为缓冲区分配连续内存慢一个数量级。这绝对属于“过早优化”类别。消除在迭代中创建缓冲区不会使您的代码明显更快。

重用缓冲区

但是要重用缓冲区,将其移出循环,创建所需的最大缓冲区,并在每次迭代中将其切片到所需的大小。这样做是可以的,因为我们将用随机数据覆盖我们需要的整个部分。

请注意,我稍微更改了 size 生成(可能是您的代码中的一个错误,因为您总是增加生成的临时文件,因为您对新文件使用了 size 累积大小)。

另请注意,使用 []byte 中准备的内容写入文件最简单,只需调用 os.WriteFile().

像这样:

bigRaw := make([]byte, 1 << 32)

for totalSize := int64(0); ; {
    size := rand.Int63n(1 << 32) // random dimension up to 4GB
    totalSize += size
    if totalSize >= temporaryFilesTotalSize {
        break
    }

    raw := bigRaw[:size]
    rand.Read(raw) // It's documented that rand.Read() always returns nil error

    filePath := filepath.Join(dir, random.HexString(12))
    if err := os.WriteFile(filePath, raw, 0666); err != nil {
        panic(err)
    }

    files = append(files, filePath)
}

在没有中间缓冲区的情况下解决任务

由于您正在编写大文件 (GB),因此分配这么大的缓冲区并不是一个好主意:运行运行该应用程序将需要 GB 的 RAM!我们可以通过内部循环改进它以使用较小的缓冲区,直到我们写入预期大小,这解决了大内存问题,但增加了复杂性。对我们来说幸运的是,我们可以在没有任何缓冲的情况下解决任务,甚至可以降低复杂性!

我们应该以某种方式“引导”来自 rand.Rand to the file directly, something similar what io.Copy() does. Note that rand.Rand implements io.Reader, and os.File implements io.ReaderFrom 的随机数据,这表明我们可以简单地将 rand.Rand 传递给 file.ReadFrom(),而 file 本身会直接从 rand.Rand 获取将要写入的数据。

这听起来不错,但是 ReadFrom() 从给定的 reader 读取数据直到 EOF 或错误。如果我们通过 rand.Rand,这两种情况都不会发生。我们确实知道要读取和写入多少字节:size.

我们的“救援”来了 io.LimitReader():我们将一个 io.Reader 和一个大小传递给它,返回的 reader 将提供不超过给定字节数的字节数,然后会报告 EOF.

请注意,创建我们自己的 rand.Rand 也会更快,因为我们传递给它的源将使用 rand.NewSource() 创建,其中 returns 一个“未同步”的源(不安全并发使用)这反过来会更快! default/global rand.Rand 使用的源是同步的(因此对于并发使用是安全的——但速度较慢)。

完美!让我们看看实际效果:

r := rand.New(rand.NewSource(time.Now().Unix()))

for totalSize := int64(0); ; {
    size := r.Int63n(1 << 32)
    totalSize += size
    if totalSize >= temporaryFilesTotalSize {
        break
    }

    filePath := filepath.Join(dir, random.HexString(12))
    file, err := os.Create(filePath)
    if err != nil {
        return nil, err
    }

    if _, err := file.ReadFrom(io.LimitReader(r, fsize)); err != nil {
        panic(err)
    }

    if err = file.Close(); err != nil {
        panic(err)
    }

    files = append(files, filePath)
}

请注意,如果 os.File 不会实现 io.ReaderFrom,我们仍然可以使用 io.Copy(),提供文件作为目标,以及有限的 reader(上面使用的) 作为来源。

最后说明:最好使用 defer 关闭文件(或任何资源),因此无论如何都会调用它。虽然在循环中使用 defer 有点棘手,因为延迟函数 运行 在封闭函数的末尾,而不是在循环迭代的末尾。所以你可以把它包装在一个函数中。详情见