СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Передача типа map в функцию в черновиках

Go
Недавно проскакивала статья о том, как устроены разные простые типы и слайсы в памяти. Из этой статьи мы узнали, почему переданный «по значению» слайс в функцию является передачей слайса по ссылке только до того момента, пока слайс внутри функции не потребует реаллокацию в памяти при увеличении своего capacity. Если внутри функции capacity этого слайса изменяется, и он был передан «по значению», а не в виде указателя, то слайс начинает ссылаться на совсем другой массив, совсем не тот, который будет дальше использоваться в вызывающей функции.

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

У меня возник вопрос, а нет ли похожей ситуации с типом map? Ведь у него тоже есть capacity, и он тоже может менять аллокацию в памяти при росте числа пар значений.

И я провел небольшой эксперимент, написав такой код:

package main

import (
	"fmt"
)

type myMap map[string]string

func main() {

	mymap := make(myMap, 1)
	mymap["firstKey"] = "firstValue"
	fmt.Printf("Init method nop: Address = %p Len = %d\n", &mymap, len(mymap))
	mymap.grow()
	fmt.Printf("Growed method nop: Address = %p Len = %d\n", &mymap, len(mymap))

	mymap = make(myMap, 1)
	mymap["firstKey"] = "firstValue"
	fmt.Printf("Init method p: Address = %p Len = %d\n", &mymap, len(mymap))
	(&mymap).growp()
	fmt.Printf("Growed method p: Address = %p Len = %d\n", &mymap, len(mymap))

	mymap = make(myMap, 1)
	mymap["firstKey"] = "firstValue"
	fmt.Printf("Init func nop: Address = %p Len = %d\n", &mymap, len(mymap))
	fgrow(mymap)
	fmt.Printf("Growed func nop: Address = %p Len = %d\n", &mymap, len(mymap))

	mymap = make(myMap, 1)
	mymap["firstKey"] = "firstValue"
	fmt.Printf("Init func p: Address = %p Len = %d\n", &mymap, len(mymap))
	fgrowp(&mymap)
	fmt.Printf("Growed func p: Address = %p Len = %d\n", &mymap, len(mymap))

}

func (m myMap) grow() {
	for i := 1; i < 1000000; i++ {
		m[fmt.Sprintf("nopAddKey%d", i)] = fmt.Sprintf("%d", i)
	}
}

func (m *myMap) growp() {
	for i := 1; i < 1000000; i++ {
		(*m)[fmt.Sprintf("pAddKey%d", i)] = fmt.Sprintf("%d", i)
	}
}

func fgrow(m myMap) {
	for i := 1; i < 1000000; i++ {
		m[fmt.Sprintf("nopAddKey%d", i)] = fmt.Sprintf("%d", i)
	}
}

func fgrowp(m *myMap) {
	for i := 1; i < 1000000; i++ {
		(*m)[fmt.Sprintf("pAddKey%d", i)] = fmt.Sprintf("%d", i)
	}
}

Здесь я определил два метода и две функции роста мапы, по значению и по указателю. Результатом выполнения я получил такой результат:
Init method nop: Address = 0xc042054018 Len = 1
Growed method nop: Address = 0xc042054018 Len = 1000000
Init method p: Address = 0xc042054018 Len = 1
Growed method p: Address = 0xc042054018 Len = 1000000
Init func nop: Address = 0xc042054018 Len = 1
Growed func nop: Address = 0xc042054018 Len = 1000000
Init func p: Address = 0xc042054018 Len = 1
Growed func p: Address = 0xc042054018 Len = 1000000


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

комментарии (20)

0
+1 –1
asnelzin ,  
На самом деле, не по ссылке: There is no pass-by-reference in Go
–3
JekaMas ,  
Да! Странно, что автор статьи не исследовал вопрос, о котором решил писать.
Map — это структура, передается по значению. Собственно поэтому возможны вот такие вещи.
0
kilgur ,  
Позволил себе переделать ваш пример. Со слайсами такие вещи тоже работают.
pfihr, map просто более сложная структура, чем slice. Если я правильно понимаю, map расширяется «бакетами», т.е. у него нет надобности копировать содержимое в новую память. Слайсу же необходима непрерывность, поэтому слайс расширяется выделением бОльшего блока и копированием старых данных в новый кусок памяти.
0
JekaMas ,  
Все так, за счет бакетов есть возможность расширяться без создания новой структуры map.
То же самое справедливо и в отношении каналов. Только с учетом того, что его вместимость мы определяем сразу и не можем поменять, не создавая новый канал.
0
pfihr ,  

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

0
JekaMas ,  
Если бы map передавался по указателю, то в обоих бы примерах он менялся для вызвавшего функцию кода. Этого не происходит, потому что происходит передача по значению.
Как верно сказали ниже, map содержит указатель, но и он передается по значению.
0
Forked ,  
В Вашем примере обычный шадоуинг: переопределена переменная внутри функции, поэтому переданный параметр остался неизменным. Исправленный пример. Map передается также, как и все другие структуры: указатель для простой передачи не нужен.
0
JekaMas ,  
И все же нет, просто мапа передалась по значению, фактически был скопирован uintptr, внутри функции был присвоен новый, но так как была передача по значению, то внешний код о новом указателе не узнает.
0
pfihr ,  

Maps, like channels, but unlike slices, are just pointers to runtime types. As you saw above, a map is just a pointer to a runtime.hmap structure.


Maps have the same pointer semantics as any other pointer value in a Go program. There is no magic save the rewriting of map syntax by the compiler into calls to functions in runtime/hmap.go.

0
JekaMas ,  
Кстати, что map, chan не указатели, а структуры, содержащие в том числе указатели, указывает и то, что они создаются через make.
0
kilgur ,   * (был изменён)
Dave Cheney с вами не согласен. Map все-таки указатель, в отличие от slice, хотя оба создаются с помощью make.
var m map[int]int
var s []int
var p uintptr
fmt.Println(unsafe.Sizeof(m), unsafe.Sizeof(s), unsafe.Sizeof(p)) // 8 24 8 (linux/amd64)
0
JekaMas ,  
Если речь про uintptr, то да. Если же в чисто гошном смыле *map, то нет.
Думаю все же неверно говорить о том, что это указатель. Map не ведет себя как *map, пример я приводил выше. Если ему есть опровержение, то да — map это указатель.
Согласен с вами, если утверждение звучит как «map хранит uintptr значение, то есть с ним есть возможность работать, как с указателем в определенных условиях».
0
kilgur ,  
Там (по ссылке) объясняется почему так. В чисто гошном смысле *map в какой-то момент просто переименовали в map, п.ч. указатель, который не выглядит как указатель, смущает гораздо меньше, чем указатель, который нельзя разыменовать. Т.е. получить структуру мапа в переменную не получится, поэтому ваш пример не корректен. Например:
func someFunc(x *int) {
	a := 5
	x = &a
}
//...
b := 10
someFunc(b) // и что-то b не заменилось на 5

А должно? Вы в своем примере получаете параметром просто map, представьте на секунду, что это все-таки указатель, но получить его значение вы не можете. И? Толку от присваивания переменной с указателем make(map[...]..., 1). А к значению у вас доступа нет… вот и приходится «городить» указатель на указатель, чтобы менять исходное значение переданной переменной.
0
JekaMas ,  
Более корректный прример относительно слов Дейва https://play.golang.org/p/aSEP5pd8LH
Если мы используем мапу и ее код гошки заменяет на разыменование, то код должен быть такой и тогда внутри функции мы можем сделать такое присваивание, однако поведение map отличается от такого, где есть разыменование.
0
kilgur ,  
Заголовок вводит в заблуждение — в Go параметры передаются всегда по значению, т.е. копируются. Да, map — это указатель на структуру, но этот указатель один фиг передается копированием.
0
pfihr ,  

все верно, цель заметки была в том, чтобы показать, что изменение мапы в функции не равно изменению слайса

0
kilgur ,  
Передайте указатель на слайс и найдите 6 отличий.
Указатели и передача параметра по ссылке — это разные вещи. Например, в си вы можете передать параметр по ссылке, а можете (как в Go) передать по значению указатель на переменную. Передача параметра по ссылке нужна, когда вызываемая подпрограмма должна изменить переменную вызывающей подпрограммы. Грань довольно тонкая в сравнении с передачей указателя, я не могу придумать сходу кейс, где «по ссылке» действительно необходимо и нельзя обойтись передачей параметров по значению.
Может лучше озаглавить статью как-то так: «Тип map — ссылочный»?
0
pfihr ,  
Согласен, использовал некорректный термин. Внес правки.
0
kilgur ,  
Спасибо
0
kamilsk ,