复合数据类型介绍
一 什么是复合数据类型
基本数据类型是Go语言世界中的原子,以不同的方式组合基本数据类型得到的就是复合数据类型。复合类型是通过组合基础类型,来表达更加复杂的数据结构,即使用其他类型定义的类型,因而复合类型又称之为派生类型。本章主要介绍:指针、数组array、切片slice、字典map、结构体。
数据类型分为值类型与引用类型,在学习复合数据类型前,我们需要研究一下
二 值类型与引用类型
- 1、数值型复合类型:
- 2.1 数组(array)
- 2.2 结构体(struct)
- 2、引用型复合类型:
- 3.1 指针(pointer)
- 3.2 切片(slice)
- 3.3 字典(map)
- 3.4 通道(chan)
- ps:函数也属于引用类型
基础类型(数字、字符串、字符、布尔)以及数值型复合类型都属于值类型
// 值类型的特点
1、对于值类型,即便我们在声明的时候都没有为其初始化值,那么编译器会依据它们的零值为它们申请好内存空间(分配于栈上),所以对于值类型我们都不必费心为其申请内存空间
var a int //int类型默认值为 0
var b string //string类型默认值为 nil空
var c bool //bool类型默认值为false
var d [2]int //数组默认值为[0 0]
fmt.Println(&a) //默认已经分配内存地址,所以我们可以直接使用&操作,取到的是一个合法的内存地址,而非nil
2、值类型的变量赋值操作,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,如果修改某个变量的值i=3,不会影响另一个
例1:
var a =10 //定义变量a
b := a //将a的值赋值给b
b = 101 //修改b的值,此时不会影响a
fmt.Printf("值%v,内存地址%p\n",a,&a) //a的值是10,a的内存地址是0xc42000e228
fmt.Printf("值%v,内存地址%p\n",b,&b) //b的值是101,b的内存地址是0xc42000e250
例2:
var c =[3]int{1,2,3} //定义一个长度为3的int类型的数组
d := c //将数组c赋值给d
d[1] = 100 //修改数组d中索引为1的值为100
fmt.Printf("值%v,内存地址%p\n",c,&c) //c的值是[1 2 3],c的内存地址是0xc42000a180
fmt.Printf("值%v,内存地址%p\n",d,&d) //d的值是[1 100 3],d的内存地址是0xc42000a1a0
引用型复合类型(指针、切片、字典、通道)、函数、接口类型都属于引用类型。
// 引用类型的特点
1、对于引用类型,如果我们在声明的时候没有为其初始化“值”,那么默认零值为nil,nil代表空,编译器不会为其分配内存,而引用类型存在的意义在于引用了一个已经存在的内存空间,所以对于引用类型我们必须为其申请好内存才可以使用,需要用到函数new()和make()
new()与make()的异同
相同点:
I、都是针对引用类型的内存空间分配操作
II、分配的内存都在堆上
III、第一个参数都是一个类型,而不是一个值
不同点:
I、make 只用于引用类型map, slice, channel的内存分配,返回的还是这三个引用类型本身,而new返回的是一个指针。
II、new多用于匿名变量的空指针操作,如new(int),new(string), new(array)等,而且内存置为零,此外,大多数场景下new并不常用,
2、引用类型的变量赋值操作,如 j = i ,当然也是在内存中将 i 的值拷贝给了j,但是因为引用类型的变量直接存放的就是一个内存地址值(这个地址值指向的空间存的才是值),即i与j都是同一个地址。所以通过i或j修改对应内存地址空间中的值,另外一个也会修改。
例如
var a = []int{1,2,3,4,5}
b := a //此时a,b都指向了内存中的[1 2 3 4 5]的地址
b[1] = 10 //相当于修改同一个内存地址,所以a的值也会改变
c := make([]int,5,5) //切片的初始化
copy(c,a) //将切片a拷贝到c
c[1] = 20 //copy是拷贝值、创建了新的底层数组,所以a不会改变
fmt.Printf("值%v,内存地址%p\n",a,&a) //a的值是[1 10 3 4 5],a的内存地址是0xc42000a180
fmt.Printf("值%v,内存地址%p\n",b,&b) //b的值是[1 10 3 4 5],b的内存地址是0xc42000a1a0
fmt.Printf("值%v,内存地址%p\n",c,&c) //c的值是[1 20 3 4 5],c的内存地址是0xc42000a1c0
d := &a //将a的内存地址赋值给d,取值用*d
a[1] = 11
fmt.Printf("值是%v,内存地址%p\n",*d,d) //d的值是[1 11 3 4 5],d的内存地址是0xc420084060
fmt.Printf("值是%v,内存地址%p\n",a,&a) //a的值是[1 11 3 4 5],a的内存地址是0xc420084060
a,b底层数组是一样的,即操作的都是同一个底层数组,但是上层切片不同,所以内存地址不一样。
强调1:引用类型的零值是nil,而nil只能与nil做相等性判断
// 例
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil 此时不等于nil,而该切片仍然是空,所以如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断
// 思考题
var a []string //声明一个字符串切片,初始零值为?
fmt.Println(a == nil) //true
var b []int // 声明一个整型切片,初始零值为?
fmt.Println(b == nil) //true
var c = []int{} //声明一个整型切片并初始,化初始零值为?
fmt.Println(c == nil) //false
强调2:关于函数
函数的参数传递机制是将实参的值拷贝一份给形参.只不过这个实参的值有可能是地址, 有可能是数据.
所以, 函数传参的传递其实本质全都是值传递,只不过该值有可能是数据(通常被简称为”值传递“),也有可能是地址(通常被简称为”引用传递“).