英文:
Using an io.WriteSeeker without a File in Go
问题
我正在使用一个第三方库来生成PDF。为了在最后写入PDF(在使用该库的API添加所有内容之后),pdfWriter
类型有一个期望io.WriteSeeker
的Write
函数。
如果我想使用文件进行操作,这是可以的,但我需要在内存中进行操作。问题是,我找不到任何方法来实现这一点 - 我找到的唯一实现io.WriteSeeker
的本地类型是File。
以下是在pdfWriter
的Write
函数中使用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 > 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 > 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
}
Yes, and that's it.
Testing it:
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))
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 emptybuf
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 := &mywriter{buf: make([]byte, 0, 2<<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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论