提案详情
当源支持时,io.Copy
和 io.CopyBuffer
函数将使用 WriteTo
;当目标支持时,将使用 ReadFrom
。
有时,一种类型可以高效地支持 WriteTo
或 ReadFrom
,但只是在某些情况下。例如,CL 472475 为 *os.File
添加了一个 WriteTo
方法,该方法在 Linux 系统上当目标是 Unix 或 TCP 套接字时,在复制操作上执行更高效的操作。在所有其他情况下,它会回退到 io.Copy
。io.Copy
的回退意味着具有 *os.File
源的 io.CopyBuffer
将不再使用提供的缓冲区,因为缓冲区没有通过 WriteTo
传递。(这也导致了 https://go.dev/issue/66988,其中回退导致 *net.TCPConn.ReadFrom 中的快速路径未被采用。)
之前有关于解决此问题的建议:
- proposal: io: "unimplemented" error return for WriterTo et al. #21904:被拒绝。
- proposal: io: CopyBuffer should avoid ReadFrom/WriteTo #16474:讨论正在进行中,但这不会解决
io.Copy
的情况。
这些问题还包含一些其他示例出现此问题的链接,但我认为 *os.File.WriteTo
的情况是一个需要解决的问题的例子,因为 io.CopyBuffer
在文件上不再按预期工作。
我建议我们在 io
包中添加两个新接口:
package io
// CopierTo is the interface that wraps the CopyTo method.
//
// CopyTo writes data to w until there is no more data to write or when an error occurs.
// The return value n is the number of bytes written.
// Any error encountered during the write is also returned.
//
// When len(buf) > 0, CopyTo may use the provided buffer as temporary space.
//
// CopyTo may return an error wrapping errors.ErrUnsupported to indicate that
// it is unable to efficiently copy to w.
//
// The Copy function uses CopierTo if available.
type CopierTo interface {
CopyTo(w Writer, buf []byte) (n int64, err error)
}
// CopierFrom is the interface that wraps the CopyFrom method.
//
// CopyFrom reads data from r until EOF or error.
// The return value n is the number of bytes read.
// Any error except EOF encountered during the read is also returned.
//
// When len(buf) > 0, CopyFrom may use the provided buffer as temporary space.
//
// CopyFrom may return an error wrapping errors.ErrUnsupported to indicate that
// it is unable to efficiently copy from r.
//
// The Copy function uses CopierFrom if available.
type CopierFrom interface {
CopyFrom(r Reader, buf []byte) (n int64, err error)
}
当可用时,CopierTo
和 CopierFrom
接口取代了 WriterTo
和 ReaderFrom
。它们提供了一种将复制缓冲区沿着复制操作传递的方法,并允许实现为某些源或目的地实现快速路径,同时将其他情况推迟给 io.Copy
。
我们将更新 io.Copy
和 io.CopyBuffer
以在可用时优先选择 CopierTo
和 CopierFrom
。
我们将更新 *os.File
和 *net.TCPConn
(以及可能的其他类型),以添加 CopyTo
和 CopyFrom
方法。
7条答案
按热度按时间vof42yt11#
当CopyTo返回错误时,IsCopyTo是否保证没有可观察到的效果?Is(errors.ErrUnsupported)错误?
bjg7j2ky2#
当CopyTo返回错误时,IsCopyTo是否保证没有可观察到的效果?
是的。
9njqaruj3#
也许我错过了要点,但解决这个问题的具体问题是什么?通过强制使用屏障结构(如在#16474中提到的)来避免任何不需要的类型擦除?如果你正在使用io.CopyBuffer,那么你很可能已经为缓冲区做了特殊准备,所以我们谈论的是有意的、明确的优化,而不是io.WriterTo等隐式系统范围内的优化。
io.CopyBuffer并不是唯一表现出无法用类型系统表达的行为的函数,需要引起注意:如果读取器无法将数据擦除为精确值,compress/zlib.NewReader可能会丢弃数据;bufio.(*Reader).WriteTo甚至可以调用ReadFrom(尽管未记录)。如果你想阻止一个函数利用其快速路径,隐藏你自己的功能。
eqfvzcg84#
这个提案的目的是修复在使用标准库类型(如
*os.File
和*net.TCPConn
)时io.Copy
的错误。也许一开始并不明显io.Copy
是有问题。让我们看一下来自net/http
包的一个具体示例(https://go.googlesource.com/go/+/refs/tags/go1.22.2/src/net/http/transfer.go#412):这个函数从用户提供的
io.Reader
复制到一个io.Writer
。它不知道源和目标,尽管通常由net/http
包创建。它使用io.CopyBuffer
并进行缓冲池化以最小化分配。复制操作利用了sendfile
时可用。让我们考虑在 macOS 上的情况,从
*os.File
创建的os.Pipe
复制到一个*net.TCPConn
。在这种情况下:
WriteTo
并调用它。*os.File.WriteTo
在 macOS 上没有特殊的处理方式,所以它将源 Package 在一个移除WriteTo
方法并调用io.Copy
的类型中。io.Copy
发现目标支持ReadFrom
并调用它。*net.TCPConn.ReadFrom
调用 sendfile,但失败了,因为 macOS 不支持从管道发送文件。*net.TCPConn.ReadFrom
将目标 Package 在一个移除ReadFrom
方法并调用io.Copy
的类型中。io.Copy
分配缓冲区并执行复制。注意调用栈中有 三个
io.Copy
操作。我们在第一个操作后丢失了提供给CopyBuffer
的用户分配的缓冲区。对于原始调用者(即doBodyCopy
)来说,显然提供给CopyBuffer
的缓冲区不会被使用;毕竟,它一直在操作一个普通的io.Reader
和io.Writer
。我们可以更改
doBodyCopy
以屏蔽读取器和写入器上的任何ReadFrom
或WriteTo
方法,代价是在需要时禁用 sendfile 优化。要求每个调用io.CopyBuffer
的调用者在所有地方都这样做会很不幸。这很混乱。
总的来说,我有两个目标:当使用标准库中的常见类型(文件、网络套接字)时:
io.Copy
(及相关函数)不应递归地调用自身;io.CopyBuffer
应使用提供的缓冲区。可认为对
Copy
的递归调用并非问题,但它们为调用栈添加了如此多的混乱,以至于变得非常难以理解复制操作中实际发生了什么。我认为在这个点上丢失的缓冲区是一个明显的 bug:自 Go 版本 2.2 以来,具有 x2e0f0xa 作为源的 x2e0f0xb 将永远不会使用提供的缓冲区。这种变化似乎在我看来是从 Go 版本 2.2 到 2.3 之间的一个简单的回归。Go兼容性承诺关闭了许多修复此问题的途径:
(在这些选项中,最后一条似乎是最可行的:如果可以在 fast sendfile path 不可用时从现有的 x2e0f0yi 和 x2e0f0zj 方法返回,那么我们就可以不用新的 API 通过关掉它来解决这个问题。这是 e0f0xa,但它被拒绝了。也许应该重新开启?)
问题的根本在于 x2e0f0xb 和 x2e0f0xc 不是在 x2e0f0xd 和 x2e0f0xe 之间正确连接的标准库和类型之间的接口:
提议的 x2e0f0xefn/x2e0f0xfmn旨在通过提供包含必要功能的接口来修复在 gopkg.in/gomail.v2 以及 gopkg.in/gomail.v3 package types上的一些问题。
添加更多魔法方法以帮助我们摆脱由于现有魔法方法造成的困境似乎有些奇怪。如果我们一开始就设计了 gopkg.in/gomail.v4 而不是现在这样,可能会有更好的方法来做这件事。但鉴于当前的限制,我认为没有其他出路了,我相信这个提案确实解决了问题,而且我认为引入 gopkg.in/gomail.v5 可以使出现令人惊讶的行为变得普遍到足以需要采取行动的程度。
ogsagwnx5#
我们可以通过使用内部包来约束这个,并寻找返回在那个内部包中定义的类型的那些方法。这意味着只有标准库中的类型才会实现新的接口。一方面,这将阻止其他包利用这些新方法。另一方面,它将限制额外的复杂性。
例如:
然后在io包中:
zpf6vheq6#
我们可以将此限制为仅内部使用,但我认为这并不值得。我们仍然会面临新方法的复杂性,只是不允许其他人利用这些优势。
zbwhf8kr7#
我认为如果没有人能实现新方法,那么它们的复杂性就会显著降低。特别是我们可以设计新方法,使它们在源和目标之间工作,这是我们需要的。当前方法的一个问题是它们只在源或目标上工作,这就是我们陷入需要使用两者,因此需要关闭两者,从而增加复杂性的情况。