2 回答
TA贡献1796条经验 获得超7个赞
对于直接 C 代码的相当不完美的类比,想象一下:
var x interface{ ... } // fill in the `...` part with functions
或者在本例中,声明i I
makei
具有您定义的接口类型,就像声明具有struct
两个成员的 C,一个用于保存类型,另一个用于保存该类型的值:
struct I {
struct type_info *type;
union {
void *p;
int i;
double d;
// add more types if/as needed here
} u;
};
struct I i;
i.type当您传递&sto时,编译器会填充槽i,并填充i.u.p为指向对象s。1
当您调用 时i.Set(10),Go 编译器会将其转换为等价的:
(*__lookup_func(i, "Set"))(i.u.p)
where__lookup_func找到实际的func (s *S) Set(age int)并且过多的魔法发现它应该将指向s(from i.u.p) 的指针传递给该 setter 函数。2
事实上,某些接口类型的变量具有这两个槽(“类型”部分和保存当前值的类似联合的部分),这是这里真正的秘密武器。您可以使用类型断言:
v, ok := i.(int)
或类型开关:
switch v := i.(type) {
case int: // code where `v` is `var v int`
case float64: // code where `v` is `var v float64` ...
// add more cases as desired
}
检查类型槽,同时将值槽复制到新变量v。3
请注意,当且仅当两个槽(和) 都为零时interface,变量才比较等于。总是让人困惑的是,如果你从某种非接口类型初始化一个值,它的槽就不再是 nil,并且测试:nil i.typei.uinterfacetype
if i == nil { // handle the case ...
不起作用,即使值槽(i.u.p在我们的类比中)是 nil。
我将其显示为多个 C 类型的联合,但不包括struct
类型。事实上,interface
编译器并没有对值的第二个槽的大小做出任何承诺,尽管在当前的编译器中,它与任何其他指针一样只有 8 个字节。但是,如果您拥有的任何值类型对于实际的底层实现来说太大,编译器会插入一个分配:该值进入一些额外的内存,并且联合的指针字段被设置为指向该值。
编译器在编译时检查您填充到某个接口中的实际值的类型是否适合该接口。接口类型具有它必须支持的函数列表。如果底层类型具有这些函数,则赋值就可以(并且编译器知道构建脚注 2 中提到的适当的类似 vtable 的数据)。如果基础类型缺少某些函数,您会收到编译时错误。因此,您绝对可以保证以后对接口变量的函数查找总是会成功。
2这里的查找比隐含的字符串查找更快,因为Set
编译器在编译时为该特定接口类型分配了一个整数代码值,并且内部struct type_info
有各种快速查找表(有点类似于 C++ vtable)来帮助它以及。
在大多数情况下,“过多的魔法”大大减少为“将正确的参数放入正确的参数寄存器或堆栈位置”:复制被调用者从未读取的额外字节是无害的。不过,如果整数与浮点需要不同的参数寄存器,那就有点棘手了,而且我不确定当前的 Go 编译器在这里实际上做了什么。
3在该v, ok := i.(int)
表单中,如果类型槽不包含int
,v
则设置为零,并且ok
设置为false
。无论实际类型如何,这都成立:所有类型都有默认零值,并v
成为您指定类型的零值。
TA贡献1884条经验 获得超4个赞
f(&s)
正在按值传递指针地址s
- 就像任何其他 go 函数调用一样。该函数采用接口参数这一事实并没有改变这一事实。
现在关于接口如何工作:接口值包含 2 项:值和底层类型。本例中的值是指向该结构的指针。该类型验证是否s
满足接口 - 因为它实现了 Get/Set 函数签名。
由于方法的指针接收器可以更改接收器的数据字段 -&s
可以由该方法更改Set
。通过扩展,调用f(&s)
(调用 Set)也会改变 structs
的状态。
PS 这种行为对于大多数 go 标准库来说至关重要。例如,许多包都http
依赖于io.Reader
&io.Writer
接口。接受实现这些接口的值的函数和方法依赖于改变状态、读取网络端口、刷新缓存等的底层具体类型来工作——同时不会给调用者带来这些内部副作用的负担。
- 2 回答
- 0 关注
- 118 浏览
添加回答
举报