首先为什么讨论这个基础的问题,因为博主今天在写代码的时候又遇到了这个基础的问题,之前也遇到过,但是没有记录,所以又忘记了,还是那句话好记性不如烂笔头,记录下来。
问题是这样的,我要去修改内存中的map里头的数据,发现改了怎么也没生效,于是写了个demo验证
package main
import (
"fmt"
)
func main() {
clothesMap := map[int]Clothes{
1:{
"Nike",
300,
},
2:{
"Adidas",
300,
},
}
fmt.Println("clothesMap:", clothesMap[2])
for k,v := range clothesMap {
if k == 2 {
v.price --
fmt.Println("v.times:", v.price)
}
}
fmt.Println("clothesMap:", clothesMap[2])
}
type Clothes struct {
name string
price int
}
输出:
clothesMap: {Adidas 300}
v.times: 299
clothesMap: {Adidas 300}
看到这个结果就能发现在遍历时,value是map中的数据拷贝,我们修改的只是拷贝后的数据,并不是原数据,面对这类问题两种解决方法
- 修改后,把对应的key,value再次放入map覆盖之前的即可
- 定义的时候定义指针类型,修改的时候就修改的是指针类型,例如
package main
import (
"fmt"
)
func main() {
clothesMap := map[int]*Clothes{
1:{
"Nike",
300,
},
2:{
"Adidas",
300,
},
}
fmt.Println("clothesMap:", clothesMap[2])
for k,v := range clothesMap {
if k == 2 {
v.price --
fmt.Println("v.times:", v.price)
}
}
fmt.Println("clothesMap:", clothesMap[2])
}
type Clothes struct {
name string
price int
}
输出:
clothesMap: &{Adidas 300}
v.times: 299
clothesMap: &{Adidas 299}
与我们想得到的一致
根据这个问题又深入了一下go语言的值类型,引用类型,值传递问题
值类型,引用类型
在go语言中值类型:int、float、bool、string和数组这些类型都属于值类型,值类型的变量直接指向存在内存中的值,值类型的变量的值存储在栈中。当使用=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝。可以通过 &i 获取变量 i 的内存地址,值拷贝。
引用类型:特指slice、map、channel这三种预定义类型。引用类型拥有更复杂的存储结构:(1)分配内存 (2)初始化一系列属性等一个引用类型的变量r1存储的是r1的值所在的内存地址(数字),或内存地址中第一个字所在的位置,这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。
举例说明值类型和引用类型
首先用数组举例,数组是值类型
func main() {
a :=[5]int{1,2,3,4,5}
b := a
b[2] = 8
fmt.Println(a, b)
}
out:
[1 2 3 4 5] [1 2 8 4 5]
因为b是a的值拷贝,所以修改b不会对a产生任何变化
但是如果使用切片类型:
//由 main 函数作为程序入口点启动
func main() {
a :=[]int{1,2,3,4,5}
b := a
b[2] = 8
fmt.Println(a, b)
}
out:
[1 2 8 4 5] [1 2 8 4 5]
a和b指向同一个底层数组,切片的底层数据结构是一个指针,len,cap
值传递和引用传递
- java中基本变量传递都是值传递,引用类型就是也是值传递,和golang以下解释类似
- go里面只存在只存在值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。之所以对于引用类型的传递可以修改原内容数据,是因为在底层默认使用该引用类型的指针进行传递,但是也是使用指针的副本,依旧是值传递。
接下来代码验证:
func main() {
i := 10 //整形变量 i
ip := &i //指向整型变量 i 的指针ip,包含了 i 的内存地址
fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,ip,&ip)
modifyBypointer(i)
fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,ip,&ip)
}
func modifyBypointer(i int) {
fmt.Printf("modify i 为:%v,i 的内存地址为:%v",i,&i)
i = 11
}
----output----
main中 i 的值为:10,i 的内存地址为:0xc0420080b8,i 的指针的内存地址为:0xc042004028
modify i 为:10,i 的内存地址为:0xc0420080d8
main中 i 的值为:10,i 的内存地址为:0xc0420080b8,i 的指针的内存地址为:0xc042004028
上面在函数接收的参数中没有使用指针,所以在传递参数时,传递的是该值的副本(变量和副本的值一样,但是内存地址不一样),因此在函数中对该变量进行操作不会影响到原变量的值。
值传递
将上面函数的参数传递方式改一下,改为指针接收
func main() {
i := 10 //整形变量 i
ip := &i //指向整型变量 i 的指针ip,包含了 i 的内存地址
fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,ip,&ip)
modifyBypointer(ip)
fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,ip,&ip)
}
func modifyBypointer(i *int) {
fmt.Printf("modify i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,&i)
*i = 11
}
out:
main中i的值为:10,i 的内存地址为:0xc042060080,i的指针的内存地址为:0xc042080018
modify i 的内存地址为:0xc042060080,i的指针的内存地址为:0xc042080028
main中i的值为:11,i 的内存地址为:0xc042060080,i的指针的内存地址为:0xc042080018
函数的参数改为指针后,函数内部对于变量的修改就会影响原变量的值,但不会影响原变量的内存地址,但是我们发现对于i的指针的内存地址与main中又不一样,那为什么我们在函数中修改变量能影响到原变量呢
图二
由此可见,在指针传递时,其实也是值传递i的指针ip的内存地址(0xc042080018)和拷贝的副本(0xc042080028)指向的都是i的内存地址(0xc042060080)所以修改可以生效。
由此,我们验证了非引用类型和指针的参数传递都是传递副本,那么对于引用类型的参数传递又是如何的呢?
- map,查看makemap源码
func makemap(t *maptype, hint int, h *hmap) *hmap {}
发现返回的是该map的指针,也就是说,对于引用类型map来讲,实际上
在作为传递参数时还是使用了指针的副本进行传递,属于值传递,与上面一致
- chan类型
使用make初始化 chan类型,底层其实跟map一样,都是返回该值的指针,也是值传递
func makechan(t *chantype, size int) *hchan {}
- slice类型
Slice类型对于之前的map,chan类型不太一样
举例:
func main() {
i := []int{1,2,3}
fmt.Printf("i:%p\n",i)
fmt.Println("i[0]:",&i[0])
fmt.Printf("i:%v\n",&i)
}
out:
i:0xc00001a0a8
i[0]: 0xc00001a0a8
i:&[1 2 3]
发现使用%p输出的的内存地址与slice的第一个元素地址是一样的,并且使用&操作符表示slice的地址是无效的
…省去一大段源码
所以当是slice类型的时候,fmt.Printf返回是slice这个结构体里第一个元素的地址。说到底,又转变成了指针处理,只不过这个指针是slice中第一个元素的内存地址。之前说Slice类型对于之前的map,chan类型不太一样,不一样就在于slice是一种结构体+第一个元素指针的混合类型,通过元素array(Data)的指针,可以达到修改slice里存储元素的目的。
type slice struct {
array unsafe.Pointer //这里的指针其实是第一个元素的指针
len int
cap int
}
根据slice与map,chan对比,我们可以总结一条规律:
可以通过某个变量类型本身的指针(如map,chan)或者该变量类型内部的元素的指针(如slice的第一个元素的指针)修改该变量类型的值。
因此slice也跟chan与map一样,属于值传递,传递的是第一个元素的指针的副本
总结:在Go语言中只存在值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。之所以对于引用类型的传递可以修改原内容数据,是因为在底层默认使用该引用类型的指针进行传递,但是也是使用指针的副本,依旧是值传递。参照图二