go-基于指针对象的方法-map深度理解
代码如下,请分析为何值接收器方法可以直接改变原变量?
1 | package url |
Map 本质上就是一个指针
在 Go 内部,map 变量存的不是数据本身,而是一个指向底层哈希表的指针:
1 | m := url.Values{"lang": {"en"}} |
所以值拷贝拷的是什么?
当用值接收器调用方法时,Go 确实做了拷贝,但拷贝的是这个指针,不是底层数据:
1 | func (v Values) Add(key, value string) { |
对比 struct 就明白了
1 | // struct — 值拷贝是深拷贝,改不了原始值 |
| 类型 | 值拷贝拷的是什么 | 修改能影响原始值? |
|---|---|---|
| struct | 整个结构体的所有字段 | ❌ 不能 |
| map | 指向底层哈希表的指针 | ✅ 能 |
| slice | 指向底层数组的指针 + 长度 + 容量 | ✅ 能(修改元素时) |
| channel | 指向底层结构的指针 | ✅ 能 |
所以 map 用值接收器就够了?
对于 map 类型,值接收器和指针接收器效果几乎一样,因为值本身就是个指针。这就是为什么标准库里 url.Values 用的是值接收器——没必要再取一次指针的指针。
唯一的区别是:值接收器不能让 map 变量本身变成另一个 map(比如从 nil 变成非 nil),这也解释了最后一行为什么 panic:
1 | m = nil |
一句话总结
Map 变量本身就是个指针,值拷贝拷的是指针,所以值接收器也能修改底层数据——这不是例外,而是 map 的本质决定的。
对Get的最后一次调用中,nil接收器的行为即是一个空map的行为。我们可
以等价地将这个操作写成Value(nil).Get(“item”),但是如果你直接写nil.Get(“item”)的话是无法通过编译的,因为nil的字面量编译器无法判断其准确类型。所以相比之下,最后的那行m.Add的调用就会产生一个panic,因为他尝试更新一个空map。
由于url.Values是一个map类型,并且间接引用了其key/value对,因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上,就像在普通函数中一样,虽然可以通过引用来操作内部值,但在方法想要修改引用本身时是不会影响原始值的,比如把他置换为nil,或者让这个引用指向了其它的对象,调用方都不会受影响。(译注:因为传入的是存储了内存地址的变量,你改变这个变量本身是影响不了原始的变量的,想想C语言,是差不多的)
这段话知识点解读如下:
第一段:nil map 可以读,不能写
“nil 接收器的行为即是一个空 map 的行为”
1 | m = nil |
Go 的规则:从 nil map 读取,不会 panic,返回零值。这和空 map 的行为一样:
1 | var m1 url.Values = nil // nil map |
“Value(nil).Get(“item”) 可以,nil.Get(“item”) 不行”
1 | url.Values(nil).Get("item") // ✅ 编译器知道 nil 是 url.Values 类型 |
nil 本身没有类型,它可以是 nil map、nil slice、nil pointer……编译器无法判断该调用哪个类型的 Get 方法。必须通过显式类型转换告诉编译器。
“m.Add 会 panic,因为尝试更新一个空 map”
1 | m = nil |
这是 Go 的硬性规则:
| 操作 | nil map | 空 map |
|---|---|---|
读 m["key"] |
✅ 返回零值 | ✅ 返回零值 |
写 m["key"] = val |
❌ panic | ✅ 正常 |
第二段:能改内容,但改不了引用本身
“对 map 里的元素做更新、删除对调用方都是可见的”
这就是上面讨论过的,map 是指针,值拷贝后共享底层数据:
1 | func (v Values) Add(key, value string) { |
“方法想要修改引用本身时是不会影响原始值的”
这是关键。方法能改 map 里面的东西,但不能改 map 变量本身:
1 | func (v Values) Reset() { |
用图理解
1 | 调用前: |
译注中的 C 语言类比
1 | // C 语言 |
Go 的 map 变量和 C 的指针变量道理完全一样。
一句话总结
Map 是个指针:你能通过它改里面的数据,但不能通过赋值 nil 或新 map 来影响调用方——因为你改的只是指针的副本,不是调用方手里那个指针。