YANGHONG

#Go# Use map without making one

The mysterious map problem

I accidentally wrote the following piece of code and I became confused.

func serverParseAll(r *http.Request) {
    // cpuinfo := hwinfo{}
    var cpuinfo map[string]string
    dec := json.NewDecoder(r.Body)
    dec.Decode(&cpuinfo)
    fmt.Println(cpuinfo)
    // for test I add the following code
    cpuinfo["hello"] = "world"
}

I Googled a lot and am very sure that "maps must be allocated using make before using it". But apparently I used cpuinfo here without making one. To make sure cpuinfo works like a pre-allocated map, I added a line of code and of course it worked.

Replay the bug

Well…It's odd…So I wrote the following code

func main() {
    //1// var m map[string]string
    //2// m := map[string]string{}
    //3// m := make(map[string]string)

    m["hello"] = "world"
}

Without surprise, expr-1 doesn't work. expr-2 works since m is a literal. expr-3 works perfectly. So it's true that if I use m without making one, I would get the panic: assignment to entry in nil map error. But what's problem on earth?

Golang source

I discovered this odd while dealing with parsing JSON data in golang so I digged into the source code and found…

// file: src/encoding/json/decode.go

func (d *decodeState) object(v reflect.Value) {
    // Omit lines of code dealing with pointer
    // If v is a reflect.Ptr, use `v.Elem()` to get the object
    // pointed to by v into pv
    v = pv

    // Decoding into nil interface?  Switch to non-reflect code.
    if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
	v.Set(reflect.ValueOf(d.objectInterface()))
	return
    }

    // Check type of target: struct or map[string]T
    switch v.Kind() {
    case reflect.Map:
	// map must have string kind
	t := v.Type()
	if t.Key().Kind() != reflect.String {
	    d.saveError(&UnmarshalTypeError{"object", v.Type()})
	    d.off--
	    d.next() // skip over { } in input
	    return
	}
	if v.IsNil() {
	    v.Set(reflect.MakeMap(t))
	}
    case reflect.Struct:

    default:
	d.saveError(&UnmarshalTypeError{"object", v.Type()})
	d.off--
	d.next() // skip over { } in input
	return
    }

    // ... remaining code
}

What makes the difference is…

if v.IsNil() {
    v.Set(reflect.MakeMap(t))
}

Verification

To double check it…

func bar(m interface{}) {
    rv := reflect.ValueOf(m)
    if rv.Kind() != reflect.Ptr || rv.IsNil() {
	fmt.Println("error interface")
    }

    switch rv.Kind() {
    case reflect.Ptr:
	fmt.Println("kind is ptr")
    case reflect.Interface:
	fmt.Println("kind is interface")
    default:
	fmt.Println("kind is something else")
    }

    rv = rv.Elem()
    if rv.Kind() == reflect.Map {
	fmt.Println("kind is map pointer")
    }
    if rv.IsNil() {
	fmt.Println("kind is originally nil")
    }
}

func main() {
    var m map[string]string

    bar(&m)
}

Conclusion

Golang has a relative stable API and is ready for production environment. So if there are some conflicts against basic rules of this language, then it can be caused by library or 3rd-party code. Interface feature is very interesting and can alter the ways things used to be in.

comments powered by Disqus