我想如果我尝试将指针传递给函数,那么此函数声明也应该接收指针?不知道,我试过了:
package main
import (
"fmt"
)
type I interface {
Get() int
Set(int)
}
type S struct {
Age int
}
func (s S) Get() int {
return s.Age
}
func (s *S) Set(age int) {
s.Age = age
}
func f(i I) {
i.Set(10)
fmt.Println(i.Get())
}
func main() {
s := S{}
f(&s) //4
fmt.Println(s.Get())
}
它打印
10
10
我们看到f的函数是
func f(i I)
我不确定这是否是“按值传递”声明,如果按值表示,则不应在函数“ f”外部(即“ f”内部的副本)外部更改“ i”。 >
那我错了哪一点?
答案 0 :(得分:2)
f(&s)
通过值传递s
的指针地址-就像其他任何go函数调用一样。函数带有接口参数的事实并不会改变这一事实。
现在有关接口的工作方式:接口值包含2个项目:值和基础类型。在这种情况下,该值是指向该结构的指针。该类型验证s
是否满足该接口-因为它实现了Get / Set函数签名。
由于方法的指针接收者可以更改接收者的数据字段-&s
可以通过Set
方法来更改。并且通过扩展,调用f(&s)
-这会调用Set-因此也会更改结构s
的状态。
P.S。对于大多数go标准库而言,此行为至关重要。许多包裹例如http
依赖于io.Reader
和io.Writer
接口。接受实现这些接口的值的函数和方法,依赖于更改状态,读取网络端口,刷新缓存等底层具体类型来起作用-始终不会给调用者带来这些内部副作用。
答案 1 :(得分:2)
请参见colminator's answer,但是,对于直接C代码的不完全类比,可以想象一下:
var x interface{ ... } // fill in the `...` part with functions
-或在这种情况下,声明i I
以使i
具有您定义的接口类型-就像声明C有两个成员的C struct
,一个成员持有一个类型,一个保存该类型的值的
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
传递到&s
时,编译器将填充i
插槽,并填充i.u.p
以指向对象s
。 > 1
当您调用i.Set(10)
时,Go编译器会将其转换为以下内容:
(*__lookup_func(i, "Set"))(i.u.p)
在__lookup_func
找到实际的func (s *S) Set(age int)
的地方,大量的魔术发现它应该将指向s
的指针(从i.u.p
传递到该setter函数。< sup> 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
和{{1 }}为零。一直困扰着人们的是,如果您从某个非接口类型初始化一个nil
值,那么它的i.type
槽将不再为nil,并且进行测试:
i.u
不起作用,即使值槽(在我们的比喻中为interface
),是 type
。
1 我将其显示为几种C类型的并集,但不包括if i == nil { // handle the case ...
类型。实际上,i.u.p
值的第二个插槽的大小并不是编译器作出任何承诺的,尽管在当前的编译器中,它与其他任何指针一样只是8个字节。但是,如果您拥有的任何值类型对于实际的基础实现而言都太大,则编译器会插入一个分配:该值将进入一些额外的内存中,并且并集的指针字段设置为指向该值。
编译器在编译时检查您要填充到某个接口中的 actual 值的类型是否适合该接口。接口类型具有它必须支持的功能列表。如果基础类型具有这些功能,则分配正常(并且编译器知道要构建脚注2中提到的类似vtable的数据)。如果基础类型缺少某些函数,则会出现编译时错误。因此,您可以肯定保证以后在接口变量上进行的函数查找将始终成功。
2 这里的查找比隐含的字符串查找要快,因为nil
具有整数代码值,编译器在编译时将其分配给该特定接口类型,而内部{ 1}}的东西具有各种类似于C ++ vtables的快速查找表,以帮助解决问题。
在大多数情况下,“过多的魔术”减少为仅“将正确的参数放入正确的参数寄存器或堆栈位置”:复制被调用者从未读取的多余字节是无害的。但是,如果整数与浮点数需要不同的参数寄存器,则将变得有些棘手,而且我不确定当前的Go编译器在此实际上做什么。
3 在struct
形式中,如果类型插槽不保持interface
,则Set
设置为零,并且struct type_info
设置为v, ok := i.(int)
。这与实际类型无关,都保持不变:所有类型都有默认的零值,而int
变成您给定类型的零值。