Go 返回值初始化分析

317 阅读2分钟
package main

import (
	"fmt"
	"strconv"
)

func main() {
	b := maps()
 	fmt.Println(b)
}

func maps() (ret map[string][]string) {
	//ret = make(map[string][]string)
	for j := 0; j < 5; j++ {
		jstr := strconv.Itoa(j)
		for i := 0; i < 10; i++ {
			// if _, ok := ret[jstr]; !ok {
			// 	ret[jstr] = []string{strconv.Itoa(i)}
			// }
			ret[jstr] = append(ret[jstr], strconv.Itoa(i))
		}
	}
	return
}

s@m demo % go run main.go
map[0:[0 1 2 3 4 5 6 7 8 9] 1:[0 1 2 3 4 5 6 7 8 9] 2:[0 1 2 3 4 5 6 7 8 9] 3:[0 1 2 3 4 5 6 7 8 9] 4:[0 1 2 3 4 5 6 7 8 9]]

上例中可以看到 ret 是 maps 的返回值, 其中 ret = make(map[string][]string) 对其做了初始化, 如果不对他做这个初始化, 会发生什么呢?

我们注释掉对 ret 初始化的这一行后执行, 结果如下:

s@m demo % go run main.go
panic: assignment to entry in nil map

goroutine 1 [running]:
main.maps()
        /Users/s/go/src/github.com/wonderful-ya/demos/demo/main.go:21 +0xa7
main.main()
        /Users/s/go/src/github.com/wonderful-ya/demos/demo/main.go:9 +0x19
exit status 2

可以看到, 报 nil 错误。由此可见, 对于函数的返回值, 必须要对其初始化之后, 才可以操作它。

这其实是常事, 但是容易忽略。

忽略是因为, 如果这个 ret map 对象不是返回值, 而是入参的话, 我们一般是可以对其直接操作, 而无需初始化的。不过入参可以直接操作的原因是因为, 在调用这个函数的调用方会提前初始化这个入参,我们都知道,Go 语言是值拷贝语言, 入参和调用方的参数不一定是同一个对象,但是 Go 在做值拷贝的时候, 就自动会初始化该入参了(值拷贝等于是创建了一个相同对象)。而 map 对象又有一定的特殊, 因为 map 实现的底层是一个数组的引用, 因而函数体中对 map 的修改, 是会影响到调用方的参数对象的。

回到这里的函数返回对象, 如果函数返回对象是该函数体内新的对象, 那么该函数一定要记得初始化它。 不然就会报 nil 错误。

上面的代码还有第二个要点, ret = make(map[string][]string) 在对ret做初始化的时候, 由于这个map的 value 值是 Slice 类型, 所以在对 value 进行赋值的时候, 该 Slice 也是需要做初始化的。 然后该代码却把

			// if _, ok := ret[jstr]; !ok {
			// 	ret[jstr] = []string{strconv.Itoa(i)}
			// }

这块代码注释掉了。 这是因为 append 函数会自动初始化 Slice 对象。