go proposal: io: CopierTo and CopierFrom interfaces

qvtsj1bj  于 3个月前  发布在  Go
关注(0)|答案(7)|浏览(42)

提案详情

当源支持时,io.Copyio.CopyBuffer 函数将使用 WriteTo;当目标支持时,将使用 ReadFrom
有时,一种类型可以高效地支持 WriteToReadFrom,但只是在某些情况下。例如,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 中的快速路径未被采用。)
之前有关于解决此问题的建议:

这些问题还包含一些其他示例出现此问题的链接,但我认为 *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)
}

当可用时,CopierToCopierFrom 接口取代了 WriterToReaderFrom。它们提供了一种将复制缓冲区沿着复制操作传递的方法,并允许实现为某些源或目的地实现快速路径,同时将其他情况推迟给 io.Copy
我们将更新 io.Copyio.CopyBuffer 以在可用时优先选择 CopierToCopierFrom
我们将更新 *os.File*net.TCPConn(以及可能的其他类型),以添加 CopyToCopyFrom 方法。

vof42yt1

vof42yt11#

当CopyTo返回错误时,IsCopyTo是否保证没有可观察到的效果?Is(errors.ErrUnsupported)错误?

bjg7j2ky

bjg7j2ky2#

当CopyTo返回错误时,IsCopyTo是否保证没有可观察到的效果?
是的。

9njqaruj

9njqaruj3#

也许我错过了要点,但解决这个问题的具体问题是什么?通过强制使用屏障结构(如在#16474中提到的)来避免任何不需要的类型擦除?如果你正在使用io.CopyBuffer,那么你很可能已经为缓冲区做了特殊准备,所以我们谈论的是有意的、明确的优化,而不是io.WriterTo等隐式系统范围内的优化。

io.CopyBuffer并不是唯一表现出无法用类型系统表达的行为的函数,需要引起注意:如果读取器无法将数据擦除为精确值,compress/zlib.NewReader可能会丢弃数据;bufio.(*Reader).WriteTo甚至可以调用ReadFrom(尽管未记录)。如果你想阻止一个函数利用其快速路径,隐藏你自己的功能。

eqfvzcg8

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):

func (t *transferWriter) doBodyCopy(dst io.Writer, src io.Reader) (n int64, err error) {
	buf := getCopyBuf()
	defer putCopyBuf(buf)
	n, err = io.CopyBuffer(dst, src, buf)
	if err != nil && err != io.EOF {
		t.bodyReadError = err
	}
	return
}

这个函数从用户提供的 io.Reader 复制到一个 io.Writer 。它不知道源和目标,尽管通常由 net/http 包创建。它使用 io.CopyBuffer 并进行缓冲池化以最小化分配。复制操作利用了 sendfile 时可用。
让我们考虑在 macOS 上的情况,从 *os.File 创建的 os.Pipe 复制到一个 *net.TCPConn
在这种情况下:

  • doBodyCopy 从其池中分配一个复制缓冲区。
  • io.CopyBuffer 发现源支持 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.Readerio.Writer
我们可以更改 doBodyCopy 以屏蔽读取器和写入器上的任何 ReadFromWriteTo 方法,代价是在需要时禁用 sendfile 优化。要求每个调用 io.CopyBuffer 的调用者在所有地方都这样做会很不幸。
这很混乱。
总的来说,我有两个目标:当使用标准库中的常见类型(文件、网络套接字)时:

  • io.Copy (及相关函数)不应递归地调用自身;
  • io.CopyBuffer 应使用提供的缓冲区。

可认为对 Copy 的递归调用并非问题,但它们为调用栈添加了如此多的混乱,以至于变得非常难以理解复制操作中实际发生了什么。我认为在这个点上丢失的缓冲区是一个明显的 bug:自 Go 版本 2.2 以来,具有 x2e0f0xa 作为源的 x2e0f0xb 将永远不会使用提供的缓冲区。这种变化似乎在我看来是从 Go 版本 2.2 到 2.3 之间的一个简单的回归。
Go兼容性承诺关闭了许多修复此问题的途径:

  • 我们记录为在可用时调用 x2e0f0xc / x2e0f0xe。
  • 将 x2e0f0xf 更改为不调用 x2e0f0xe 似乎可能导致对当前行为的代码期望回归。
  • 我们无法从 x2e0f0xi 中删除 x2e0f0yj 方法。
  • 我们无法从 x2e0f0zi 中删除 x2e0f0zh 方法。
  • 我们无法从现有的 x2e0f0yi 或 x2e0f0zj 方法中开始返回 x2e0f0zw 。

(在这些选项中,最后一条似乎是最可行的:如果可以在 fast sendfile path 不可用时从现有的 x2e0f0yi 和 x2e0f0zj 方法返回,那么我们就可以不用新的 API 通过关掉它来解决这个问题。这是 e0f0xa,但它被拒绝了。也许应该重新开启?)
问题的根本在于 x2e0f0xb 和 x2e0f0xc 不是在 x2e0f0xd 和 x2e0f0xe 之间正确连接的标准库和类型之间的接口:

  • 对快速路径复制的支持可能取决于(源,目标)配对,而与它们关联的 x2e0f0xe 和 x2e0f0xf 必须只能与其中一个相关联。需要有一种方法让类型支持快速复制,同时有回退到常规复制路径的方法。
  • 如果一个类型需要缓冲区来支持其复制,则该类型的 x2e0f0xf 没有访问提供的缓冲区的权限。

提议的 x2e0f0xefn/x2e0f0xfmn旨在通过提供包含必要功能的接口来修复在 gopkg.in/gomail.v2 以及 gopkg.in/gomail.v3 package types上的一些问题。
添加更多魔法方法以帮助我们摆脱由于现有魔法方法造成的困境似乎有些奇怪。如果我们一开始就设计了 gopkg.in/gomail.v4 而不是现在这样,可能会有更好的方法来做这件事。但鉴于当前的限制,我认为没有其他出路了,我相信这个提案确实解决了问题,而且我认为引入 gopkg.in/gomail.v5 可以使出现令人惊讶的行为变得普遍到足以需要采取行动的程度。

ogsagwnx

ogsagwnx5#

我们可以通过使用内部包来约束这个,并寻找返回在那个内部包中定义的类型的那些方法。这意味着只有标准库中的类型才会实现新的接口。一方面,这将阻止其他包利用这些新方法。另一方面,它将限制额外的复杂性。
例如:

package iofd

type FD uintptr

type SysFDer interface {
    SysFD() FD
    CopyBetween(dst, src FD, buf []byte) (int64, bool, error)
}

然后在io包中:

srcfd, srcOK := src.(iofd.SysFDer)
    dstfd, dstOK := dst.(iofd.SysFDer)
    if srcOK && dstOK {
        len, handled, err := srcfd.CopyBetween(dstfd, srcfd, buf)
        if handled {
            return len, err
        }
    }
zpf6vheq

zpf6vheq6#

我们可以将此限制为仅内部使用,但我认为这并不值得。我们仍然会面临新方法的复杂性,只是不允许其他人利用这些优势。

zbwhf8kr

zbwhf8kr7#

我认为如果没有人能实现新方法,那么它们的复杂性就会显著降低。特别是我们可以设计新方法,使它们在源和目标之间工作,这是我们需要的。当前方法的一个问题是它们只在源或目标上工作,这就是我们陷入需要使用两者,因此需要关闭两者,从而增加复杂性的情况。

相关问题