Go中的数据类型、指针、new和make
值类型与引用类型
《Go语言圣经》中将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。
- 基础类型:数字、字符串和布尔型。
- 复合数据类型:数组、结构体。是通过组合简单类型,来表达更加复杂的数据结构。
- 引用类型:指针、切片、字典、函 数、通道。
- 接口类型:通过type name interface定义的接口
我们可以根据数据赋值时的行为将数据类型分为2类,值类型与引用类型。其中值类型复制的时候得到的是数据的一份拷贝,引用类型赋值的时候得到的是引用地址的一份拷贝。接口和函数可以看做是引用类型。
具体如下:
- 值类型有
bool
、int系列
(int、int8、int16、int32、int64、uint、uintptr、uint8、uint16、uint32、uint64、byte、rune)、float系列
(float64、float32)、complex系列
(complex64、complex128)、string
、数组
、struct
- 引用类型有
值类型对应的指针
,map
、slice
、chan
注意,
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"
可以输出变量的地址,不管是传递给变量还是传递给函数,传递的是引用地址的拷贝,所以outerSlice
、sliceCopy
、innerSlice
的addr
都一样,但是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
进行比较的。slice
、map
、func
则只能判断是否等于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()
用于为slice
、map
、chan
三种引用类型分配空间。
传入值类型:
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()
用于为slice
、map
、chan
三种引用类型分配空间,声明这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) // 获取不到值就阻塞
}