一.结构体
现在有一个需求,要求存储学生的详细信息,例如,学生的学号,学生的姓名,年龄,家庭住址等。按照以前学习的存储方式,
可以以如下的方式进行存储:
var id int var name string var sex string var age int var addr string
通过定义变量的信息,进行存储。但是这种方式,比较麻烦,并且不利于数据的管理。
在Go语言中,我们可以通过结构体来存储以上类型的数据。
那什么是结构体呢?
结构体(struct)是用户自定义的类型,它代表若干字段的集合,可以用于描述一个实体对象,类似java中的class,是Go语言面向对象编程的基础类型。
1.1 结构体定义
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体有中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct { member definition; member definition; ... member definition; }
type 后面跟着的是结构体的名字, struct表示定义的是一个结构体,一个定义好的结构体是一种类型。
大括号中是结构体的成员和成员属性,注意在定义结构体成员时,不要加var,示例如下:
type Student struct { Id int Name string Sex string Age int Addr string }
通过以上的定义,大家能够感觉出,通过结构体来定义复杂的数据结构,非常清晰。
1.2 结构体初始化
结构体定义完成后,可以进行初始化。
在定义结构体类型变量的同时为其初始化(我就直接用上面定义好的结构体)
func main() { //定义结构体类型变量 var stu Student = Student{1001, "张三", "male", 28, "北京"} fmt.Println(stu) }
注意:顺序初始化,每个成员必须初始化,在初始化时,值的顺序与结构体成员的顺序保持一致。
结果如下:
{1001 张三 male 28 北京}
初始化后,可以获取结构体成员信息,方法是:结构体变量.成员名
func main() { //定义结构体类型变量 var stu Student = Student{1001, "张三", "male", 28, "北京"} fmt.Println("stu.Id = ", stu.Id) fmt.Println("stu.Name = ", stu.Name) fmt.Println("stu.Age = ", stu.Age) }
结果如下:
stu.Id = 1001 stu.Name = 张三 stu.Age = 28
也可以在初始化后,在修改结构体成员信息,方法是:结构体变量.成员信息 = 值
func main() { //定义结构体类型变量 var stu Student = Student{1001, "张三", "male", 28, "北京"} fmt.Println("------修改前------") fmt.Println("stu.Id = ", stu.Id) fmt.Println("stu.Name = ", stu.Name) fmt.Println("stu.Age = ", stu.Age) stu.Id = 1005 stu.Name = "李四" stu.Age = 30 fmt.Println("------修改后------") fmt.Println("stu.Id = ", stu.Id) fmt.Println("stu.Name = ", stu.Name) fmt.Println("stu.Age = ", stu.Age) }
结果如下:
------修改前------ stu.Id = 1001 stu.Name = 张三 stu.Age = 28 ------修改后------ stu.Id = 1005 stu.Name = 李四 stu.Age = 30
1.3 结构体内存存储
先看一下下面的代码:
type Student struct { Id int Name string Sex string Age int Addr string } func main() { //通过自动推导类型定义结构体变量 stu := Student{1001, "王五", "male", 27, "BJ"} //计算结构体在内存中占的字节大小 fmt.Println("Size(Student) = ", unsafe.Sizeof(stu)) }
结果是:
Size(Student) = 64
这个64是怎么算出来的呢?
我们看一次Student结构体中各成员的数据类型:Id和Age是int类型,内存中占8个字节;Name,Sex和Addr是string类型,内存中占16个字节;8+8+16+16+16=64,结构体在内存中占的字节大小就等于结构体中个成员的数据类型在内存中占字节大小的和。我们再看一下下面的代码:
type Student struct { Id int Name string Sex string Age int Addr string like []string } func main() { //通过自动推导类型定义结构体变量 stu := Student{1001, "王五", "male", 27, "BJ", []string{"唱", "跳", "RAP", "篮球"}} //计算结构体在内存中占的字节大小 fmt.Println("Size(Student) = ", unsafe.Sizeof(stu)) }
这个结构体相比前面的结构体,多了一个like成员,数据类型是字符串切片,那现在这个结构体再内存中占多大字节呢?
结果如下:
Size(Student) = 88
因为切片在内存中占24个字节,64+24 = 88
1.4 结构体的赋值和比较
相同结构体类型的变量可以互相赋值,且为互相独立的个体。
func main() { //通过自动推导类型定义结构体变量 stu := Student{1001, "王五", "male", 27, "BJ"} //结构体赋值给另外一个结构体 stu1 := stu //修改结构体信息 stu1.Name = "赵四" fmt.Println("stu = ", stu) fmt.Println("stu1 = ", stu1) fmt.Printf("%p ", &stu) fmt.Printf("%p ", &stu1) }
结果如下:
stu = {1001 王五 male 27 BJ} stu1 = {1001 赵四 male 27 BJ} 0xc000048040 0xc000048080
两个结构体变量还可以判断内容是否相同,注意两个结构体变量只能比较内容是否相同:==或!=,不能比较大小。
type Student struct { Id int Name string Sex string Age int Addr string } func main() { //通过自动推导类型定义结构体变量 stu := Student{1001, "王五", "male", 27, "BJ"} //结构体赋值给另外一个结构体 stu1 := stu fmt.Println("------修改前------") if stu1 == stu { fmt.Println("内容相同") } else { fmt.Println("内容不相同") } //修改结构体信息 stu1.Name = "赵四" fmt.Println("------修改后------") if stu1 == stu { fmt.Println("内容相同") } else { fmt.Println("内容不相同") } }
结果如下:
------修改前------ 内容相同 ------修改后------ 内容不相同
1.5 结构体数组
结构体数组就是结构体的数组,表示一个数组中元素都是结构体类型变量。结构体数组定义格式如下:
var 数组名 [元素个数]结构体名 = [元素个数]结构体名{}
演示如下:
type Student struct { id int name string sex string age int } func main() { var array [5]Student = [5]Student{ {1001, "曹操", "男", 53}, {1002, "赵云", "男", 27}, {1003, "典韦", "男", 37}, {1004, "张飞", "男", 31}, {1005, "鲁肃", "男", 45}, //如果每个结构体变量元素是分行定义的,最后一下结构体变量也是要加上,的 } fmt.Println(array) fmt.Printf("type(array) = %T ", array) }
结果如下:
[{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45}] type(array) = [5]main.Student
可以通过循环的方式,将结构体数组中的每一项进行输出。
func main() { var array [5]Student = [5]Student{ {1001, "曹操", "男", 53}, {1002, "赵云", "男", 27}, {1003, "典韦", "男", 37}, {1004, "张飞", "男", 31}, {1005, "鲁肃", "男", 45}, //如果每个结构体变量元素是分行定义的,最后一下结构体变量也是要加上,的 } for _, data := range array { fmt.Println(data) } }
结果如下:
{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45}
1.6 结构体切片
结构体切片就是结构体的切片,表示一个切片中元素都是结构体类型变量。结构体切片定义格式如下:
var 切片名 []结构体名 = []结构体名{}
演示如下:
type Student struct { id int name string sex string age int } func main() { var slice []Student = []Student{ {1001, "曹操", "男", 53}, {1002, "赵云", "男", 27}, {1003, "典韦", "男", 37}, {1004, "张飞", "男", 31}, {1005, "鲁肃", "男", 45}, //如果每个结构体变量元素是分行定义的,最后一下结构体变量也是要加上,的 } fmt.Println(slice) fmt.Printf("type(slice) = %T ", slice) }
结果如下:
[{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45}] type(slice) = []main.Student
同样也可以进行切片的追加,截取和复制:
func main() { var slice []Student = []Student{ {1001, "曹操", "男", 53}, {1002, "赵云", "男", 27}, {1003, "典韦", "男", 37}, {1004, "张飞", "男", 31}, {1005, "鲁肃", "男", 45}, //如果每个结构体变量元素是分行定义的,最后一下结构体变量也是要加上,的 } //向切片追加数据 fmt.Println("------切片的追加------") slice = append(slice, Student{1006, "关羽", "男", 35}, Student{1007, "马超", "男", 30}, ) fmt.Println(slice) //切片的截取 fmt.Println("------切片的截取------") slice1 := slice[2:4] fmt.Println(slice1) //切片的复制 fmt.Println("------切片的复制------") slice2 := make([]Student, len(slice)) copy(slice2, slice) fmt.Println(slice2) }
结果如下:
------切片的追加------ [{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45} {1006 关羽 男 35} {1007 马超 男 30}] ------切片的截取------ [{1003 典韦 男 37} {1004 张飞 男 31}] ------切片的复制------ [{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45} {1006 关羽 男 35} {1007 马超 男 30}]
同样可以通过循环的方式,将结构体切片中的每一项进行输出。
func main() { var slice []Student = []Student{ {1001, "曹操", "男", 53}, {1002, "赵云", "男", 27}, {1003, "典韦", "男", 37}, {1004, "张飞", "男", 31}, {1005, "鲁肃", "男", 45}, //如果每个结构体变量元素是分行定义的,最后一下结构体变量也是要加上,的 } for _, data := range slice { fmt.Println(data) } }
结果如下:
{1001 曹操 男 53} {1002 赵云 男 27} {1003 典韦 男 37} {1004 张飞 男 31} {1005 鲁肃 男 45}
1.7 map中的value是结构体
map是一种key:value结构的数据类型,当map的value是结构体类型时该如何定义。
type Student struct { name string sex string age int addr string } func main() { //定义map,其中value是结构体类型 m := map[int]Student{ 1001: Student{"路飞", "男", 18, "风车村"}, 1002: Student{"娜美", "女", 16, "未知"}, 1003: Student{"乔巴", "未知", 13, "雪乡"}, 1004: Student{"山治", "男", 20, "厨房"}, } //循环打印map的key和value for k, v := range m { fmt.Printf("key : %v , value : %v ", k, v) } }
结果如下:
key : 1001 , value : {路飞 男 18 风车村} key : 1002 , value : {娜美 女 16 未知} key : 1003 , value : {乔巴 未知 13 雪乡} key : 1004 , value : {山治 男 20 厨房}
为map添加数据和删除数据:
func main() { //定义map,其中value是结构体类型 m := map[int]Student{ 1001: Student{"路飞", "男", 18, "风车村"}, 1002: Student{"娜美", "女", 16, "未知"}, 1003: Student{"乔巴", "未知", 13, "雪乡"}, 1004: Student{"山治", "男", 20, "厨房"}, } //添加数据 fmt.Println("------添加数据------") m[1005] = Student{"索隆", "男", 17, "忘了"} for k, v := range m { fmt.Printf("key=%v , value=%v ", k, v) } //删除数据 fmt.Println("------删除数据------") delete(m, 1003) for k, v := range m { fmt.Printf("key=%v , value=%v ", k, v) } }
结果如下:
------添加数据------ key=1001 , value={路飞 男 18 风车村} key=1002 , value={娜美 女 16 未知} key=1003 , value={乔巴 未知 13 雪乡} key=1004 , value={山治 男 20 厨房} key=1005 , value={索隆 男 17 忘了} ------删除数据------ key=1002 , value={娜美 女 16 未知} key=1004 , value={山治 男 20 厨房} key=1005 , value={索隆 男 17 忘了} key=1001 , value={路飞 男 18 风车村}
1.8 map中的value是结构体切片
当map中的value是结构体类型时,该如何定义:
type Student struct { name string marry bool age int addr string } func main() { //定义map,其中value是结构体切片类型 m := map[int][]Student{ 1001: []Student{ {"林冲", true, 22, "梁山"}, {"武松", false, 20, "阳谷"}, {"卢俊义", true, 32, "河北"}, }, 1002: []Student{ {"曹操", true, 58, "河南"}, {"赵云", false, 21, "正定"}, }, 1003: []Student{ {"悟空", false, 520, "花果山"}, {"悟净", false, 620, "流沙河"}, {"悟能", false, 420, "高老庄"}, {"悟道", true, 100, "东非大裂谷"}, }, } //修改value中的数据 m[1003][3].name = "悟禅" for k, v := range m { fmt.Printf("key=%v , value=%v ", k, v) } }
结果如下:
key=1002 , value=[{曹操 true 58 河南} {赵云 false 21 正定}] key=1003 , value=[{悟空 false 520 花果山} {悟净 false 620 流沙河} {悟能 false 420 高老庄} {悟禅 true 100 东非大裂谷}] key=1001 , value=[{林冲 true 22 梁山} {武松 false 20 阳谷} {卢俊义 true 32 河北}]
1.9 结构体作为函数参数
结构体也可以作为函数参数,进行传递,如下所示:
type Student struct { id int name string sex string age int } func ChangeStruct(stu Student) { stu.id = 1002 stu.name = "李四" stu.sex = "female" stu.age = 20 } func main() { stu := Student{1001, "张三", "male", 28} fmt.Println("------修改前------") fmt.Println(stu) ChangeStruct(stu) fmt.Println("------修改后------") fmt.Println(stu) }
结果如下:
------修改前------ {1001 张三 male 28} ------修改后------ {1001 张三 male 28}
我们发现修改前和修改后的值没有变化,这是因为结构体作为函数参数进行传递,是值传递,形参无法修改实参的值,想要得到修改后的结果一是添加返回值,而是用之后会讲到的指针。
现在我们仍然用返回值的方式:
func ChangeStruct(stu Student) Student { stu.id = 1002 stu.name = "李四" stu.sex = "female" stu.age = 20 return stu } func main() { stu := Student{1001, "张三", "male", 28} fmt.Println("------修改前------") fmt.Println(stu) res := ChangeStruct(stu) fmt.Println("------修改后------") fmt.Println(res) }
结果如下:
------修改前------ {1001 张三 male 28} ------修改后------ {1002 李四 female 20}
二.指针
了解指针之前,先回顾一下什么是变量。
每当我们编写任何程序时,我们都需要在内存中存储一些数据/信息。数据存储在特定地址的存储器中。内存地址看起来像0xAFFFF
(这是内存地址的十六进制表示)。
现在,要访问数据,我们需要知道存储它的地址。我们可以跟踪存储与程序相关的数据的所有内存地址。但想象一下,记住所有内存地址并使用它们访问数据会有非常困难。这就是为什么引入变量。
变量是一种占位符,用于引用计算机的内存地址,可理解为内存地址的标签。
什么是指针
指针是存储另一个变量的内存地址的变量。所以指针也是一种变量,只不过它是一种特殊变量,它的值存放的是另一个变量的内存地址。
2.1 变量的内存和地址
前面我们讲过存储数据的方式,可以通过变量,或者复合类型中的数组,切片,Map,结构体。
我们不管使用变量存储数据,还是使用复合类型存储数据,都有两层的含义:
存储的数据(内存),对应的地址。
接下来,通过变量来说明以上两个含义。例如,定义如下变量:
func main() { var i int i = 100 fmt.Printf("i=%d ", i) fmt.Printf("&i=%p ", &i) }
第一个Printf( )函数的输出,大家都很熟悉,输出变量i的值,这个实际上就是输出内存中存储的数据。在前面,已经讲解过,定义一个变量,就是在内存中开辟一个空间,用来存储数据,当给变量i赋值为100,其实就是将100存储在该空间内。
第二个Printf( )函数的输出,输出的是变量i在内存中的地址。通过如下图来给大家解释:
这张图,大家也应该非常熟悉,是在讲解变量时,画的一张图,0x100010假设是变量i的内存地址(通过第二个输出可以获取实际的地址),内存地址的作用:在输出变量中存储的数据时,是通过地址来找到该变量内存空间的。
这个内存地址和实际生活中的地址也很相似,例如:大家可以将内存空间想象成,我们上课的教室,教室中存放有学生,那么现在要找一个学生,必须要知道具体的地址以及教室门牌号。
上面代码的结果是:
i=100 //内存中存储的值 &i=0xc00008e000 //指向存储该数据的内存空间的地址
2.2 指针变量
现在已经知道怎样获取变量在内存中的地址,但是如果想将获取的地址进行保存,应该怎样做呢?
可以通过指针变量来存储,所谓的指针变量:就是用来存储任何一个值的内存地址。
指针变量的定义如下:
var 指针变量名 *变量数据类型
这里的变量数据类型是指该指针变量存储的地址指向的内存空间中存储数据的数据类型。
func main() { //定义变量 var i int = 100 //定义指针变量 var ptr *int //初始化指针变量,用变量i的地址给ptr赋值 ptr = &i fmt.Printf("i=%d,ptr=%p ", i, ptr) }
指针变量ptr的定义是通过*这个符号来定义,指针变量ptr的类型为*int, 表示存储的是一个整型变量的地址。
如果指针变量ptr存储的是一个字符串类型变量的地址,那么指针变量ptr的类型为*string
ptr=&i:该行代码的意思是,将变量i的地址取出来,并且赋值给指针变量ptr。也就是指针变量ptr指向了变量i的存储单元。
可以通过如下图来表示:
在以上图中,一定要注意:指针变量ptr存储的是变量i的地址。
大家可以思考一个问题:
既然指针变量ptr指向了变量i的存储单元,那么是否可以通过指针变量p,来操作变量i中存储的数据?
答案是可以的,具体操作方式如下:
func main() { //定义变量 var i int = 100 //定义指针变量 var ptr *int //初始化指针变量,用变量i的地址给ptr赋值 ptr = &i *ptr = 80 fmt.Printf("i=%d,ptr=%p ", i, ptr) }
注意:在使用指针变量p来修改变量i的值的时候,前面一定要加上*.(通过指针访问目标对象)
现在打印变量i的值已经有100变为80。
这种*ptr的格式叫做解引用,解引用的格式是*指针变量(前面一定要加上*),*ptr就是把 ptr 的值取出来,当作地址来看待,找到该地址对应的内存空间,然看在判断其是左值还是右值。如果是左值(左值就是等号左边的变量,指代变量所指定的内存空间),就取空间;如果是右值(右值就是等号右边的变量,指代变量所指定的内存空间中数据值),就取空间值。
所以,*ptr的作用就是根据存储的变量的地址,来操作变量的存储单元(包括输出变量存储单元中的值,和对值进行修改)
2.3 注意事项
(1)默认值为nil
先看下面的代码:
func main() { //声明指针变量 var p *int fmt.Println(p) }
输出结果是:
<nil>
由此可知指针变量的默认值是nil。
(2)不要操作没有合法指向的内存
此时我们直接对此指针变量进行解引用赋值:
func main() { //声明指针变量 var p *int *p = 56 fmt.Println(p) }
执行结果如下:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1092f4f]
发生了异常,我们此时看一下指针变量p存储的地址是多少:
func main() { //声明指针变量 var p *int fmt.Printf("%p ", p) }
结果如下:
0x0
只声明的指针变量存储的地址是指向 0 号地址空间(被操作系统占用,不允许用户使用),所以,在使用指针变量时,一定要让指针变量有正确的指向。以下的操作是合法的:
func main() { var a int //声明指针变量 var p *int p = &a *p = 56 fmt.Printf("%d,%v", a, *p) }
结果如下:
56,56
在该案例中,定义了一个变量a,同时定义了一个指针变量p,将变量a的地址赋值给指针变量p,也就是指针变量p指向了变量a的存储单元。给指针变量p赋值,影响到了变量a.最终输出变量a中的值也是56。
(3)野指针
有上面我们知道,只声明的指针变量是不能使用的,要让指针变量有正确的指向。那我们直接为指针变量赋值一个地址可以吗?看下面的代码:
func main() { //声明指针变量 var p *int p = 0xc000098000 *p = 123 fmt.Printf("p=%p,*p=%v", p, *p) }
结果如下:
cannot use 824634343424 (type int) as type *int in assignment
将一个地址直接赋值给指针变量是不可以的,因为这样的地址属于无效的空间地址,也叫野指针。
2.4 new()函数
指针变量,除了以上介绍的指向以外(p=&a),还可以通过new( )函数来指向。
具体的应用方式如下:
func main() { //声明指针变量,初始值为nil var p *int p = new(int) //p := new(int) //上面两句话,用自动推导类型一步完成 *p = 123 fmt.Printf("p=%p,*p=%v ", p, *p) }
结果如下:
p=0xc00007e008,*p=123
new(int)作用就是创建一个整型大小的空间。
然后让指针变量p指向了该空间,所以通过指针变量p进行赋值后,该空间中的值就是123。
new( )函数的作用就是C语言中的动态分配空间。但是在这里与C语言不同的地方,就是最后不需要关心该空间的释放。GO语言会自动释放。这也是比C语言使用方便的地方。
2.5 指针的大小和指针的运算
(1)指针的大小
指针的大小与数据类型无关,受操作系统影响,64位系统中 8字节,32位系统 4字节。
数据类型,在指针解引用时,会产生用作,按类型所对应字节数,读取内存单元个数,获取数据。
我们先看一下各个数据类型在内存占的字节数:
type Student struct { Id int Name string Age int } func main() { var a int = 1 var b float64 = 3.13 var c byte = 'a' var d string = "hello" var e bool = true var f [5]int = [5]int{1, 2, 3, 4, 5} var g []int = []int{1, 2, 3, 4, 5} var h map[int]string = map[int]string{1: "a", 2: "b", 3: "c"} var i Student = Student{1, "张三", 26} fmt.Printf("size(a)=%v ", unsafe.Sizeof(a)) fmt.Printf("size(b)=%v ", unsafe.Sizeof(b)) fmt.Printf("size(c)=%v ", unsafe.Sizeof(c)) fmt.Printf("size(d)=%v ", unsafe.Sizeof(d)) fmt.Printf("size(e)=%v ", unsafe.Sizeof(e)) fmt.Printf("size(f)=%v ", unsafe.Sizeof(f)) fmt.Printf("size(g)=%v ", unsafe.Sizeof(g)) fmt.Printf("size(h)=%v ", unsafe.Sizeof(h)) fmt.Printf("size(i)=%v ", unsafe.Sizeof(i)) }
结果如下:
size(a)=8 size(b)=8 size(c)=1 size(d)=16 size(e)=1 size(f)=40 size(g)=24 size(h)=8 size(i)=32
接下来我们再看一下各数据类型的指针在内存中占的字节数(我的电脑是64位的):
fmt.Printf("size(&a)=%v ", unsafe.Sizeof(&a)) fmt.Printf("size(&b)=%v ", unsafe.Sizeof(&b)) fmt.Printf("size(&c)=%v ", unsafe.Sizeof(&c)) fmt.Printf("size(&d)=%v ", unsafe.Sizeof(&d)) fmt.Printf("size(&e)=%v ", unsafe.Sizeof(&e)) fmt.Printf("size(&f)=%v ", unsafe.Sizeof(&f)) fmt.Printf("size(&g)=%v ", unsafe.Sizeof(&g)) fmt.Printf("size(&h)=%v ", unsafe.Sizeof(&h)) fmt.Printf("size(&i)=%v ", unsafe.Sizeof(&i))
结果如下:
size(&a)=8 size(&b)=8 size(&c)=8 size(&d)=8 size(&e)=8 size(&f)=8 size(&g)=8 size(&h)=8 size(&i)=8
(2)指针的运算
在Go语言中,两个指针只允许比较 == 或 !=,其余的都不允许。
func main() { a := 10 b := 10 p1 := &a p2 := &b if p1 == p2 { fmt.Println("两个指针相等") } else { fmt.Println("两个指针不相等") } }
结果是:
两个指针不相等
func main() { a := 10 p1 := &a p2 := p1 if p1 == p2 { fmt.Println("两个指针相等") } else { fmt.Println("两个指针不相等") } }
结果如下:
两个指针相等
2.6 指针作为函数参数
指针也可以作为函数参数,那么指针作为函数参数在进行传递的时候,是值传递还是引用传递呢?
大家都知道,普通变量作为函数参数进行传递是值传递,如下案例所示:
定义一个函数,实现两个变量值的交换。
func swap(a, b int) { a, b = b, a } func main() { a := 10 b := 20 swap(a, b) fmt.Printf("a=%d ", a) fmt.Printf("b=%d ", b) }
结果如下:
a=10 b=20
通过以上案例,证实普通类型变量在传递时,为值传递。值传递就是将实参的值,拷贝一份赋值给形参。
那么使用指针作为函数参数呢?现在将以上案例修改成,用指针作为参数,如下所示:
func swap(a, b *int) { *a, *b = *b, *a } func main() { a := 10 b := 20 swap(&a, &b) fmt.Printf("a=%d ", a) fmt.Printf("b=%d ", b) }
结果如下:
a=20 b=10
通过以上案例证实,指针作为参数进行传递时,为引用传递,也就是传递的地址。引用传递也是将实参的值,拷贝一份赋值给 形参,但实参的值是地址值。
在调用Swap( )函数时,将变量a与变量b的地址传分别传递给指针变量num1,num2,这时num1和num2,分别指向了变量a,与变量b的内存存储单元,那么操作num1,num2实际上操作的就是变量a与变量b,所以变量a与变量b的值被交换。
指针作为函数参数,核心思想: 在 A 函数中借助地址,操作 B 函数内部的变量。
2.7 数组指针
前面在讲解数组的时候,我们用数组作为函数参数,但是数组作为参数进行传递是值传递,如果想引用传递,可以使用数组指针。
数组指针就是数组的指针,我们先看一下数组指针的简单用法:
func main() { array := [5]int{1, 2, 3, 4, 5} //定义指针变量p,存储数组中首元素地址 p := &array fmt.Printf("%T ", p) fmt.Printf("%p ", p) //解引用,修改数组中的元素 (*p)[0] = 6 //上面的写法较为繁琐,Go语言中也可以这么写 p[1] = 7 fmt.Printf("array=%v ", array) }
结果如下:
*[5]int 0xc00001a090 array=[6 7 3 4 5]
我们也可以通过new()函数初始化一个数组指针:
func main() { p := new([5]int) for i := 0; i < len(*p); i++ { p[i] = i + 1 } fmt.Printf("*p=%v ", *p) }
结果如下:
*p=[1 2 3 4 5]
再看一下数组指针作为函数参数:
func BubbleSort(array *[10]int) { for i := 0; i < len(array)-1; i++ { for j := 0; j < len(array)-1-i; j++ { if array[j] > array[j+1] { array[j], array[j+1] = array[j+1], array[j] } } } } func main() { array := [10]int{3, 6, 8, 4, 5, 1, 9, 2, 7, 10} BubbleSort(&array) fmt.Printf("array=%v ", array) }
结果如下:
array=[1 2 3 4 5 6 7 8 9 10]
2.8 指针数组
指针数组就是指针的数组,也就是数组中的每个元素都是指针。一定要跟上面的数组指针区分开。
(1)数组中,保存变量的地址
func main() { a := 10 b := 20 c := 30 var array [3]*int array[0] = &a array[1] = &b array[2] = &c for i := 0; i < len(array); i++ { fmt.Println(*array[i]) } }
结果如下:
10 20 30
指针数组的定义方式,与数组指针定义方式是不一样的,注意指针数组是将“*”放在了下标的后面。
由于指针数组存储的都是地址,所以将变量a,变量b与变量c的地址赋值给了指针数组array。
那如何通过指针变量array修改变量a,b,c的值呢?
func main() { a := 10 b := 20 c := 30 var array [3]*int array[0] = &a array[1] = &b array[2] = &c for i := 0; i < len(array); i++ { *array[i] = i + 1 } fmt.Printf("a=%d,b=%d,c=%d ", a, b, c) }
结果如下:
a=1,b=2,c=3
(2)数组中保存数组的地址
func main() { a := [3]int{1, 2, 3} b := [3]int{4, 5, 6} c := [3]int{7, 8, 9} var array [3]*[3]int array[0] = &a array[1] = &b array[2] = &c for i := 0; i < len(array); i++ { for j := 0; j < len(*array[i]); j++ { fmt.Printf("%d ", (*array[i])[j]) } fmt.Println() } }
结果如下:
1 2 3 4 5 6 7 8 9
2.9 结构体指针变量
我们前面定义了指针指向了数组,解决了数组引用传递的问题。那么指针是否可以指向结构体,也能够解决结构体引用传递的问题呢?完全可以。
下面我们先来看一下,结构体指针变量的定义:
type Student struct { id int name string score float64 } func main() { //定义结构体指针变量 var p *Student //将结构体地址赋值给p p = &Student{1001, "张三", 99.5} fmt.Println(*p) }
结果如下:
{1001 张三 99.5}
也可以使用自动推导类型
type Student struct { id int name string score float64 } func main() { //定义结构体指针变量,并赋值 p := &Student{1002, "李四", 96.5} fmt.Println(*p) }
结果如下:
{1002 李四 96.5}
现在定义了一个结构体指针变量,那么可以通过该指针变量来操作结构体中的成员项。
type Student struct { id int name string score float64 } func main() { //定义结构体指针变量,并赋值 p := &Student{1002, "李四", 96.5} (*p).name = "王五" //p.name = "王五"也可以 fmt.Println(*p) }
结果如下:
{1002 王五 96.5}
前面在讲解结构体时,用结构体作为函数的参数,默认的是值传递,那么通过结构体指针,可以实现结构体的引用传递。具体实现的方式如下:
type Student struct { id int name string score float64 } func initPerson(stu *Student) { stu.id = 1003 stu.name = "赵七" stu.score = 98 } func main() { //定义结构体指针变量,并赋值 stu := &Student{1002, "李四", 96.5} fmt.Println("old_stu = ", *stu) initPerson(stu) fmt.Println("new_stu = ", *stu) }
结果如下:
old_stu = {1002 李四 96.5} new_stu = {1003 赵七 98}
2.10 切片指针
前面解释过数组指针,其实切片指针跟数组指针类似,就是切片的指针。
先看一下下面的代码:
func appendslice(slice []int) { slice = append(slice, 5, 6, 7, 8, 9, 10) } func main() { slice := []int{1, 2, 3, 4, 5} appendslice(slice) fmt.Println(slice) }
结果是:
[1 2 3 4 5]
嗯?切片作为函数参数不是引用传递吗,为什么形参没有修改实参的值呢。这里涉及了两个方面:
(1)切片的本质
切片的本质是结构体,切片的定义源码在runtime包下的slice.go文件中,具体如下:
type slice struct { array unsafe.Pointer len int cap int }
切片有三个属性:数组指针,长度和容量。
(2)切片的扩容
每当切片在扩容时,并不是在原基础上进行扩容,是要在内存中开辟出一个新的空间,先把旧数据放入,再把扩容的数据放入。也就是说每当切片容量发生变化时,其中数组指针保存的地址都会发生变化,示例如下:
func main() { slice := []int{1, 2, 3, 4, 5} fmt.Printf("cap(slice)=%d,addr(slice)=%p ", cap(slice), slice) slice = append(slice, 6, 7, 8, 9) fmt.Printf("cap(slice)=%d,addr(slice)=%p ", cap(slice), slice) }
结果如下:
cap(slice)=5,addr(slice)=0xc00001a090 cap(slice)=10,addr(slice)=0xc00001e0a0
那怎样才能得倒修改后的切片呢?一是用返回值,二是就是用切片指针,具体实现如下:
func appendslice(slice *[]int) { *slice = append(*slice, 5, 6, 7, 8, 9, 10) } func main() { slice := []int{1, 2, 3, 4, 5} appendslice(&slice) fmt.Println(slice) }
结果如下:
[1 2 3 4 5 5 6 7 8 9 10]
所以当使用切片作为函数参数时,如果该函数会修改切片的容量,要得到修改后的切片,要么使用返回值,要么使用切片指针。
2.11 map和指针
map的value可以是任意数据格式,那么当value为指针类型时,该怎么定义呢?
(1)map中value是变量地址。方法一:直接初始化
func main() { a := 10 b := 20 c := 30 //当map的value为指针类型时 m := map[int]*int{ 1001: &a, 1002: &b, 1003: &c, } //修改value *m[1002] = 40
for _, v := range m {
fmt.Println(*v)
}
}
(2)map中的value是变量地址。方法二:使用make函数初始化
func main() { a := 10 b := 20 c := 30 m := make(map[int]*int) m[1001] = &a m[1002] = &b m[1003] = &c for _, v := range m { fmt.Println(*v) } }
(3)map中的value是指针数组
func main() { a := 10 b := 20 c := 30 m := make(map[int][3]*int) m[1001] = [3]*int{&a} m[1002] = [3]*int{&b} m[1003] = [3]*int{&c} *m[1002][0] = 40 for _, v := range m { for i := 0; i < len(v); i++ { if v[i] != nil { fmt.Println(*v[i]) } } } }
为什么上面的循环中要加上 if v[i] != nil 这个判断呢?因为这里定义的map的value时一个长度为3的整形指针数组,且每个数组中都只有一个值,那每个数组剩余的值是多少呢?是nil,因为指针的默认就是nil。
2.12 多级指针
多级指针用大白话讲就是指针的指针的指针.......
直接用代码说明吧:
func main() { var i int = 10 var p *int = &i //一级指针,存储的是变量 i 的地址 var pp **int = &p //二级指针,存储的是一级指针变量 p 的地址 var ppp ***int = &pp //三级指针,存储的是二级指针变量 pp 的地址 fmt.Println(***ppp) fmt.Println(**pp) fmt.Println(*p) }
结果如下:
10 10 10