【问题标题】:Type assertion to interfaces, what happens internally对接口进行类型断言,内部发生了什么
【发布时间】:2018-11-30 09:23:22
【问题描述】:

我很好奇当 Go 以另一个接口为目的地执行类型断言时内部会发生什么。只是为了举例,请考虑来自Dave Cheney's blog的这个例子:

type temporary interface {
    Temporary() bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
        te, ok := err.(temporary)
        return ok && te.Temporary()
}

我预计这里会发生相当多的运行时开销,因为它必须检查 err 的类型并确定它是否具有所有方法。是这样吗,还是在下面发生了一些聪明的魔法?

【问题讨论】:

  • 嗯,这是纯粹的实现细节,并不是每个编译器都必须、能够、将要或将在未来做或过去做同样的事情。如果在某个时间对某个编译器感到好奇:检查源代码。
  • @Volker 感谢您提供的信息丰富的回答。

标签: go methods interface type-assertion


【解决方案1】:

您描述的期望是有效的并且成立。运行时必须检查动态类型的method set 是否是您要断言的接口类型的超集。

但不要害怕。执行此操作经过了高度优化(这是您的“智能魔法”)。

首先,函数类型由结构内部描述,其中方法签名(参数和结果类型)由称为签名id的单个整数值表示。如果 2 个函数具有相同的签名,则它们具有相同的签名 id。因此,要比较 2 个函数(判断 2 个方法是否相同),运行时只需比较名称(字符串比较)和签名 id(整数比较)。

接下来,动态类型T是否实现接口I只检查/计算一次,并将结果缓存。所以即使这个检查涉及到一些工作,也不会执行多次,只执行一次,每当需要进行相同的类型检查(相同的类型断言)时,都会查找并使用缓存的结果。

因此,对接口类型的类型断言最终归结为:(1)计算哈希值(一些按位运算),(2)从映射中查找值,(3)构造结果接口值。

有关接口表示的介绍,请阅读Russ Cox: Go Data Structures: Interfaces

这是一篇包含上述所有细节的文章:How interfaces work in Go

例如描述一个函数的相关部分是:

type _func struct {
    name      string  
    methodSig uint // two methods with the same signature have
                   // the same signature id. Receiver parameter
                   // doesn't contribute to this signature.
    funcSig   uint // receiver parameter accounts to this signature.

    // other information ...
}

类型断言到接口类型:

这是将接口值断言为接口类型的内部函数:

// To call this function, compilers must assure 
// 1. itype is an interface type.
// 2. outI is nil or stores the address of a value of itype.
// 3. outOk is nil or stores the address of a bool value.
func assertI2I (ivalue _interface, itype *_type,
        outI *_interface, outOk *bool) {
    // dynamic value is untype nil.
    if ivalue.dynamicTypeInfo == nil {
        // if ok is not present, panic.
        if outOk == nil {
            panic("interface is nil, not " + itype.name)
        }

        *outOk = false
        if outI == nil {
            *outI = _interface {
                dynamicValue:    nil,
                dynamicTypeInfo: nil,
            }
        }

        return
    }

    // check whether or not the dynamic type implements itype
    var impl = getImpl(itype, ivalue.dynamicTypeInfo.dtype)

    // assersion fails.
    if impl == nil {
        // if ok is not present, panic.
        if outOk == nil {
            panic("interface is " +
                ivalue.dynamicTypeInfo.dtype.name +
                ", not " + itype.name)
        }

        // return (zero value, false)
        *outOk = false
        if outI != nil {
            *outI = _interface {
                dynamicValue:    nil,
                dynamicTypeInfo: nil,
            }
        }

        return
    }

    // assersion succeeds.

    if outI == nil {
        *outOk = true
    }
    if outI != nil {
        *outI = _interface {
            dynamicValue:    ivalue.dynamicValue,
            dynamicTypeInfo: impl,
        }
    }
}

下面是从接口类型和非接口类型获取_implementation值的函数:

// global table
var cachedImpls = map[uint64]*_implementation{}

// itype must be an interface type and
// dtype must be a non-interface type.
// Return nil if dtype doesn't implement itype.
// Must not return nil if dtype implements itype.
func getImpl (itype *_type, dtype *_type) *_implementation {
    var key = uint64(itype.id) << 32 | uint64(dtype.id)
    var impl = cachedImpls[key]
    if impl == nil {
        // for each (dtype, itype) pair, the implementation
        // method table is only calculated most once at
        // run time. The calculation result will be cached.

        var numMethods = len(itype.methods)
        var methods = make([]*_func, numMethods)

        // find every implemented methods.
        // The methods of itype and dtype are both sorted
        // by methodSig and name.
        var n = 0
        var i = 0
        for _, im := range itype.methods {
            for i < len(dtype.methods) {
                tm := dtype.methods[i]
                i++

                // Here, for simplicity, assume
                // all methods are exported.

                if tm.methodSig < im.methodSig {
                    continue
                }
                if tm.methodSig > im.methodSig {
                    // im method is not implemented
                    return nil
                }
                if tm.name < im.name {
                    continue
                }
                if tm.name > im.name {
                    // im method is not implemented
                    return nil
                }

                methods[n] = tm
                n++
                break
            }
        }

        // dtype doesn't implement all methods of itype
        if n < numMethods {
            return nil
        }

        // dtype implements itype.
        // create and cache the implementation.
        impl = &_implementation{
            dtype: dtype, 
            itype: itype, 
            methods: methods,
        }
        cachedImpls[key] = impl
    }

    return impl
}

【讨论】:

  • 附录:虽然类型断言确实很便宜,但你不应该开始到处乱扔类型断言并使用空接口传递所有值(如果你想这样做 - 只需使用蟒蛇!:P)。在像 OP 示例这样的情况下(从错误中获取更多信息)在适当的接口上使用类型断言,并尝试尽可能少地使用空接口,并且仅在绝对必要时使用 (as Rob Pike said, "interface{} means nothing").
  • 我还推荐阅读this article。今天的 Go 与此略有不同——uintptr 大小的值不再直接嵌入到接口值中——但其余部分仍然适用,并且 IMO 很好地解释了如何实现接口的想法。
  • @kostix 是的,那是我最初想要包含的链接,但另一个直接回答了类型断言的工作原理,所以我选择了它,并在制作过程中忘记了这个链接回答。
猜你喜欢
  • 2017-12-12
  • 2023-04-03
  • 1970-01-01
  • 1970-01-01
  • 2021-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多