Zero value of the map

3141 views
Skip to first unread message

Tad Vizbaras

unread,
Apr 18, 2017, 8:04:16 AM4/18/17
to golang-nuts
Making zero value useful is great idea. It works for slice, mutex and many others.

var a []int
a = append(a, 1)
fmt.Println(a)

This works and prints:

[1]

Sadly maps do not seem to have useful zero value. They require call to make() or literal to get to useful state.

var a map[int]int
a[1] = 1  // Fails with panic.
// panic: assignment to entry in nil map

Zero value maps are read-only. 

var a map[int]int
fmt.Println(len(a))
// Works and prints 0.

The argument could be that slices are read-only too. Just that "append" is special and it makes zero value slices useful.
var a []int
a[0] = 1 // Fails.
// panic: runtime error: index out of range

I am just curious what is the reason behind not making zero maps more useful? Is it space?

 

Ian Davis

unread,
Apr 18, 2017, 8:09:11 AM4/18/17
to golan...@googlegroups.com

On Tue, 18 Apr 2017, at 01:04 PM, Tad Vizbaras wrote:

The argument could be that slices are read-only too. Just that "append" is special and it makes zero value slices useful.
var a []int
a[0] = 1 // Fails.
// panic: runtime error: index out of range

I am just curious what is the reason behind not making zero maps more useful? Is it space?

A nitpick: slices are always read only. append creates a new slice, it doesn't modify the original.

In that respect map and slice zero value behaviours are similar.

Ian

Jesse McNelis

unread,
Apr 18, 2017, 10:03:38 AM4/18/17
to Tad Vizbaras, golang-nuts
On Tue, Apr 18, 2017 at 10:04 PM, Tad Vizbaras <eta...@gmail.com> wrote:
> I am just curious what is the reason behind not making zero maps more
> useful? Is it space?

The problem is that maps are mutable.

var a map[int]int
a[1] = 1 // could be fine and useful.

var a map[int]int
someFunc(a) // Does someFunc need to return the map or not?

If someFunc() allocates the map(because it got nil) by adding
something to it then it needs to return the map it allocated to it's
caller.
But if it didn't need to allocate because it got a valid map it
doesn't need to return the map.

The possibility that a function might be passed a nil map would then
mean that all functions that work with maps would need to always
return the map.











--
=====================
http://jessta.id.au

Ian Lance Taylor

unread,
Apr 18, 2017, 10:16:25 AM4/18/17
to Jesse McNelis, Tad Vizbaras, golang-nuts
And another reason is that it's very convenient for the zero value for
all types to be literally a sequence of zero bytes. We could never
figure out how to preserve that property while not requiring the make
call for map values. In the very early days what we call maps now
were written as pointers, so you wrote *map[int]int. We moved away
from that when we realized that no one ever wrote `map` without
writing `*map`. That simplified many things but it left this issue
behind as a complication.

The most plausible fix that I know for this is to invent a map
function that is similar to the append function, but I haven't yet
seen a good design for that.

Ian

Chris Hopkins

unread,
Apr 18, 2017, 10:28:09 AM4/18/17
to golang-nuts
I'm not sure what you mean by the append doesn't modify the original. Append will use the same backing store (if there is available capacity in it) and by definition the address of the slice in question must be invariant across its context. e.g.: https://play.golang.org/p/lBRpKSo-9P
I think of a slice as a built in structure so the pointer to the backing store will stay the same, the capacity will stay the same, only the length will change with append. (unless there is insufficient capacity then all bets are off except that again the original structure will remain, but all the values in it will be overwritten, then copied back into the original struct)

Or have I really misunderstood what happens under the hood here?

Ian Davis

unread,
Apr 18, 2017, 10:41:13 AM4/18/17
to golan...@googlegroups.com

On Tue, 18 Apr 2017, at 03:20 PM, Chris Hopkins wrote:
I'm not sure what you mean by the append doesn't modify the original. Append will use the same backing store (if there is available capacity in it) and by definition the address of the slice in question must be invariant across its context. e.g.: https://play.golang.org/p/lBRpKSo-9P
I think of a slice as a built in structure so the pointer to the backing store will stay the same, the capacity will stay the same, only the length will change with append. (unless there is insufficient capacity then all bets are off except that again the original structure will remain, but all the values in it will be overwritten, then copied back into the original struct)

Or have I really misunderstood what happens under the hood here?

The backing store is reused if there is capacity but append returns a new slice that points to it. A small modification to your code demonstrates it:


Ian

Tad Vizbaras

unread,
Apr 18, 2017, 10:59:41 AM4/18/17
to golang-nuts
Thank you for detailed explanation.

I find myself never using "var a map[int]int" or similar var like map. Maybe just my limited understanding.
But I am glad we end up with map[int]int instead of *map[int]int.



Matt Harden

unread,
Apr 18, 2017, 10:12:20 PM4/18/17
to Tad Vizbaras, golang-nuts
It seems to me the equivalent of append for maps is merge, which would be a very useful operation to have in its own right. A useful design for map could have been an immutable structure supporting literals, merge, lookup and delete operations, where all except lookup would return a new map value. The obvious zero value would be an empty map. This can be made efficient by sharing underlying data which would be safe since the map was immutable.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Val

unread,
Apr 19, 2017, 6:32:46 AM4/19/17
to golang-nuts, eta...@gmail.com
That's interesting, though quite different from the map implementation we're used to.  Do you know (in any language) such an implementation, and would it really be as fast?  Computing hash, accessing some bucket, and dealing with collisions seem unavoidable. But I have no idea of the extra cost of dealing with a "versioned" immutable map, where each new version is not overwriting the previous one.

Will Faught

unread,
Apr 20, 2017, 3:41:50 AM4/20/17
to golang-nuts, jes...@jessta.id.au, eta...@gmail.com
Why couldn't maps be implemented as a pointer to the map implementation? If you try to use the map and the pointer is nil, then the map allocates the backing implementation. Pseudocode for a built-in implementation:

type map struct {
    impl *mapimpl
}

func (m map) set(k, v interface{}) { // used for m[k] = v
    if m.impl == nil {
        m.impl = newMapImpl()
    }
    m.impl.set(k, v)

Jan Mercl

unread,
Apr 20, 2017, 4:22:56 AM4/20/17
to Will Faught, golang-nuts
On Thu, Apr 20, 2017 at 9:42 AM Will Faught <will....@gmail.com> wrote:

> Why couldn't maps be implemented as a pointer to the map implementation? If you try to use the map and the pointer is nil, then the map allocates the backing implementation. Pseudocode for a built-in implementation:
>
> type map struct {
> impl *mapimpl
> }


> func (m map) set(k, v interface{}) { // used for m[k] = v
> if m.impl == nil {
> m.impl = newMapImpl()
> }
> m.impl.set(k, v)
> }

Without a pointer receiver the set method above is ineffective. With a pointer receiver every map operation is double dereferencing.

--

-j

Matt Harden

unread,
Apr 21, 2017, 10:17:48 PM4/21/17
to Val, golang-nuts, eta...@gmail.com
I can't speak for real-world speed, but algorithms with similar asymptotic complexity exist that work with immutable data. The hash map is not the only possible map implementation. Example from Haskell: http://hackage.haskell.org/package/containers-0.5.10.2/docs/Data-Map-Strict.html

Will Faught

unread,
May 8, 2017, 4:18:04 AM5/8/17
to golang-nuts, will....@gmail.com
Yes, you're right, the receiver should be a pointer.

What's bad about a double pointer dereference? Isn't that what, say, bytes.Buffer does, more or less, at least conceptually?
Reply all
Reply to author
Forward
0 new messages