Почему Go не сообщает об ошибке компиляции при передаче указателя на функцию в качестве значения? - PullRequest
0 голосов
/ 04 ноября 2019

Полагаю, если я попытаюсь передать указатель на функцию, то это объявление функции также должно получить указатель? Не уверен, я попробовал это:

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» right, которая является копией внутри «f».

Так, какой пункт я получилнеправильно?

Ответы [ 2 ]

2 голосов
/ 04 ноября 2019

См. ответ Колминатора , но для довольно несовершенной аналогии с прямым C-кодом представьте, что:

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

- или в этом случае объявив i I, чтобы сделать i имеет тип интерфейса, который вы определили - это все равно что объявить 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) к этой функции-установщику. 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 тогда и только тогда, когда оба слота (i.type иi.u) - ноль. То, что постоянно запутывает людей, заключается в том, что если вы инициализируете значение interface из какого-либо неинтерфейсного типа, его слот type больше не равен нулю, и тест:

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

не делаетне работает, , даже если значение слота (i.u.p в нашей аналогии здесь) равно nil.


1 Я показываю это как объединение нескольких типов Си, но не включаю struct типов. Фактически, размер второго слота значения interface не является чем-то, что компилятор дает какие-либо обещания, хотя в современных компиляторах это всего лишь 8 байтов, как и любой другой указатель. Если какой-либо тип значения, который у вас есть, слишком велик для реальной базовой реализации, тем не менее, компилятор вставляет выделение: значение помещается в некоторую дополнительную память, и поле указателя объединения устанавливается для указания значения.

во время компиляции компилятор проверяет, соответствует ли тип фактического значения, которое вы вставляете в какой-либо интерфейс, этому интерфейсу. Тип интерфейса имеет список функций, которые он должен поддерживать. Если базовый тип имеет эти функции, присваивание в порядке (и компилятор знает, как построить соответствующие vtable-подобные данные, упомянутые в сноске 2). Если в базовом типе отсутствуют некоторые функции, вы получите ошибку во время компиляции. Таким образом, вы определенно гарантированы, что последующий поиск функции в переменной интерфейса всегда будет успешным.

2 Поиск выполняется быстрее, чем подразумеваемый поиск строки, поскольку Set имеет целое числокодовое значение, которое компилятор назначил во время компиляции этому конкретному типу интерфейса, и внутреннее содержимое struct type_info имеет различные таблицы быстрого поиска, несколько сродни таблицам C ++, чтобы помочь ему.

"чрезмерный"количество магии "в большинстве случаев значительно уменьшается, чтобы просто" поместить правильный параметр в правильный регистр аргументов или расположение в стеке ": копировать лишние байты, которые вызываемый вызывающий никогда не считывает, безопасно. Однако если для целочисленного или плавающего числа требуются разные регистры аргументов, это становится немного сложнее, и я не уверен, что на самом деле делают текущие компиляторы Go.

3 В v, ok := i.(int) форма, если слот типа не удерживает int, v установлен на ноль, а ok установлен на false. Это справедливо независимо от фактического типа: все типы имеют нулевое значение по умолчанию, а v становится нулевым значением указанного вами типа.

2 голосов
/ 04 ноября 2019

f(&s) передает адрес указателя s по значению - как и любой другой вызов функции go. Тот факт, что функция принимает параметр интерфейса, не меняет этого факта.

Теперь относительно того, как работает интерфейс: значение интерфейса содержит 2 элемента: значение и базовый тип. Значением в этом случае является указатель на структуру. Тип проверяет, что s удовлетворяет интерфейсу - потому что он реализует сигнатуры функции Get / Set.

Поскольку получатель указателя метода может изменять поля данных получателя - &s может быть изменено с помощьюSet метод. И, соответственно, вызов f(&s) - который вызывает Set - таким образом также изменяет состояние struct s.

PS это поведение является критическим для большей части стандартной библиотеки go. Многие пакеты, например http, используют интерфейсы io.Reader & io.Writer. Функции и методы, которые принимают значения, которые реализуют эти интерфейсы, полагаются на изменение базовых конкретных типов, чтение сетевых портов, очистку кешей и т. Д., При этом не обременяя вызывающего абонента этими внутренними побочными эффектами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...