Wątek przeniesiony 2021-09-16 09:51 z Inne języki programowania przez cerrato.

[GO] Funkcja zmienia wartość pola mimo przekazywania przez wartość

0

Cześć, w Go jestem nowy i trafiłem na niezrozumiałe dla mnie zachowanie. Ogólnie rozumiem to, że jak przekazujemy mapę przez wartość to i tak tam siedzi wskaźnik itp. Spoko. Ale czemu w moim przykładzie, mimo jawnego kopiowania wszystkiego, wartość bar.foo.m z maina jest zmieniana?
https://play.golang.org/p/2GMMxdf5HK_w

package main

import (
	"fmt"
)

type Foo struct {
	m map[int]struct{}
}

func NewFoo() Foo {
	return Foo{
		m: make(map[int]struct{}),
	}
}

func FooCopy(foo Foo) Foo {
	newFoo := NewFoo()
	for k, _ := range foo.m {
		newFoo.m[k] = struct{}{}
	}
	
	return newFoo
}

type Bar struct {
	foo Foo
}

func baz(bar Bar) {
	oldFoo := FooCopy(bar.foo)
	bar.foo.m[42] = struct{}{}
	bar.foo = oldFoo
}

func main() {
	var bar Bar
	bar.foo = NewFoo()
	baz(bar)
	fmt.Printf("%v", len(bar.foo.m)) // Wypisze 1
}
3

Ekspertem w Go nie jestem, ale zakładam, że z tego powodu, że map[] jest wewnętrznie mutowalny. Więc przekazujesz wartości przez kopię, ale map[] jest de facto wskaźnikiem (co sugeruje to, że by go zbudować potrzebujemy make). Widać to jak sobie wyprintujemy wskaźniki:

https://play.golang.org/p/5F9p-wxDP52

0

Po paru godzinach snu to skumałem. Jak się w baz dobieram do bar.foo.m, to to jest ta sama mapa co z maina, mimo, że strukturę skopiowaliśmy. Czyli:

func MapCopy(m map[int]struct{}) map[int]struct{} {
	newMap := make(map[int]struct{})
	for k, _ := range m {
		newMap[k] = struct{}{}
	}
	
	return newMap
}

func baz(bar Bar) {
	copiedMap := MapCopy(bar.foo.m)
	bar.foo.m[42] = struct{}{} // Wrzuca do mapy z maina bo operuje na wskaźniku z kopii, który dalej wskazuje na mapę z maina

	bar.foo.m = copiedMap // Zmienia wskaźnik w kopii. Mapa z maina nie będzie już zmieniana

// Wrzuca do nowej mapy.
	bar.foo.m[0] = struct{}{} 
	bar.foo.m[1] = struct{}{} 
	bar.foo.m[2] = struct{}{}
}

W sumie ma sens. Dziękuję (:

Od siebie dodam, że jest to design z d**y trochę. Zakładałem, że Go celuje, żeby być językiem, w którym nie martwisz się pamięcią, a tu takie coś. Z takim działaniem, żeby w jakiejś funkcji być pewnym, że nie zmienisz zewnętrznego stanu, musisz zrobić kopię samemu, mimo, że przyjmujesz parametr przez kopię. Dodaj do tego, że język nie udostępnia tak podstawowej funkcjonalności jak głębokiego kopiowania. += mapy i im podobne mogą nie być eksportowane z danej struktury, więc jeżeli pakiet nie ma metody do skopiowania struktury, to jesteś w lesie.

1

Mapa jest jest typem referencyjnym:

Map types are reference types, like pointers or slices, and so the value of m above is nil; it doesn't point to an initialized map.

https://blog.golang.org/maps

1

tak, mapa jest typem referencyjnym. Podobnie jak slice. Więc jak kopiesz mapę/slice/channel to kopiujesz samą strukturę, ale bez danych. Opisałem te zachowanie na przykładzie tablicy właśnie na blogu https://developer20.com/what-you-should-know-about-go-slices/.

type slice struct {
	array unsafe.Pointer
	len int
	cap int
}

Kopiując tę strukturę, to kopiujesz tylko te wartości co widzisz, lecz tablica z aktualnymi danymi nadal pozostaje jedna i obie kopie struktury wskazują na ten sam kawałek pamięci. Podobnie jest z mapami https://github.com/golang/tools/blob/master/go/types/typeutil/map.go#L26

type Map struct {
	hasher Hasher             // shared by many Maps
	table  map[uint32][]entry // maps hash to bucket; entry.key==nil means unused
	length int                // number of map entries
}
0
Dregorio napisał(a):

@no_solution_found: https://dave.cheney.net/2017/04/30/if-a-map-isnt-a-reference-variable-what-is-it No raczej nie referencyjnym :P
I dodatkowo https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go - choć to powinno czytać się pierwsze :P

Czepiasz się słówek, a kolega bardzo dobrze wytłumaczył.

1

@Dregorio: ja się zgodzę co do teorii, ale my mówimy o języku GO i w jego kontekście rozmawiamy, nawet linkowany przez Ciebie Dave Chaney, w artykule odnosi się do C++:

There is no pass-by-reference in Go:

(...)
In languages** like C++** you can declare an alias, or an alternate name to an existing variable. This is called a reference variable.
(...)
Unlike C++, each variable defined in a Go program occupies a unique memory location.

Ale, żeby nie było, ja się z nim zgadzam:

If a map isn’t a reference variable, what is it?:

(...)
A map value is a pointer to a runtime.hmap structure.
(...)

I tak pojmuje typy referencyjne w GO, zresztą nie tylko ja:

Go maps in action:

(...)
Map types are reference types, like pointers or slices, and so the value of m above is nil; it doesn't point to an initialized map.
(...)

Normalnie bym olal Twój post, ale polska sieć jest tak uboga w materiały o GO, że wprowadzanie takiego szumu uważam za szkodliwe. Dużo fajniej byłoby gdybyś sypnął przykładem np. takim:

package main

import (
	"fmt"
)

func main() {
	m := map[string]int{"originalMap": 1}
	reallocate(m)
	fmt.Println(m) // prints originalMap: 1
	reallocatePtrWrong(&m)
	fmt.Println(m) // prints originalMap: 1
	reallocatePtr(&m)
	fmt.Println(m) // prints overrideMap: 1
	extend(m)
	fmt.Println(m) // prints overrideMap: 1 extended: 2
}

func reallocate(m map[string]int) {
	// Orginal value of m outside function not changed.
	m = map[string]int{"overrideMap": 1}
}

func extend(m map[string]int) {
	// Chaging m without realocation works, and don't need a pointer.
	m["extended"] = 2
}

func reallocatePtrWrong(m *map[string]int) {
	// Realocate using pointer don't become visible outisde if we
	// only update the poniter value.
	m = &map[string]int{"overrideMap": 1}
}

func reallocatePtr(m *map[string]int) {
	// Realocate using pointer work if we do it right.
	*m = map[string]int{"overrideMap": 1}
}

źródło: https://stackoverflow.com/questions/40680981/are-maps-passed-by-value-or-by-reference-in-go

Lub wyjaśnił, ża na początku mapy w GO były zapisywane w taki sposób: *map[int]int, co nie wprowadzałoby niepotrzebnego zamieszania.
Natomiast dywagacje o "prawdziwych" referencjach pozostawił w komentarzach

1 użytkowników online, w tym zalogowanych: 0, gości: 1