В Go есть ли способ доступа к закрытым полям структуры из другого пакета?

У меня есть структура в одном пакете с частными полями:

package foo

type Foo struct {
    x int
    y *Foo
}

И другой пакет (например, пакет тестирования белого ящика) нуждается в доступе к ним:

package bar

import "../foo"

func change_foo(f *Foo) {
    f.y = nil
}

Есть ли способ объявить bar как своего рода "дружественный" пакет или любой другой способ иметь доступ к foo.Foo частным членам из bar, но при этом сохранять их частными для всех других пакетов ( возможно, что-то в unsafe)?

Ответы

Ответ 1

Существует способ чтения неэкспонированных элементов с использованием отражения

func read_foo(f *Foo) {
    v := reflect.ValueOf(*f)
    y := v.FieldByName("y")
    fmt.Println(y.Interface())
}

Однако попытка использовать y.Set или иначе установить поле с отражением приведет к панике кода, который вы пытаетесь установить в неэкспонированном поле вне пакета.

Вкратце: нераспределенные поля должны быть непортированы по какой-либо причине, если вам нужно их изменить, либо поставить вещь, которая должна изменить ее в том же пакете, либо выставить/экспортировать некоторый безопасный способ ее изменения.

Тем не менее, в интересах полного ответа на этот вопрос вы можете сделать это

func change_foo(f *Foo) {
    // Since structs are organized in memory order, we can advance the pointer
    // by field size until we're at the desired member. For y, we advance by 8
    // since it the size of an int on a 64-bit machine and the int "x" is first
    // in the representation of Foo.
    //
    // If you wanted to alter x, you wouldn't advance the pointer at all, and simply
    // would need to convert ptrTof to the type (*int)
    ptrTof := unsafe.Pointer(f)
    ptrTof = unsafe.Pointer(uintptr(ptrTof) + uintptr(8)) // Or 4, if this is 32-bit

    ptrToy := (**Foo)(ptrTof)
    *ptrToy = nil // or *ptrToy = &Foo{} or whatever you want

}

Это действительно очень плохая идея. Он не переносится, если int когда-либо изменяется в размере, он будет терпеть неудачу, если вы когда-либо измените порядок полей в Foo, измените их типы или их размеры или добавите новые поля перед уже существующими, эта функция будет весело изменять новое представление в случайные данные тарабарщины, не сообщая вам. Я также думаю, что это может сломать сбор мусора для этого блока.

Пожалуйста, если вам нужно изменить поле извне пакета, напишите функциональность, чтобы изменить его из пакета или экспортировать.

Изменить: здесь немного более безопасный способ сделать это:

func change_foo(f *Foo) {
    // Note, simply doing reflect.ValueOf(*f) won't work, need to do this
    pointerVal := reflect.ValueOf(f)
    val := reflect.Indirect(pointerVal)

    member := val.FieldByName("y")
    ptrToY := unsafe.Pointer(member.UnsafeAddr())
    realPtrToY := (**Foo)(ptrToY)
    *realPtrToY = nil // or &Foo{} or whatever

}

Это безопаснее, поскольку он всегда найдет правильное поле имени, но он все еще недружелюбен, возможно, медленный, и я не уверен, что это связано с сборкой мусора. Он также не сможет предупредить вас, если вы делаете что-то странное (вы можете сделать этот код немного более безопасным, добавив несколько проверок, но я не буду беспокоиться, это очень хорошо подходит).

Также имейте в виду, что FieldByName восприимчив к разработчику пакета, изменяющему имя переменной. Как разработчик пакета, я могу сказать, что у меня нет абсолютно никаких проблем с изменением имен вещей, о которых пользователи не должны знать. Вы можете использовать поле, но тогда вы склонны к тому, чтобы разработчик менял порядок полей без предупреждения, и это тоже то, что я тоже не испытываю. Имейте в виду, что эта комбинация отражения и небезопасности... небезопасна, в отличие от обычных изменений имен, это не даст вам ошибки времени компиляции. Вместо этого программа будет внезапно паниковать или сделать что-то странное и undefined, потому что она получила неправильное поле, что означает, что даже если вы являетесь разработчиком пакета, который изменил имя, вы все равно можете не помнить, где бы вы ни занимались, и потратили в то время как отслеживание, почему ваши тесты внезапно сломались, потому что компилятор не жалуется. Я упоминал, что это плохая идея?

Edit2: Поскольку вы упоминаете тестирование White Box, обратите внимание, что если вы укажете файл в своем каталоге <whatever>_test.go, он не будет компилироваться, если вы не используете go test, поэтому, если вы хотите выполнить тестирование белого ящика, вверху объявить package <yourpackage>, который предоставит вам доступ к невыполненным полям, и если вы хотите выполнить тестирование черного ящика, вы используете package <yourpackage>_test.

Если вам нужно белую коробку проверить два пакета одновременно, я думаю, вы можете застрять и, возможно, придется переосмыслить свой дизайн.