Go中的数据类型、指针、new和make

值类型与引用类型

《Go语言圣经》中将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

  • 基础类型:数字、字符串和布尔型。
  • 复合数据类型:数组、结构体。是通过组合简单类型,来表达更加复杂的数据结构。
  • 引用类型:指针、切片、字典、函 数、通道。
  • 接口类型:通过type name interface定义的接口

我们可以根据数据赋值时的行为将数据类型分为2类,值类型与引用类型。其中值类型复制的时候得到的是数据的一份拷贝,引用类型赋值的时候得到的是引用地址的一份拷贝。接口和函数可以看做是引用类型。

具体如下:

  • 值类型有boolint系列(int、int8、int16、int32、int64、uint、uintptr、uint8、uint16、uint32、uint64、byte、rune)、float系列(float64、float32)、complex系列(complex64、complex128)、string数组struct
  • 引用类型有值类型对应的指针mapslicechan

注意,uintptr是一个值类型,用于存放指针的值(也就是指针指向的地址)

在进行数据传递的时候,Go始终是传递的值,对于值类型而言,传递的是值的拷贝;对应引用类型而言,传递的是引用地址的拷贝。

比如拿数组slice进行测试:

func Test01(t *testing.T) {
	outerArray := [3]int{1, 2, 3} // 值类型
	arrayCopy := outerArray       // 将值拷贝给arrayCopy
	t.Log("【数组】")
	t.Logf("outerArray:%v, point:%p", outerArray, &outerArray)
	t.Logf("arrayCopy:%v, point:%p", arrayCopy, &arrayCopy)
	arrayFunc := func(innerArray [3]int) { // 传递过来的是值的拷贝
		t.Logf("innerArray:%v, point:%p", innerArray, &innerArray)
		innerArray[0] = 100
	}
	arrayFunc(outerArray)
	t.Logf("修改后, outerArray=%v", outerArray)

	outerSlice := []int{1, 2, 3} // 引用类型
	sliceCopy := outerSlice      // 将引用地址拷贝给sliceCopy
	t.Log("【slice】")
	// outerSlice存的是引用地址(即outerSlice的值),&outerSlice是存放该地址的指针(即存储outerSlice的位置)
	t.Logf("outerSlice:%v, addr:%p, point:%p", outerSlice, outerSlice, &outerSlice)
	t.Logf("sliceCopy:%v, addr:%p, point:%p", sliceCopy, sliceCopy, &sliceCopy)
	sliceFunc := func(innerSlice []int) { // 传递过来的是引用地址的拷贝
		t.Logf("innerSlice:%v, addr:%p, point:%p", innerSlice, innerSlice, &innerSlice)
		innerSlice[0] = 100
	}
	sliceFunc(outerSlice)
	t.Logf("修改后, outerSlice=%v", outerSlice)
}

输出结果:

【数组】
outerArray:[1 2 3], point:0xc00000c4e0
arrayCopy:[1 2 3], point:0xc00000c500
innerArray:[1 2 3], point:0xc00000c5c0
修改后, outerArray=[1 2 3]
【slice】
outerSlice:[1 2 3], addr:0xc00000c680, point:0xc000004600
sliceCopy:[1 2 3], addr:0xc00000c680, point:0xc000004620
innerSlice:[1 2 3], addr:0xc00000c680, point:0xc000004740
修改后, outerSlice=[100 2 3]

可以看出,对于值类型而言,变量outerArray存储的本身就是值,也无法通过"%p"输出对应变量的地址,需要通过&outerArray才能取到对应的指针(内容是变量的地址),并且不管是传递给变量还是传递给函数,传递的是值的一份拷贝,对拷贝进行修改不会影响原有内容。

对于引用类型而言,变量outerSlice存储的是引用地址,通过"%p"可以输出变量的地址,不管是传递给变量还是传递给函数,传递的是引用地址的拷贝,所以outerSlicesliceCopyinnerSliceaddr都一样,但是point却不同,这是因为我们每次传递过去的时候都是传递引用地址的一份拷贝,所以point不同。当我们对slice进行修改时,其实修改的是对应地址上的内容,所以会在函数外体现出变化。

从下面可以看出,在array基础上创建的slice,其实引用的就是array的内容,引用地址都是0xc00000c4e0。如果slice发生扩容了,则会创建一份新的底层数组(我们不可见),将原有值拷贝进去,然后重新指向该新的数组。

func Test02(t *testing.T) {
	array := [3]int{1, 2, 3}
	slice := array[:] // 从array创建一份slice
	t.Logf("array:%v, point:%p", array, &array)
	t.Logf("array[0]_point:%p", &array[0])
	t.Logf("slice:%v, addr:%p, point:%p", slice, slice, &slice)
    
    slice = append(slice, 4) // 扩容
	t.Logf("slice:%v, addr:%p, point:%p", slice, slice, &slice)
}

输出为:

array:[1 2 3], point:0xc00000c4e0
array[0]_point:0xc00000c4e0
slice:[1 2 3], addr:0xc00000c4e0, point:0xc000004540
slice:[1 2 3 4], addr:0xc0000104e0, point:0xc000004540

可以看到,slice指向新的0xc0000104e0内存区域,这片区域的底层数组对我们不可见,我们只能通过slice来访问,这也体现了设计模式中的外观模式。

指针

从前面可以看出,对于引用类型而言,slice变量存的是引用地址(指向存储真正内容的地址),&slice是指向该地址的一个指针。

我们可以通过point := unsafe.Pointer(&outerSlice)来通过指针创建一个unsafe.Pointer指针,利用该指针可以进行更多操作。

我们能从指针得到什么,取决于我们把这个指针看做(强制转换)是什么类型的指针,如果我们把这个指针看做是*int,那么通过(*int)(point)获得的就是一个int类型的数据,我们转换后的仍然还是一个指针,需要通过*来获取指向的内容。需要注意的是转换的类型要匹配,否则就会得到不符合预期的结果,如通过*(*[]int32)(point),就会得到[1 0 2 ]

func Test03(t *testing.T) {
	slice := []int{1, 2, 3}         // 引用类型
	point := unsafe.Pointer(&slice) // 通过指向引用地址的指针创建一个unsafe.Pointer指针
	rightPoint := (*[]int)(point)   // 换成[]int指针
	intPoint := (*int)(point)       // 转为为int指针
	bytePoint := (*byte)(point)     // 换成byte指针
	t.Logf("rightPoint:%v, point:%p, value:%v", rightPoint, rightPoint, *rightPoint)
	t.Logf("intPoint:%v, point:%p, value:%v", intPoint, intPoint, *intPoint)
	t.Logf("bytePoint:%v, point:%p, value:%v", bytePoint, intPoint, *bytePoint)

	// 假设&slice的值为0xc000004540
	point2 := unsafe.Pointer(uintptr(0xc000004540))
	rightPoint2 := (*[]int)(point2)
	t.Logf("rightPoint2:%v, point:%p, value:%v", rightPoint2, rightPoint2, *rightPoint2)
}

输出为:

rightPoint:&[1 2 3], point:0xc000004540, value:[1 2 3]
intPoint:0xc000004540, point:0xc000004540,value:824633771232
bytePoint:0xc000004540, point:0xc000004540, value:224
rightPoint2:&[1 2 3], point:0xc000004540, value:[1 2 3]

可以看到,同样指向0xc000004540地址的指针,但是我们转换成不同类型的指针,就会得到不同的结果。

注意,我们通过%v输出指针时,格式化函数会帮我们进行友好转换,比如&[1 2 3]就表示指向[1 2 3]的一个指针。

事实上,&slice是具体指针类型,uintptr是值类型,只不过这个值存放的是地址,两者之间进行转换需要通过unsafe.Pointer来进行。

func Test04(t *testing.T) {
	slice := []int{1, 2, 3} // 引用类型
	var slicePoint *[]int
	var slicePtr uintptr
	slicePoint = &slice
	slicePtr = uintptr(unsafe.Pointer(slicePoint)) // *[]int -> uintptr
	t.Logf("slicePoint:%p, slicePtr:%x", slicePoint, slicePtr)
	slicePoint = (*[]int)(unsafe.Pointer(slicePtr)) // uintptr -> *[]int
	t.Logf("slicePoint:%p, slicePtr:%x", slicePoint, slicePtr)
}

输出为:

slicePoint:0xc000004540, slicePtr:c000004540
slicePoint:0xc000004540, slicePtr:c000004540

默认值

对于值类型而言,声明的时候就会有默认值,为该类型对应的零值,bool为false,int/float系列类型为0,complex系列为(0+0i),数组为对应类型的零值填充,结构体比较复杂,下面再说。

func Test05(t *testing.T) {
	var a1 bool       // 等价于 a1 := false
	var a2 int        // 等价于 a2 := 0
	var a3 float64    // 等价于 a3 := 0
	var a4 complex128 // 等价于 a4 := 0+0i
	var a5 string     // 等价于 a5 := ""
	var a6 byte       // 等价于 a6 := '\u0000'
	var a7 [2]int     // 等价于 a7 := [2]int{0,0}
	t.Logf("%v, %v, %v, %v, %v, %v, %v", a1, a2, a3, a4, a5, a6, a7)
	t.Logf("%v, %v, %v, %v, %v, %v, %v", false, 0, 0, 0+0i, "", '\u0000', [2]int{0, 0})
}

输出为:

false, 0, 0, (0+0i), , 0, [0 0]
false, 0, 0, (0+0i), , 0, [0 0]

对于引用类型而言,声明的时候默认值为nil,除指针外,其余的还未分配空间,需要通过make()函数分配空间后才可使用。

func Test06(t *testing.T) {
	var s1 *int
	var s2 []int
	var s3 map[string]int
	var s4 chan int
	t.Log(s1 == nil, s2 == nil, s3 == nil, s4 == nil)
}

结构体也是值类型,声明的时候默认得到一个空结构体,结构体中字段的值为该类型的零值,如果字段为值类型,则默认值为值类型的零值,如果字段为引用类型,则默认值为nil。

func Test07(t *testing.T) {
	type student struct {
		name string
		age  int
	}
	var stu1 student
	stu2 := student{"", 0}
	stu3 := student{"marray", 21}
	t.Log(stu1 == stu2) // 只含值类型的结构体是可以判断是否相等的
	t.Log(stu1 == stu3) // 只含值类型的结构体是可以判断是否相等的

	type books struct {
		name string
		book []string
	}
	var b1 books
    b2 := books{"", nil}
	// t.Log(b1 == b2) // 报错, 含有引用类型的结构体不可判断是否相等
	t.Log(b1, b2) 
}

输出为:

true
false
{ []} { []}

可以看出,只声明结构体时会得到空结构体,其中所有字段都初始化为对应类型的零值。比较特殊的是,只含值类型的结构体是可以判断是否相等的,含有引用类型的结构体则不可判断是否相等。此外,结构体也不能和nil进行等性运算。

除开结构体,Go中的基础类型数组指针接口chan都可以判断是否相等,其中int系列float系列string可以进行判断大小,值类型是不能和nil进行比较的。slicemapfunc则只能判断是否等于nil,不可判断是否相等。以上运算均指同类型之间进行运算。

func Test08(t *testing.T) {
	s1 := "hello"
	s2 := "golang"
	// t.Log(s1==nil) // 基本类型不能和nil进行比较
	t.Log(s1 < s2) // string是可以判断大小的,按照字典序
	c1 := 0 + 0i
	c2 := 0 + 0i
	t.Log(c1 == c2) // complex系列不能判断大小,只能判断是否相等
	a1 := [3]int{1, 2, 3}
	a2 := [3]int{1, 2, 1}
	t.Log(a1 == a2) // 数组不能判断大小,只能判断是否相等
	slice1 := []int{1, 2, 3}
	slice2 := slice1
	t.Log(slice1 == nil, slice2) // slice只能判断是否等于nil,之间无法比较大小
}

输出为:

false
true
false
false [1 2 3] [1 2 3]

new和make

new()make()是内置函数,new()用于获取传入类型的一个指针,可传入值类型也可传入引用类型,make()用于为slicemapchan三种引用类型分配空间。

传入值类型:

func Test09(t *testing.T) {
	intPoint := new(int)              // 获取指向int类型的指针
	t.Log(*intPoint == 0)             // int类型指针指向的值为int类型的零值:0
	point := unsafe.Pointer(intPoint) // 创建一个unsafe.Pointer指针,指向intPoint指向的地址
	*intPoint = 100                   // 修改int指针指向地址的内容
	t.Log(*(*int)(point))             // 通过point读出来的内容也会发生改变(因为指向同一个地址)
}

输出为:

true
100

传入引用类型:

func Test10(t *testing.T) {
	slicePoint := new([]int)            // 获取指向[]int类型的指针
	t.Log(*slicePoint == nil)           // []int类型指针指向的值为[]int类型的零值:nil
	point := unsafe.Pointer(slicePoint) // 创建一个unsafe.Pointer指针,指向intPoint指向的地址
	*slicePoint = []int{1}              // 修改[]int指针指向地址的内容
	t.Log(*(*[]int)(point))             // 通过point读出来的内容也会发生改变(因为指向同一个地址)
}

输出为:

true
[1]

make()用于为slicemapchan三种引用类型分配空间,声明这3种类型后,其值都是默认值nil,必须要通过make()函数分配空间后才可使用。

func Test11(t *testing.T) {
	var s1 []int
	// s1[0] = 1 // 没分配空间,会报错
	// 第2个参数用于指定当前已用空间,range、append都是以这个值作为slice包含元素个数
	// 第3个参数用于指定容量,不够会自动扩容
	s1 = make([]int, 5, 10)
	s1 = append(s1, 9)
	t.Logf("s1:%v, len:%v, cap:%v", s1, len(s1), cap(s1))

	var m1 map[string]int
	t.Log(m1 == nil)
	// m1["a"] = 1               // 没分配空间,会报错
	m1 = make(map[string]int, 5) // 第2个参数用于指定初始空间,不够会自动扩容
	m1["a"] = 1
	t.Log(m1)

	var c1 chan int
	// c1 <- 1	// 没分配空间,会报错
	c1 = make(chan int, 0) // 第2个参数表示缓冲空间
	// c1 <- 1                                 		// 无缓冲空间,主线程发送会导致死锁
	go func() { c1 <- 1; t.Log("发送1成功") }() // 开一个线程往c1发送值
	t.Logf("接收到%v", <-c1)                   // 获取不到值就阻塞
}
posted @ 2020-04-26 01:32  爱Code的王饱饱  阅读(653)  评论(1编辑  收藏  举报