5

As far as I understand, types slice and map are similar in many ways in Go. They both reference (or container) types. In terms of abstract data types, they represent an array and an associative array, respectively.

However, their behaviour is quite different.

var s []int
var m map[int]int

While we can use a declared slice immediately (append new items or reslice it), we cannot do anything with a newly declared map. We have to call make function and initialize a map explicitly. Therefore, if some struct contains a map we have to write a constructor function for the struct.

So, the question is why it is not possible to add some syntaсtic sugar and both allocate and initialize the memory when declaring a map.

I did google the question, learnt a new word "avtovivification", but still failing to see the reason.

I am not talking about struct literal. Yes, you can explicitly initialize a map by providing values such as m := map[int]int{1: 1}. However, if you have some struct:

package main

import (
    "fmt"
)

type SomeStruct struct {
    someField map[int]int
    someField2 []int
}

func main() {
    s := SomeStruct{}
    s.someField2 = append(s.someField2, -1) // OK
    s.someField[0] = -1 // panic: assignment to entry in nil map
    fmt.Println(s)
}

It is not possible to use a struct immediately (with default values for all fields). One has to create a constructor function for SomeStruct which has to initialize a map explicitly.

Community
  • 1
  • 1
John Snow
  • 339
  • 4
  • 17
  • 2
    That "syntactical sugar" is already there but it is not sugar but plain simple Go object literal syntax: {}`. Both--slices and maps--behave exactly the same. – Volker Jan 10 '19 at 08:26
  • @Volker as far as understand, they are not behaving the same way. Could you please have a look at the example I added to the question? – John Snow Jan 10 '19 at 08:58
  • 3
    They do behave exactly the same. If you try to assign to any element of a nil slice you get a panic too. What you perceive as a difference in the behavior of nil slices and nil maps is a _special_ _property_ of the append function which allows its first argument to be nil. There is no append for maps but this is not a difference on how nil slices and nil maps behave. If you write your own appendMap function you can treat a nil map special too. – Volker Jan 10 '19 at 09:39
  • @Volker, ок, thank you for the clarification. Now I see that they behave exactly the same. Maybe I should reframe the question. Why do they both behave as they behave? You mentioned that a `special property` allows `append` to deal with nil slices. But why instead we do not initialize both map and slice during the declaration? Anyway in 99% of cases we *eventually* will initialize the variable (map or slice). In this case, we do not need any special properties. – John Snow Jan 10 '19 at 10:59
  • 1
    Initializing during declaration is undoable in practice. Take slices. How long would you like your slice be? At declaration time you cannot know so it must be 0. How large should the capacity be? You do not know and any default like 17 will destroy the ability to initialize a slice with the right capacity to avoid reallocation. If you want a constructed non-nil slice or map just add {}. – Volker Jan 10 '19 at 11:07
  • 1
    @Volker But how this behaviour is different from `m := make(map[int]int)`? As far as I understand, the map is also created with some non-nil default capacity. – John Snow Jan 10 '19 at 11:39
  • I do not understand your last question. A slice made like `make(map[int]int, 0)` has capacity of zero, just like one made with `[]int{}`. For maps there might or might not be a difference in capacity for `make(map[int]int)` and `map[int]int{}` as this is unspecified by the language spec and any Go version might do something different. I am not sure what actual problem you are trying to understand. – Volker Jan 10 '19 at 13:06
  • Questions of language design decisions can only be answered by language designers, not by the community at large. – Adrian Jan 10 '19 at 14:21

2 Answers2

14

While we can use a declared slice immediately (append new items or reslice it), we cannot do anything with a newly declared map. We have to call make function and initialize a map explicitly. Therefore, if some struct contains a map we have to write a constructor function for the struct.

That's not true. Default value–or more precisely zero value–for both slices and maps is nil. You may do the "same" with a nil map as you can do with a nil slice. You can check length of a nil map, you can index a nil map (result will be the zero value of the value type of the map), e.g. the following are all working:

var m map[int]int

fmt.Println(m == nil) // Prints true
fmt.Println(len(m))   // Prints 0
fmt.Println(m[2])     // Prints 0

Try it on the Go Playground.

What you "feel" more about the zero-value slice is that you may add values to it. This is true, but under the hood a new slice will be allocated using the exact make() builtin function that you'd have to call for a map in order to add entries to it, and you have to (re)assign the returned slice. So a zero-value slice is "no more ready for use" than a zero-value map. append() just takes care of necessary (re)allocation and copying over. We could have an "equivalent" addEntry() function to which you could pass a map value and the key-value pairs, and if the passed map is nil, it could allocate a new map value and return it. If you don't call append(), you can't add values to a nil slice, just as you can't add entries to a nil map.

The primary reason that the zero value for slices and maps is nil (and not an initialized slice or map) is performance and efficiency. It is very often that a map or slice value (either variable or a struct field) will never get used, or not right away, and so if they would be allocated at declaration, that would be a waste of memory (and some CPU) resources, not to mention it gives more job to the garbage collector. Also if the zero value would be an initialized value, it would often be insufficient (e.g. a 0-size slice cannot hold any elements), and often it would be discarded as you add new elements to it (so the initial allocation would be a complete waste).

Yes, there are cases when you do want to use slices and maps right away, in which cases you may call make() yourself, or use a composite literal. You may also use the special form of make() where you supply the (initial) capacity for maps, avoiding future restructuring of the map internals (which usually requires non-negligible computation). An automatic non-nil default value could not guess what capacity you'd require.

icza
  • 389,944
  • 63
  • 907
  • 827
  • as always thank you for the great answer. Maybe I wrong, but I thought that when you declare variable `var m map[int]int`, the memory is already allocated, but not initialized? – John Snow Jan 10 '19 at 09:31
  • 1
    @AyZu Map values internally are pointers (see [slice vs map to be used in parameter](https://stackoverflow.com/questions/47590444/slice-vs-map-to-be-used-in-parameter/47590531#47590531)). A map declaration claims memory for just a pointer (which will point to nowhere), but nothing more will be allocated, not until `make()` is called or a composite literal is used. – icza Jan 10 '19 at 09:33
3

You can! What you're looking for is:

package main

import "fmt"

func main() {
    v := map[int]int{}

    v[1] = 1
    v[2] = 2

    fmt.Println(v)
}

:= is declare and assign, where as var is simply declare.

syntaqx
  • 2,636
  • 23
  • 29