Недавно проскакивала статья о том, как устроены разные простые типы и слайсы в памяти. Из этой статьи мы узнали, почему переданный «по значению» слайс в функцию является передачей слайса по ссылке только до того момента, пока слайс внутри функции не потребует реаллокацию в памяти при увеличении своего 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)


  1. asnelzin
    23.05.2017 13:48

    На самом деле, не по ссылке: There is no pass-by-reference in Go


    1. JekaMas
      23.05.2017 14:35
      -3

      Да! Странно, что автор статьи не исследовал вопрос, о котором решил писать.
      Map — это структура, передается по значению. Собственно поэтому возможны вот такие вещи.


      1. kilgur
        23.05.2017 15:30

        Позволил себе переделать ваш пример. Со слайсами такие вещи тоже работают.
        pfihr, map просто более сложная структура, чем slice. Если я правильно понимаю, map расширяется «бакетами», т.е. у него нет надобности копировать содержимое в новую память. Слайсу же необходима непрерывность, поэтому слайс расширяется выделением бОльшего блока и копированием старых данных в новый кусок памяти.


        1. JekaMas
          23.05.2017 15:36

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


      1. pfihr
        23.05.2017 15:53

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


        1. JekaMas
          23.05.2017 16:59

          Если бы map передавался по указателю, то в обоих бы примерах он менялся для вызвавшего функцию кода. Этого не происходит, потому что происходит передача по значению.
          Как верно сказали ниже, map содержит указатель, но и он передается по значению.


      1. Forked
        24.05.2017 06:21

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


        1. JekaMas
          24.05.2017 10:45

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


  1. pfihr
    23.05.2017 16:05

    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.


    1. JekaMas
      23.05.2017 19:11

      Кстати, что map, chan не указатели, а структуры, содержащие в том числе указатели, указывает и то, что они создаются через make.


      1. kilgur
        23.05.2017 19:35

        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)
        


        1. JekaMas
          23.05.2017 20:55

          Если речь про uintptr, то да. Если же в чисто гошном смыле *map, то нет.
          Думаю все же неверно говорить о том, что это указатель. Map не ведет себя как *map, пример я приводил выше. Если ему есть опровержение, то да — map это указатель.
          Согласен с вами, если утверждение звучит как «map хранит uintptr значение, то есть с ним есть возможность работать, как с указателем в определенных условиях».


          1. 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). А к значению у вас доступа нет… вот и приходится «городить» указатель на указатель, чтобы менять исходное значение переданной переменной.


            1. JekaMas
              23.05.2017 22:16

              Более корректный прример относительно слов Дейва https://play.golang.org/p/aSEP5pd8LH
              Если мы используем мапу и ее код гошки заменяет на разыменование, то код должен быть такой и тогда внутри функции мы можем сделать такое присваивание, однако поведение map отличается от такого, где есть разыменование.


  1. kilgur
    23.05.2017 16:33

    Заголовок вводит в заблуждение — в Go параметры передаются всегда по значению, т.е. копируются. Да, map — это указатель на структуру, но этот указатель один фиг передается копированием.


    1. pfihr
      23.05.2017 19:15

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


      1. kilgur
        23.05.2017 19:28

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


        1. pfihr
          23.05.2017 22:26

          Согласен, использовал некорректный термин. Внес правки.


          1. kilgur
            23.05.2017 22:47

            Спасибо