目录

深入理解golang的slice、map、channel类型

前言

学习golang的同学都知道slice、map、channel这三种特别的类型,他们在传入函数的时候,实参和形参的改变会相互影响,类似指针的效果。但当我们使用reflect.TypeOf()打印的时候却又不是指针,这到底是为什么?
今天我们就来看看下面几个问题:

  • 值类型、指针类型、引用类型是什么?有何特点?
  • slice、map、channel到底是什么类型?
  • 作为函数参数时,它们为什么会产生类似指针的效果?

一、值类型

首先看看值类型变量,拿int来举例,上代码。

实验1:值传递

1
2
3
4
5
6
7
8
9
var x int = 10
fmt.Printf("before changeX() func : x=%d, &x=%#x \r\n", x, &x)
changeX(x)
fmt.Printf("after changeX() func : x=%d, &x=%#x \r\n", x, &x)

func changeX(x int){
x = 20
fmt.Printf("inner changeX() func : x=%d, &x=%#x \r\n", x, &x)
}

输出:

1
2
3
before changeX() func : x=10, &x=0xc000094030
inner changeX() func : x=20, &x=0xc000094040 # 形参地址发生变化,说明产生了一个新变量
after changeX() func : x=10, &x=0xc000094030

结论:函数内外变量x的地址发生了变化,说明函数参数是值拷贝,形参是一个新的变量x,对形参的修改也不会影响到实参。

二、指针类型

实验2:指针的作用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var x int = 10 //声明实际变量
var ip *int    //声明指针变量
ip = &x        //把实际变量的内存地址赋给指针变量

fmt.Printf("before changeIp() func, x=%d, &x=%x \r\n", x, &x)
changeIp(ip)
fmt.Printf("after changeIp() func, x=%d, &x=%#x \r\n", x, &x)

func changeIp(ip *int){
*ip = 20
fmt.Printf("inner changeIp() func, ip=%#x, *ip=%d\r\n", ip, *ip)
}

输出:

1
2
3
before changeIp() func, x=10, &x=c000094008
inner changeIp() func, ip=0xc000094008, *ip=20
after changeIp() func, x=20, &x=0xc000094008

结论:无论函数内还是函数外,通过指针变量去修改原变量,都会引起原变量的变化,这正是指针的作用。 /images/20220615/img.png 实验1中说到了值传递会拷贝实参,那实验2中的指针变量拷贝情况是怎么样的呢?继续看实验3来验证下。

实验3:指针传递

1
2
3
4
5
6
7
8
9
fmt.Printf("before copyIp() func, ip=%#x, &ip=%#x \r\n", ip, &ip)
copyIp(ip)
fmt.Printf("after copyIp() func, ip=%#x, &ip=%#x \r\n", ip, &ip)

func copyIp(ip *int){
fmt.Printf("inner copyIp() func, ip=%#x, &ip=%#x \r\n", ip, &ip)
ip = nil
fmt.Printf("inner copyIp() func ip=>nil, ip=%#x, &ip=%#x \r\n", ip, &ip)
}

输出:

1
2
3
4
before copyIp() func, ip=0xc00001a080, &ip=0xc00000e028 #指针变量实参ip,地址为0xc00000e028
inner copyIp() func, ip=0xc00001a080, &ip=0xc00000e038 #指针变量形参ip,地址变为了0xc00000e038,说明发生了值拷贝
inner copyIp() func ip=>nil, ip=0x0, &ip=0xc00000e038 #对形参做修改
after copyIp() func, ip=0xc00001a080, &ip=0xc00000e028 #实参ip不会引起变化

结论:指针变量传参也发生了值拷贝。 经过实验1到3,可以得出结论:不管是普通变量还是指针变量,传递到函数内都会发生一次值拷贝,函数内外对变量的修改都互不影响。如果通过指针的方式去修改一个变量,则会影响。(指针变量的作用)

三、引用类型

1. c++中的引用

先来看下c++中对引用类型的定义:

A reference is not an object. Instead, a reference is just another name for an already existing object. 也就是说引用类型并不是像值类型或者指针类型那样是一个具体的内存对象,而仅仅是对某个变量的别名,它必须和某个变量一一绑定。

来看一个关于引用的定义:

1
2
3
4
int x = 10;
int &reX = x;// 定义一个引用变量reX,此时的&符号可不是取地址的意思哦。
printf("x=%d, &x=%p\r\n", x, &x);// x=10, &x=0x7ffeefbffdc8
printf("reX=%d, &reX=%p\r\n", reX, &reX);// reX=10, &reX=0x7ffeefbffdc8 地址一样的哟

2. golang中的引用

关于golang中的引用并没有找到相关官方的说明和定义。

四、slice

先来看看go对slice的定义:

Go的切片是在数组之上的抽象数据类型。 再看源码reflect/value.go中对切片的定义:

type SliceHeader struct { Data uintptr Len int Cap int } 不难看出切片这个结构体就三个字段:len表示长度,cap表示容量,data表示具体的数据(data是个指针哦)。

使用unsafe.sizeof()打印某个切片始终是24字节=8byte+8byte+8byte。而数组使用unsafe.sizeof()结果会根据数组的长度不同而变化。

那么为什么在函数内外修改slice会相互影响呢,函数对slice类型参数的传递也是拷贝传递的吗?我们先看一个实验。

实验4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var arrX = [3]int{5, 6}
fmt.Printf("before arrX=%v, &arrX=%p \r\n", arrX, &arrX)
changeArrX(arrX)
fmt.Printf("after arrX=%v, &arrX=%p \r\n\r\n", arrX, &arrX)

var sliceX = []int{5, 6}
fmt.Printf("before sliceX=%v, &sliceX=%p \r\n", sliceX, &sliceX)
changeSliceX(sliceX)
fmt.Printf("after sliceX=%v, &sliceX=%p \r\n\r\n", sliceX, &sliceX)

func changeArrX(arrX [3]int){
arrX[0] = 100
arrX[1] = 200
fmt.Printf("inner arrX=%v, &arrX=%p \r\n", arrX, &arrX)
}

func changeSliceX(sliceX []int){
sliceX[0] = 100
sliceX[1] = 200
fmt.Printf("inner sliceX=%v, &sliceX=%p \r\n", sliceX, &sliceX)
}

输出:

1
2
3
4
5
6
7
before arrX=[5 6 0], &arrX=0xc000098080
inner arrX=[100 200 0], &arrX=0xc0000980c0 #arrX地址发生变化,说明传递进来被拷贝了一份
after arrX=[5 6 0], &arrX=0xc000098080 #函数外的值未被影响,说明是深拷贝

before sliceX=[5 6], &sliceX=0xc0000881e0
inner sliceX=[100 200], &sliceX=0xc000088220 #sliceX地址发生变化,说明传递进来被拷贝了一份
after sliceX=[100 200], &sliceX=0xc0000881e0 #函数外的值被影响,说明是浅拷贝,通过SliceHeader结构体中的Data指针字段去修改的切片数据

结论:

array的函数传递与读写和普通变量无异。 slice的函数传递发生的是浅拷贝,只会对slice结构体进行一次值拷贝。但是slice的读写是和指针的读写类似,通过一个指针变量Data去修改具体的数据,从而达到函数内外相互影响的作用。 不止是函数传递,包括slice的复制,同样也是只拷贝slice的结构体,共用底层的数据,所以复制出来的切片修改数据也会影响到原值。 来个小图吧。

/images/20220615/img_1.png

五、 map

同样先看看官网博客的定义:

Go provides a built-in map type that implements a hash table. Map types are reference types, like pointers or slices. 也就是说map和slice的原理一致,再看看源码runtime/map.go对map的创建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
...
...

以及hmap结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count     int // # live cells == size of map.  Must be first (used by len() builtin)
flags     uint8
B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0     uint32 // hash seed

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

这两个函数大致能看出map的结构,开发者使用的map就是一个指针,指向hmap这个结构体。

既然它是一个指针,那么就和指针的用法一致,函数的传递是值拷贝(拷贝hmap指针),读写也是通过指针去修改,肯定也会造成函数内外相互影响啦。

所以如果用*map的方式,相当于对指针再次使用指针,有点多此一举的感觉了。

但是为什么我们使用reflect.TypeOf()打印指针的时候没有*呢?

1
2
3
var mapZ map[int]int
fmt.Println(reflect.TypeOf(mapZ)) # map[int]int
fmt.Println(reflect.TypeOf(&user{})) # *main.user

以下摘抄自这里:

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.

六、 channel

同理channel和map一样,也是一个指针,指向的是runtime/chan.go中的hchan结构体,这里就不展开描述了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func makechan(t *chantype, size int) *hchan {
...
}

type hchan struct {
qcount   uint           // total data in the queue
dataqsiz uint           // size of the circular queue
buf      unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed   uint32
elemtype *_type // element type
sendx    uint   // send index
recvx    uint   // receive index
recvq    waitq  // list of recv waiters
sendq    waitq  // list of send waiters
lock mutex
}

七、总结

  • go里面的函数传递都是值拷贝,
  • slice是一个24Byte的结构体。
  • map和channel都是8Byte的指针。
  • slice、map、channel使用的是浅拷贝,形参实参会通过指针共享数据,所以会相互影响。但是golang对这三个结构体做了封装,从广义上来定义引用的话(通过别名去修改原数据),那这三个数据类型也属于引用类型。

原文出自知乎:https://zhuanlan.zhihu.com/p/472250730