Такая особенность слайса может порождать «случайные» ошибки логики работы программы на этапе выполнения, если программист не учел это.
У меня возник вопрос, а нет ли похожей ситуации с типом 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)
pfihr
23.05.2017 16:05Maps, 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.
JekaMas
23.05.2017 19:11Кстати, что map, chan не указатели, а структуры, содержащие в том числе указатели, указывает и то, что они создаются через make.
kilgur
23.05.2017 19:35Dave 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)
JekaMas
23.05.2017 20:55Если речь про uintptr, то да. Если же в чисто гошном смыле *map, то нет.
Думаю все же неверно говорить о том, что это указатель. Map не ведет себя как *map, пример я приводил выше. Если ему есть опровержение, то да — map это указатель.
Согласен с вами, если утверждение звучит как «map хранит uintptr значение, то есть с ним есть возможность работать, как с указателем в определенных условиях».kilgur
23.05.2017 21:21Там (по ссылке) объясняется почему так. В чисто гошном смысле *map в какой-то момент просто переименовали в map, п.ч. указатель, который не выглядит как указатель, смущает гораздо меньше, чем указатель, который нельзя разыменовать. Т.е. получить структуру мапа в переменную не получится, поэтому ваш пример не корректен. Например:
func someFunc(x *int) { a := 5 x = &a } //... b := 10 someFunc(b) // и что-то b не заменилось на 5
А должно? Вы в своем примере получаете параметром просто map, представьте на секунду, что это все-таки указатель, но получить его значение вы не можете. И? Толку от присваивания переменной с указателем make(map[...]..., 1). А к значению у вас доступа нет… вот и приходится «городить» указатель на указатель, чтобы менять исходное значение переданной переменной.JekaMas
23.05.2017 22:16Более корректный прример относительно слов Дейва https://play.golang.org/p/aSEP5pd8LH
Если мы используем мапу и ее код гошки заменяет на разыменование, то код должен быть такой и тогда внутри функции мы можем сделать такое присваивание, однако поведение map отличается от такого, где есть разыменование.
kilgur
23.05.2017 16:33Заголовок вводит в заблуждение — в Go параметры передаются всегда по значению, т.е. копируются. Да, map — это указатель на структуру, но этот указатель один фиг передается копированием.
pfihr
23.05.2017 19:15все верно, цель заметки была в том, чтобы показать, что изменение мапы в функции не равно изменению слайса
kilgur
23.05.2017 19:28Передайте указатель на слайс и найдите 6 отличий.
Указатели и передача параметра по ссылке — это разные вещи. Например, в си вы можете передать параметр по ссылке, а можете (как в Go) передать по значению указатель на переменную. Передача параметра по ссылке нужна, когда вызываемая подпрограмма должна изменить переменную вызывающей подпрограммы. Грань довольно тонкая в сравнении с передачей указателя, я не могу придумать сходу кейс, где «по ссылке» действительно необходимо и нельзя обойтись передачей параметров по значению.
Может лучше озаглавить статью как-то так: «Тип map — ссылочный»?
kamilsk
23.05.2017 22:25
asnelzin
На самом деле, не по ссылке: There is no pass-by-reference in Go
JekaMas
Да! Странно, что автор статьи не исследовал вопрос, о котором решил писать.
Map — это структура, передается по значению. Собственно поэтому возможны вот такие вещи.
kilgur
Позволил себе переделать ваш пример. Со слайсами такие вещи тоже работают.
pfihr, map просто более сложная структура, чем slice. Если я правильно понимаю, map расширяется «бакетами», т.е. у него нет надобности копировать содержимое в новую память. Слайсу же необходима непрерывность, поэтому слайс расширяется выделением бОльшего блока и копированием старых данных в новый кусок памяти.
JekaMas
Все так, за счет бакетов есть возможность расширяться без создания новой структуры map.
То же самое справедливо и в отношении каналов. Только с учетом того, что его вместимость мы определяем сразу и не можем поменять, не создавая новый канал.
pfihr
при этом, значения в этой структуре не меняются при изменении содержания и его реаллокации в памяти. В Вашем примере вы переопределяете переменную в рамках области видимости, это не связано со свойствами мапы.
JekaMas
Если бы map передавался по указателю, то в обоих бы примерах он менялся для вызвавшего функцию кода. Этого не происходит, потому что происходит передача по значению.
Как верно сказали ниже, map содержит указатель, но и он передается по значению.
Forked
В Вашем примере обычный шадоуинг: переопределена переменная внутри функции, поэтому переданный параметр остался неизменным. Исправленный пример. Map передается также, как и все другие структуры: указатель для простой передачи не нужен.
JekaMas
И все же нет, просто мапа передалась по значению, фактически был скопирован uintptr, внутри функции был присвоен новый, но так как была передача по значению, то внешний код о новом указателе не узнает.