Golang копирует структуры, содержащие указатели

TL; DR Из-за отрицательных голосов и отсутствия ответов и комментариев я предполагаю, что TL; DR необходим.

Как я могу создать структуру в golang, содержащую указатель, а затем безопасно передать ее по значению другим функциям? (Под безопасным я подразумеваю, не беспокоясь о том, что эти функции могут разыменовывать указанный указатель и изменять переменную, на которую он указывает).

А ТАКЖЕ

Если вы собираетесь дать ответ «Функции копирования», то как я могу удалить исходный конструктор/оператор копирования? Переопределить его с помощью пользовательской функции копирования? Или иным образом отговорить людей от его использования?


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

Я также могу передавать экземпляры этих структур функциям, которые их «копируют».

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

import (
        "fmt"
)

type A struct {
        a * int
}

func main() {
        var instance A
        value := 14
        instance.a = &value
        fmt.Println(*instance.a) // prints 14
        mutator(instance)
        fmt.Println(*instance.a) // prints 11 o.o
}

func mutator(instance A) {
        *instance.a = 11
        fmt.Println(*instance.a)
}

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

Вполне разумно также, что после вызова функции «мутатор» программист может захотеть продолжить использование своего экземпляра A и (предполагая, что он не обязательно кодировал структуру или знал о ее внутренностях) мог даже предположить, что, поскольку он передал копию, а не указатель, его экземпляр A останется неизменным.

Теперь есть несколько (3) популярных языков, которые позволяют программисту думать о выделении и управлении памятью, которые не являются golang. Я не знаю ни Rust, ни C, поэтому воздержусь от того, как бы я решил эту проблему на C++:

а) Предполагая, что я был разработчиком класса A, я мог бы создать конструктор копирования, что привело бы к следующему коду:

#include <iostream>

    class A {
    public:
            int * a;
            A(int value): a(new int{value}) {}
            A(const A & copyFrom): a(new int{*copyFrom.a}) {}
    };

    void mutator(A instance) {
            *instance.a = 11;
            std::cout << *instance.a << "\n";
    }


    int main() {
            A instance{14};
            std::cout << *(instance.a) << "\n";
            mutator(instance);
            std::cout << *instance.a << "\n";
    }

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

б) Предполагая, что я был разработчиком класса A и не хотел создавать конструктор копирования (скажем, что бы ни указывало на, может быть очень большим или что A часто используется в критических условиях производительности как объект только для чтения), но хотел убедитесь, что любое назначение для копирования не может изменить значение, на которое указывает a (но все же разрешить людям изменять a, присваивая ему новое значение). Я мог бы написать свой класс следующим образом:

class A {
public:
        const int * a;
        A(int value): a(new const int{value}) {}
};

Что приведет к тому, что следующий код не сможет скомпилироваться:

void mutator(A instance) {
        *instance.a = 11;
        std::cout << *instance.a << "\n";
}


int main() {
        A instance{14};
        std::cout << *(instance.a) << "\n";
        mutator(instance);
        std::cout << *instance.a << "\n";
}

Но следующий код скомпилируется просто отлично:

void mutator(A instance) {
        instance.a = new const int{11};
        std::cout << *instance.a << "\n";
}


int main() {
        A instance{14};
        std::cout << *(instance.a) << "\n";
        mutator(instance);
        std::cout << *instance.a << "\n";
}

Имейте в виду, что это типично для "объектно-ориентированного" дизайна C++ (э-э-э). Я бы предпочел, чтобы в сигнатуре функции было какое-то правило, которое гарантировало бы отсутствие модификации экземпляра A, переданного ей, или метод, с помощью которого можно объявить экземпляр A "константным" и "защитить" его динамически. выделенные поля (не только статические) от повторного назначения.

Однако, хотя решение может быть не идеальным, это решение. Это позволяет мне иметь четкое представление о «владении» моими экземплярами A.

В golang кажется, что любая «копия» экземпляра, содержащая указатели, в основном бесплатна для всех, ее нельзя безопасно передавать, даже если у автора структуры было такое намерение.

Единственное, о чем я могу думать, это иметь метод «Копировать», который возвращает совершенно новый экземпляр структуры (аналогичный конструктору копирования в приведенном выше примере). Но без возможности удалить конструктор/оператор копирования было бы трудно убедиться, что люди будут его использовать и/или заметят.

Честно говоря, мне кажется довольно странным, что golang даже позволяет перезаписать адрес памяти указателя без использования «небезопасного» пакета или чего-то подобного.

Не было бы разумнее просто запретить этот тип операций, как и многие другие?

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

Я упускаю из виду способ создания копии (или запрета копирования) в golang? Действительно ли первоначальное намерение авторов заключалось в поощрении переназначения, а не мутации, когда это позволяют память и время? Если да, то почему так легко изменять динамически выделяемые переменные? Есть ли способ имитировать поведение private/public со структурами или файлами, а не с полноценными пакетами? Есть ли другой способ обеспечить некоторое подобие владения структурами, имеющими указатели, которые я упускаю из виду?


person George    schedule 25.04.2017    source источник
comment
Пожалуйста, отредактируйте вопрос, чтобы ограничить его конкретной проблемой с достаточной детализацией, чтобы найти адекватный ответ. Не задавайте сразу несколько разных вопросов.   -  person peterSO    schedule 26.04.2017
comment
В Go это предельно просто: люди, использующие ваш пакет, не могут получить доступ к неэкспортированным идентификаторам. То есть они фактически закрыты с точки зрения C++. Если вы предоставляете способ доступа к указателю в структуре (например, метод, который возвращает такой указатель), то вы либо копируете его данные и возвращаете указатель на копию, либо просто доверяете пользователю вашего пакета, чтобы не сломать вещи . Есть аргументы для обеих сторон, но в конечном итоге это зависит от вашего варианта использования.   -  person    schedule 28.04.2017


Ответы (1)


Как я могу создать структуру в golang, содержащую указатель, а затем безопасно передать ее по значению другим функциям? (Под безопасным я подразумеваю, не беспокоясь о том, что эти функции могут разыменовывать указанный указатель и изменять переменную, на которую он указывает).

Используйте экспортированный тип пакета с неэкспортированными полями. Например,

src/ptrstruct/ptrstruct.go:

package ptrstruct

type PtrStruct struct {
    pn *int
}

func New(n int) *PtrStruct {
    return &PtrStruct{pn: &n}
}

func (s *PtrStruct) N() int {
    return *s.pn
}

func (s *PtrStruct) SetN(n int) {
    *s.pn = n
}

func (s *PtrStruct) Clone() *PtrStruct {
    // make a deep clone
    t := &PtrStruct{pn: new(int)}
    *t.pn = *s.pn
    return t
}

src/ptrstruct.go:

package main

import (
    "fmt"

    "ptrstruct"
)

func main() {
    ps := ptrstruct.New(42)
    fmt.Println(ps.N())
    pc := ps.Clone()
    fmt.Println(pc.N())
    pc.SetN(7)
    fmt.Println(pc.N())
    fmt.Println(ps.N())
}

Выход:

src $ go run ptrstruct.go
42
42
7
42
src $ 

Если вы собираетесь дать ответ «Функции копирования», то как я могу удалить исходный конструктор/оператор копирования? Переопределить его с помощью пользовательской функции копирования? Или иным образом отговорить людей от его использования?

Прекратите программировать на C++; начать программировать на Go. По замыслу Go — это не C++.

«Конструктор/оператор копирования» и «Переопределить его с помощью пользовательской функции» - это концепции С++.

Использованная литература:

Спецификация языка программирования Go

Блоки

Объявления и область применения

Экспортированные идентификаторы

person peterSO    schedule 26.04.2017
comment
У этого решения есть несколько проблем. а) Это означает, что я должен объявить структуру в отдельном пакете. б) Я полагаюсь на то, что пользователь знает, что он не должен передавать структуру по значению и вместо этого использовать метод копирования/клонирования... Я надеялся, что будет хак, чтобы переопределить или, по крайней мере, удалить конструктор копирования по умолчанию. ... но, похоже, его нет, поэтому при отсутствии лучшего ответа через 1-2 дня я отмечу этот ответ - person George; 28.04.2017
comment
Кроме того, извините за использование концепций C++, но я действительно не знаю семантики go или даже C, и, в конце концов, в C++ есть все концепции, которые есть в Go, поэтому легко сопоставить функции Go с C++, даже если GO может иметь другое имя для конструктора/операции копирования... это все еще концепция, которая существует под каким-то именем в языке, и я предполагаю, что достаточно легко связать их. - person George; 28.04.2017