为什么将指针传递给函数作为值时,Go不会报告编译错误?

时间:2019-11-04 05:06:34

标签: function pointers go parameters

我想如果我尝试将指针传递给函数,那么此函数声明也应该接收指针?不知道,我试过了:

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”。 >

那我错了哪一点?

2 个答案:

答案 0 :(得分:2)

f(&s)通过值传递s的指针地址-就像其他任何go函数调用一样。函数带有接口参数的事实并不会改变这一事实。

现在有关接口的工作方式:接口值包含2个项目:值和基础类型。在这种情况下,该值是指向该结构的指针。该类型验证s是否满足该接口-因为它实现了Get / Set函数签名。

由于方法的指针接收者可以更改接收者的数据字段-&s可以通过Set方法来更改。并且通过扩展,调用f(&s)-这会调用Set-因此也会更改结构s的状态。

P.S。对于大多数go标准库而言,此行为至关重要。许多包裹例如http依赖于io.Readerio.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变成您给定类型的零值。