Golang中如何实现不指定方法的嵌入?

Golang中如何实现不指定方法的嵌入? 我正在阅读 io.Copy 针对 TCPConn 的当前实现。其中有一个名为 tcpConnWithoutWriteTo 的结构体,它会导致从 io.ReaderWriterTo 的类型断言失败。它在内部是如何工作的,以及为什么这样设计?


更多关于Golang中如何实现不指定方法的嵌入?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复
func check(b Barer) {

为什么这里有一个 Barer 接口变量?Barer 并不包含 Fooer 接口。

更多关于Golang中如何实现不指定方法的嵌入?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,我知道这个效果。重申一下,我的问题是它为什么会起作用。根据语言的语法及其内部实现方式,这相当违反直觉。

它不会在类型转换时失败。当你尝试调用 WriteTo 时,程序会发生 panic。

我正在阅读 io.Copy 针对 TCPConn 的当前实现。其中有一个名为 tcpConnWithoutWriteTo 的结构体,它会导致从 io.ReaderWriterTo 的类型转换失败。它在内部是如何以及为何起作用的?

io.Copy 函数如何决定是应该通过使用 WriterTo 接口进行优化,还是回退到其他方法?

好的,我之前的例子举错了。它不会进行类型转换。但你的例子中也有一个错误。你的 panic 代码应该属于 noFoo。我认为这与你深入探究 TCP 和拼接有关(参见此处)。它是这样实现的,所以 io.Copy 也不会对其进行类型转换。将改用 ReadFrom 或标准的复制算法。

我发现,这被称为ambiguous selector(模糊选择器)错误。例如,如果你尝试更改check参数的接口,就能明确地看到这个错误。当存在嵌入(embedding)时,如果编译器在编译时无法确定在有多个同名方法的情况下将使用哪一个,就会发生此错误。在这种情况下,你需要通过基础结构体类型来“重新加载”该方法以解决问题。但由于在此情况下我们操作的是接口,它们是在运行时进行类型转换的。这里我们有一个copyBuffer函数,它接受Reader接口作为参数,而tcpConnWithoutWriteTo遵循这个接口,因此可以作为参数使用。但是,一旦我们尝试将其转换为WriterTo,就会因为模糊性而失败。因为我们使用了**value, ok := …**这种转换方式,它在运行时不会引发panic,错误会被静默处理,并直接返回ok == false。正如另一篇关于拼接的帖子中所讨论的,对于TCP,如果可能的话应该使用ReaderFrom,否则就使用默认的复制算法。

它绝对不会引发恐慌,每次用户在TCPConn上调用io.Copy时,程序都会崩溃。我有一个实现了相同模式的演示,类型断言只会返回nil和false,而不是引发恐慌:

package main

import "fmt"

type Fooer interface {
	Foo()
}

type Barer interface {
	Bar()
}

type base struct {
	msg string
}

func (b *base) Foo() {
	fmt.Println("Foo", b.msg)
}

func (b *base) Bar() {
	fmt.Println("Bar", b.msg)
}

type noFoo struct{}

type baseWithNoFoo struct {
	noFoo
	*base
}

func (b *baseWithNoFoo) Foo() {
	panic("Should not be called")
}

func check(b Barer) {
	f, ok := b.(Fooer)
	fmt.Println(f, ok)
}

func main() {
	b := &base{msg: "hello"}
	b.Foo()
	bNoFoo := baseWithNoFoo{base: b}
	check(bNoFoo)
}

示例输出:

[u@h d]% go run test.go
Foo hello
<nil> false

在Go中,不指定方法的嵌入是通过嵌入匿名结构体来实现的,这会继承嵌入类型的方法集,但不会继承其具体方法实现。tcpConnWithoutWriteTo 的设计正是利用了这一点。

工作原理

tcpConnWithoutWriteTo 结构体嵌入了 *net.TCPConn,但通过覆盖 WriteTo 方法为 nil 来阻止 io.Copy 使用 WriterTo 优化路径:

// net/net.go 中的实现
type tcpConnWithoutWriteTo struct {
    *net.TCPConn
}

func (c *tcpConnWithoutWriteTo) WriteTo(w io.Writer) (int64, error) {
    return c.TCPConn.WriteTo(w)
}

关键点在于,虽然结构体嵌入了 *net.TCPConn(它实现了 WriteTo 方法),但 tcpConnWithoutWriteTo 自身也定义了一个 WriteTo 方法。这会导致类型断言失败:

// io/io.go 中的类型断言检查
if wt, ok := src.(WriterTo); ok {
    return wt.WriteTo(dst)
}

示例代码

下面是一个简化示例,展示这种模式的工作原理:

package main

import (
    "fmt"
    "io"
)

// 基础类型,实现了 WriteTo 方法
type BaseConn struct{}

func (b *BaseConn) WriteTo(w io.Writer) (int64, error) {
    fmt.Println("BaseConn.WriteTo called")
    return 0, nil
}

func (b *BaseConn) Read(p []byte) (int, error) {
    fmt.Println("BaseConn.Read called")
    return 0, io.EOF
}

// 包装类型,通过嵌入继承方法但不继承 WriteTo 的具体实现
type ConnWithoutWriteTo struct {
    *BaseConn
}

// 重写 WriteTo 方法,使其不满足 WriterTo 接口
func (c *ConnWithoutWriteTo) WriteTo(w io.Writer) (int64, error) {
    // 调用基类的实现,但类型断言会失败
    return c.BaseConn.WriteTo(w)
}

func main() {
    baseConn := &BaseConn{}
    wrappedConn := &ConnWithoutWriteTo{BaseConn: baseConn}
    
    // 类型断言检查
    var src io.Reader = wrappedConn
    
    // 这个断言会失败,因为 wrappedConn 的 WriteTo 方法签名虽然匹配,
    // 但 Go 的接口满足性检查会考虑方法的接收者类型
    if wt, ok := src.(io.WriterTo); ok {
        fmt.Println("Type assertion succeeded")
        wt.WriteTo(nil)
    } else {
        fmt.Println("Type assertion failed - WriterTo optimization avoided")
    }
    
    // 直接调用 WriteTo 仍然可以工作
    wrappedConn.WriteTo(nil)
}

设计原因

这种设计的主要原因是性能优化和避免特定场景下的问题:

  1. 避免递归调用:在某些网络场景中,使用 WriteTo 可能导致不期望的递归或性能问题
  2. 控制数据路径:强制使用标准的 Read/Write 循环,而不是 sendfile 等系统调用优化
  3. 兼容性保证:确保在所有平台上行为一致

io.Copy 的实现中,当检测到 WriterTo 接口时,会使用更高效的路径。但某些情况下(如TCP连接的特殊处理),需要强制使用标准的复制循环,这时就需要通过这种嵌入技巧来绕过优化。

回到顶部