首页 > 解决方案 > 为什么 Go 在将函数指针作为值传递时不报告编译错误?

问题描述

我想如果我尝试将一个指针传递给一个函数,那么这个函数声明也应该接收一个指针?不确定,我试过这个:

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)

我不确定这是否是“按值传递”声明,如果按值,那么“i”不应该在函数“f”之外更改,它是“f”内部的副本。

那么我在哪一点上错了?

标签: functionpointersgoparameters

解决方案


请参阅colminator 的答案,但是,对于直接 C 代码的一个相当不完美的类比,想象一下:

var x interface{ ... } // fill in the `...` part with functions

——或者在这种情况下,声明i Imakei具有你定义的接口类型——就像声明一个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时填充插槽,并填充以指向对象。1&sii.u.ps

当您调用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
}

检查类型槽,同时将值槽复制到新变量v3

请注意,当且仅当两个(和) 都为零时,interface变量才比较等于。一直绊倒人们的是,如果你从某个非接口类型初始化一个值,它的槽不再是 nil,并且测试:nil i.typei.uinterfacetype

if i == nil { // handle the case ...

不起作用,即使值槽i.u.p在我们这里的类比中) nil.


1我将其显示为几种 C 类型的联合,但不包括struct类型。事实上,一个interface值的第二个槽的大小并不是编译器做出的任何承诺,尽管在当前的编译器中,它与任何其他指针一样只有 8 个字节。但是,如果您拥有的任何值类型对于实际的底层实现来说太大了,编译器会插入一个分配:该值进入一些额外的内存,并且联合的指针字段被设置为指向该值。

编译器在编译时检查您填充到某个接口的实际值的类型是否适合该接口。一个接口类型有一个它必须支持的函数列表。如果底层类型具有这些函数,则赋值是可以的(并且编译器知道要构建脚注 2 中提到的适当的类似 vtable 的数据)。如果底层类型缺少某些函数,则会出现编译时错误。因此,您绝对可以保证以后对接口变量的函数查找总是会成功。

2查找比此处隐含的字符串查找更快,因为Set它具有编译器在编译时分配给该特定接口类型的整数代码值,并且内部struct type_info有各种快速查找表,有点类似于 C++ vtables,以帮助它也是。

在大多数情况下,“过多的魔法”被大大减少为只是“将正确的参数放在正确的参数寄存器或堆栈位置”:复制被调用者从未读取的额外字节是无害的。但是,如果整数与浮点需要不同的参数寄存器,那就有点棘手了,而且我不确定当前的 Go 编译器实际上在这里做了什么。

3v, ok := i.(int)表格中,如果类型槽持有intv则设置为零,并ok设置为false。无论实际类型如何,这都成立:所有类型都具有默认的零值,并v成为您提供的类型的零值。


推荐阅读