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

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

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

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

            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
              Кстати, что map, chan не указатели, а структуры, содержащие в том числе указатели, указывает и то, что они создаются через make.
                0
                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
                  Если речь про uintptr, то да. Если же в чисто гошном смыле *map, то нет.
                  Думаю все же неверно говорить о том, что это указатель. Map не ведет себя как *map, пример я приводил выше. Если ему есть опровержение, то да — map это указатель.
                  Согласен с вами, если утверждение звучит как «map хранит uintptr значение, то есть с ним есть возможность работать, как с указателем в определенных условиях».
                    0
                    Там (по ссылке) объясняется почему так. В чисто гошном смысле *map в какой-то момент просто переименовали в map, п.ч. указатель, который не выглядит как указатель, смущает гораздо меньше, чем указатель, который нельзя разыменовать. Т.е. получить структуру мапа в переменную не получится, поэтому ваш пример не корректен. Например:
                    func someFunc(x *int) {
                    	a := 5
                    	x = &a
                    }
                    //...
                    b := 10
                    someFunc(b) // и что-то b не заменилось на 5
                    

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

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

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

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

                Самое читаемое