0

Есть ли способ получить доступ к приватным полям структуры из другого пакета?

1

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

package foo

type Foo struct {
    x int
    y *Foo
}

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

package bar

import "../foo"

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

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

5 ответ(ов)

0

Есть способ читать неэкспортированные поля с помощью пакета reflect (в Go < 1.7):

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

Однако попытка использовать y.Set или иным образом установить значение поля с помощью reflect приведет к панике из-за того, что вы пытаетесь изменить неэкспортированное поле снаружи пакета.

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

Тем не менее, для полной ясности ответа, вы можете сделать это (и в Go >= 1.7 придется использовать данный способ):

func change_foo(f *Foo) {
    // Поскольку структуры организованы в память по порядку, мы можем увеличить указатель
    // на размер поля, пока не доберемся до нужного члена. Для поля y мы движемся на 8
    // байт, так как это размер int на 64-битной системе, а int "x" идет первым
    // в представлении Foo.
    //
    // Если вы хотите изменить x, вам не нужно смещать указатель вообще, достаточно
    // просто преобразовать ptrTof в тип (*int)
    ptrTof := unsafe.Pointer(f)
    ptrTof = unsafe.Pointer(uintptr(ptrTof) + uintptr(8)) // Или 4, если это 32-битная система

    ptrToy := (**Foo)(ptrTof)
    *ptrToy = nil // или *ptrToy = &Foo{} или что-то другое, что вам нужно
}

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

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

Редактирование 2: Раз вы упомянули тестирование белого ящика, обратите внимание, что если вы назовете файл в вашем каталоге <что-то>_test.go, он не будет компилироваться, если вы не используете go test, поэтому, если вы хотите провести тестирование белого ящика, в начале файла укажите package <вашпакет>, это даст вам доступ к неэкспортированным полям; если вы хотите провести тестирование черного ящика, то используйте package <вашпакет>_test.

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

0

Внутренние поля по сути не экспортируются из пакета, что позволяет автору пакета свободно изменять его внутренности, не нарушая работу других пакетов. Использование reflect или unsafe для обхода этого ограничения не является хорошей идеей.

Язык специально не предоставляет вам средств для достижения желаемого, поэтому вам придется решить эту задачу самостоятельно. Самый простой способ — объединить два пакета, что обычно делают модульные тесты в Go.

Другой вариант — создать дополнительный accessor, который будет использоваться только пакетом bar:

// SetYPrivate устанавливает значение y. Эта функция является приватной для пакетов foo и bar
// и не должна использоваться другими пакетами. Она может исчезнуть в будущих релизах.
func (foo *Foo) SetYPrivate(y int) {
    foo.y = y
}

Примером такой техники является функция runtime.MemStats в стандартной библиотеке, которая возвращает ряд приватных данных реализации сборщика мусора (GC).

0

Для улучшения ответа от @Liner, я предлагаю несколько изменений в коде и добавление комментариев для повышения читабельности и безопасность работы с указателями. Вот переработанная версия функции:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

// Получает смещение поля структуры по имени поля.
func getFieldOffset(instance interface{}, fieldName string) (uintptr, error) {
    instanceValue := reflect.ValueOf(instance)
    
    // Проверяем, что переданный параметр - это указатель на структуру.
    if instanceValue.Kind() != reflect.Ptr {
        return 0, fmt.Errorf("первый параметр должен быть указателем на структуру")
    }

    instanceType := instanceValue.Type().Elem()
    
    // Ищем поле по имени.
    field, found := instanceType.FieldByName(fieldName)
    if !found {
        return 0, fmt.Errorf("поле '%s' не найдено в структуре", fieldName)
    }

    return field.Offset, nil
}

// Изменяет поле y структуры Foo.
func changeFoo(f *Foo) {
    yOffset, err := getFieldOffset(f, "y")
    if err != nil {
        fmt.Println(err)
        return
    }
    
    // Получаем указатель на поле y с учетом смещения.
    y := (*Foo)(unsafe.Pointer(uintptr(unsafe.Pointer(f)) + yOffset))
    
    // Выполняем необходимые действия с полем y.
    // ...
}

Изменения и пояснения:

  1. Обработка ошибок: Добавил обработку ошибки в функции changeFoo. Теперь, если поле не найдено, ошибка будет выведена, и функция завершит работу.
  2. Вработал указатели: Использование (*Foo) вместо **Foo при преобразовании указателя с учетом смещения для более ясного понимания.
  3. Комментарии: Добавлены пояснительные комментарии для каждой важной части кода для улучшения понимания.

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

0

Я только начинаю портировать код с C++ на Go и столкнулся с парой классов, которые являются "друзьями" друг друга. Я почти уверен, что если они находятся в одном пакете, то по умолчанию они действительно являются друзьями.

Большая буква в начале идентификатора ограничена рамками пакета. Таким образом, они могут находиться в разных файлах, если находятся в одной директории, и имеют возможность видеть неэкспортируемые поля друг друга.

Использование reflect, даже если это стандартная библиотека Go, — это то, о чем всегда стоит задумываться. Это добавляет значительные накладные расходы во время выполнения. Решение, по сути, заключается в том, что вы можете просто скопировать и вставить структуры, которые вы хотите сделать друзьями — они должны быть в одной папке. В противном случае вам придется экспортировать их. (Лично я считаю, что опасения по поводу "риска" экспорта конфиденциальных данных сильно преувеличены, хотя если вы пишете библиотеку, не имеющую исполняемого файла, возможно, это имеет смысл, ведь пользователи библиотеки не увидят эти поля в GoDoc и, следовательно, не подумают, что могут полагаться на их существование).

0

Существует несколько "хака", чтобы достичь этого. Один из вариантов — использовать go:linkname. Другим решением может быть создание публичного сеттера и анализ стека вызовов.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь