Using an io.WriteSeeker without a File in Go

huangapple go评论105阅读模式
英文:

Using an io.WriteSeeker without a File in Go

问题

我正在使用一个第三方库来生成PDF。为了在最后写入PDF(在使用该库的API添加所有内容之后),pdfWriter类型有一个期望io.WriteSeekerWrite函数。

如果我想使用文件进行操作,这是可以的,但我需要在内存中进行操作。问题是,我找不到任何方法来实现这一点 - 我找到的唯一实现io.WriteSeeker的本地类型是File。

以下是在pdfWriterWrite函数中使用File作为io.Writer的部分代码:

fWrite, err := os.Create(outputPath)
if err != nil {
    return err
}

defer fWrite.Close()

err = pdfWriter.Write(fWrite)

有没有办法在没有实际文件的情况下完成这个操作?比如获取一个[]byte或其他什么东西?

英文:

I am using a third party library to generate PDFs. In order to write the PDF at the end (after all of content has been added using the lib's API), the pdfWriter type has a Write function that expects an io.WriteSeeker.

This is OK if I want to work with files, but I need to work in-memory. Trouble is, I can't find any way to do this - the only native type I found that implements io.WriteSeeker is File.

This is the part that works by using File for the io.Writer in the Write function of the pdfWriter:

fWrite, err := os.Create(outputPath)
if err != nil {
    return err
}

defer fWrite.Close()

err = pdfWriter.Write(fWrite)

Is there way to do this without an actual File? Like getting a []byte or something?

答案1

得分: 12

很遗憾,标准库中没有现成的内存中的io.WriteSeeker实现解决方案。

但是,你总是可以自己实现。这并不难。

io.WriteSeeker是一个io.Writer和一个io.Seeker,所以基本上你只需要实现两个方法:

Write(p []byte) (n int, err error)
Seek(offset int64, whence int) (int64, error)

阅读它们的文档以了解这些方法的一般约定和行为。

下面是一个简单的实现示例,它使用了一个内存中的字节切片([]byte)。这个实现并没有针对速度进行优化,只是一个演示实现。

type mywriter struct {
    buf []byte
    pos int
}

func (m *mywriter) Write(p []byte) (n int, err error) {
    minCap := m.pos + len(p)
    if minCap > cap(m.buf) { // 确保buf有足够的容量:
        buf2 := make([]byte, len(m.buf), minCap+len(p)) // 添加一些额外的容量
        copy(buf2, m.buf)
        m.buf = buf2
    }
    if minCap > len(m.buf) {
        m.buf = m.buf[:minCap]
    }
    copy(m.buf[m.pos:], p)
    m.pos += len(p)
    return len(p), nil
}

func (m *mywriter) Seek(offset int64, whence int) (int64, error) {
    newPos, offs := 0, int(offset)
    switch whence {
    case io.SeekStart:
        newPos = offs
    case io.SeekCurrent:
        newPos = m.pos + offs
    case io.SeekEnd:
        newPos = len(m.buf) + offs
    }
    if newPos < 0 {
        return 0, errors.New("negative result pos")
    }
    m.pos = newPos
    return int64(newPos), nil
}

就是这样。

测试一下:

my := &mywriter{}
var ws io.WriteSeeker = my

ws.Write([]byte("hello"))
fmt.Println(string(my.buf))

ws.Write([]byte(" world"))
fmt.Println(string(my.buf))

ws.Seek(-2, io.SeekEnd)
ws.Write([]byte("k!"))
fmt.Println(string(my.buf))

ws.Seek(6, io.SeekStart)
ws.Write([]byte("gopher"))
fmt.Println(string(my.buf))

输出结果(在Go Playground上尝试):

hello
hello world
hello work!
hello gopher

可以改进的地方:

  • 创建一个mywriter值,初始时buf切片为空,但容量足以覆盖预计的结果PDF文档大小。例如,如果你估计结果PDF大约为1MB,可以创建一个容量为2MB的缓冲区,如下所示:
    my := &mywriter{buf: make([]byte, 0, 2<<20)}

  • mywriter.Write()中,当需要增加容量(并复制现有内容)时,可能会有利于使用更大的增量,例如将当前容量加倍到一定程度,这样可以为未来的追加操作保留空间,并最小化重新分配的次数。

英文:

Unfortunately there is no ready solution for an in-memory io.WriteSeeker implementation in the standard lib.

But as always, you can always implement your own. It's not that hard.

An io.WriteSeeker is an io.Writer and an io.Seeker, so basically you only need to implement 2 methods:

Write(p []byte) (n int, err error)
Seek(offset int64, whence int) (int64, error)

Read the general contract of these methods in their documentation how they should behave.

Here's a simple implementation which uses an in-memory byte slice ([]byte). It's not optimized for speed, this is just a "demo" implementation.

type mywriter struct {
	buf []byte
	pos int
}

func (m *mywriter) Write(p []byte) (n int, err error) {
	minCap := m.pos + len(p)
	if minCap &gt; cap(m.buf) { // Make sure buf has enough capacity:
		buf2 := make([]byte, len(m.buf), minCap+len(p)) // add some extra
		copy(buf2, m.buf)
		m.buf = buf2
	}
	if minCap &gt; len(m.buf) {
		m.buf = m.buf[:minCap]
	}
	copy(m.buf[m.pos:], p)
	m.pos += len(p)
	return len(p), nil
}

func (m *mywriter) Seek(offset int64, whence int) (int64, error) {
	newPos, offs := 0, int(offset)
	switch whence {
	case io.SeekStart:
		newPos = offs
	case io.SeekCurrent:
		newPos = m.pos + offs
	case io.SeekEnd:
		newPos = len(m.buf) + offs
	}
	if newPos &lt; 0 {
		return 0, errors.New(&quot;negative result pos&quot;)
	}
	m.pos = newPos
	return int64(newPos), nil
}

Yes, and that's it.

Testing it:

my := &amp;mywriter{}
var ws io.WriteSeeker = my

ws.Write([]byte(&quot;hello&quot;))
fmt.Println(string(my.buf))

ws.Write([]byte(&quot; world&quot;))
fmt.Println(string(my.buf))

ws.Seek(-2, io.SeekEnd)
ws.Write([]byte(&quot;k!&quot;))
fmt.Println(string(my.buf))

ws.Seek(6, io.SeekStart)
ws.Write([]byte(&quot;gopher&quot;))
fmt.Println(string(my.buf))

Output (try it on the Go Playground):

hello
hello world
hello work!
hello gopher

Things that can be improved:

  • Create a mywriter value with an initial empty buf slice, but with a capacity that will most likely cover the size of the result PDF document. E.g. if you estimate the result PDFs are around 1 MB, create a buffer with capacity for 2 MB like this:
    my := &amp;mywriter{buf: make([]byte, 0, 2&lt;&lt;20)}

  • Inside mywriter.Write() when capacity needs to be increased (and existing content copied over), it may be profitable to use bigger increment, e.g. double the current capacity to a certain extent, which reserves space for future appends and minimizes the reallocations.

huangapple
  • 本文由 发表于 2017年8月23日 18:09:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/45836767.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定