复合类型
分类
复合类型的分类如下图
需要注意的是 在Go语言中 空的表示是用 nil 而不是 nullptr 或者是 NULL
指针
指针是一个代表着某个内存地址的值 该内存地址往往是内存中存储着另一个变量的值的起始位置
Go语言对于指针的支持介于Java和C++之间
它既没有像Java哪有取消了代码对指针直接操作的能力
也避免了C++对于指针滥用而造成的安全和可靠性问题
基本操作
Go语言虽然保留了指针 但是和其他编程语言不同的是
- 默认值是nil 没有NULL 常量
- 操作符是 & 取地址 * 通过指针直接访问对象 (这一点和C++相同)
- 不支持指针运算 不支持->运算符 直接使用 “.” 访问目标成员
new函数
表达式new(T)将会创建一个T类型的匿名变量 我们可以使用一个T*的指针来接受结果
之后使用指针的各种操作进行访问修改即可
需要注意的是 我们无需关心new出来对象的声明周期 Go元的内存管理系统会帮我们打理一切
指针作为函数的参数
指针作为函数的参数使用和C++并无过多区别 这里就不在赘述
数组
概述
数组指的是一系列同一类型数据的集合
数组中包含的每个数据被称为数组元素 一个数组包含的元素个数被称为数组长度
注意! 和C语言一样 Go语言不支持变长数组 所以说数组的长度必须是一个常量
像是下面的写法就会报错
var n int = 10
var arr [n]int // 不能这么写 会报错
操作数据
数组的每个元素可以通过索引下标来访问 索引下标的范围是从0开始到数组长度减1的位置
这里有几个内置函数我们可以使用
- len 返回数组的长度
- cap 返回元素的数量
数组初始化
我们有下面几种初始化数组的方式
a := [3]int{1 , 2, 3}
b := [...]int{1 , 2, 3}
C := [5]int{2 : 100 , 4 : 200}
当然Go语言也支持多维数组的开辟 规则类似于C语言
我们可以自动推导行的大小 不能自动推导列的大小
数组比较
值得一提的是 Go语言中的数组可以使用 == 或者是 != 来进行比较
它会返回给我们一个bool类型 我们可以在进行数组校验的时候使用
在函数之间传递数组
在Go语言中并没有引用传参这种说法 所以说要想传递一个函数 我们只能通过传值和传指针的方式
slice
概述
数组的长度在定义之后无法再次修改 数组是值类型 每次传递都将会产生一个副本
显然这种数据结构无法满足开发者的真实需求 Go语言提供了数组切片(slice)来弥补数组的不足
切片并不是数组或数组指针 它通过内部指针和相关属性引用数组片段 以实现变长方案
slice并不是真正意义上的动态数组 而是一个引用类型 slice总是指向一个底层array slice的声明也可以像array一样 不需要长度
如下图
切片的创建和初始化
slice和数组的区别就是
- 声明数组时 方括号内写明了数组的长度或者 … 来自动计算长度
- 声明切片时方括号内没有任何的字符
s5 := []int{1, 2, 3} //创建切片并初始化
切片操作
切片的相关操作如下
演示结果如下
这里简单介绍下切片操作的规则
- low表示从哪里开始
- high表示到哪里结束
- 最后的max表示容量 如果说指定了多少 那就是多少 如果没指定则用总容量减去起始位置
切片和底层数组关系
其实我们一开始介绍的时候就说过了 切片和底层数组之间是引用的关系
所以说如果我们修改切片的值 那么底层数组的值也会被改变的
内建函数
append
append函数的是想 slice 尾部添加数据 并且会返回一个新的slice对象
var s1 []int //创建 nil 切换
//s1 := make([]int, 0)
s1 = append(s1, 1) //追加 1 个元素
s1 = append(s1, 2, 3) //追加 2 个元素
s1 = append(s1, 4, 5, 6) //追加 3 个元素
fmt.Println(s1) //[1 2 3 4 5 6]
s2 := make([]int, 5)
s2 = append(s2, 6)
fmt.Println(s2) //[0 0 0 0 0 6]
s3 := []int{1, 2, 3}
s3 = append(s3, 4, 5)
fmt.Println(s3)//[1 2 3 4 5]
这里有一个问题是 切片的容量是有限的 万一有效数据超过容量了 此时应该怎么办呢
答案是切片会自动扩容 通常会以2倍的容量重新分配底层数组 并赋值原来的数据
copy
函数copy可以在两个slice之间复制数据 复制长度以len小的为准 两个slice可指向同一底层数组
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:] //{8, 9}
s2 := data[:5] //{0, 1, 2, 3, 4}
copy(s2, s1) // dst:s2, src:s1
fmt.Println(s2) //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]
这里需要注意的是复制源和目的地址的问题 是将后面的一个切片赋值道前面
切片作为函数传参
如果我们此时修改切片内部的值 那么此时外部切片的值也会改变
但是如果我们在函数内部将切片扩容了 那么此时就算我们在函数内部修改了切片 外部的切片也不会改变
map
概述
Go语言中的map是一种内置的数据结构 它是一个 无序
的key – value集合
map的格式为
map[keyType]valueType
在一个map里所有的建都是唯一的 而且必须是支持 == 和 !=操作的
切片 函数以及包含切片的结构类型 由于这些类型具有引用语义 不能作为映射的建 使用这些类型会造成编译错误
但是map的值可以是任意类型
需要注意的是 map是无序的 我们无法决定它的返回顺序 所以说每次打印的结果都有可能不同
其实看到这里我们就应该明白了 Go语言中map的底层使用了哈希表而不是红黑树
创建和初始化
创建方式如下
var m1 map[int]string {}
此时我们创建了一个空的map
初始化方式如下
m2 := map[int]string {1 : "mike" , 2 : "yoyo"}
常用操作
赋值
我们可以使用 []
来对map进行修改和赋值
它的逻辑可以参考我直接写的这篇博客
遍历
map的遍历方式有两种
- 我们可以直接通过range获取key 和 value 之后打印即可
- 我们可以只获取下标 然后通过下标访问额度方式来获取值
如果我们想要判断某个key对应的value是否存在的话 我们可以像下面这么做
value , ok := m2[3]
fmt.Println(value , ok) // 如果ok是true则存在 如果是false则不存在
删除
我们可以使用delete函数来进行删除
具体用法如下
delete(map名 , key值)
map作函数参数
在函数之间传递map的时候并不会制造出该映射的一个副本 不是值传递 而是引用传递
到这里我们总结下 回顾之前复合类型的分类图
我们可以发现 只有slice 和 map传递的时候是使用了引用类型 这一点比较奇怪 我们记住即可
结构体
结构体类型
我们单一的结构没办法描述一个复杂的对象 比如说学生 一个学生要有学号 姓名 年龄 地址等属性
但是如果我们一个个定义的话比较繁琐 虽然单独定义以上的变量比较繁琐 数据不便于管理
结构体初始化
普通变量
下面就是我们结构体中定义普通变量的方式
type Student Struct {
id int
name string
address string
}
对于普通变量我们有两种初始化方式
- 全部初始化 这种初始化必须要按照顺序来
type Student Struct {
id int
name string
address string
}
- 指定初始化某个成员 没有初始化的成员为零值
像我们下面的代码
// 部分初始化
s2 := Student{id : 2}
指针变量
其实指针变量的初始化和普通变量差别并不大 只是语法上有些许的变化
具体的代码如下
// 此时s3就是一个指针
s3 := &Student{ 2 , "lili" , "addd"}
结构体成员的使用
在Go语言中 不管是普通变量 还是 指针变量 我们的访问方式都是不变的 所以说我们都可以使用 . 来访问并修改每个成员
结构体的比较
如果结构体的全部成员都是可以比较的 结构体是支持比较的 就像数组一样 但是只支持 == 和 != 不支持其他
结构体作为函数参数
结构体的传参很有意思 因为我们不管是普通成员还是指针都是使用 . 来访问并且修改成员变量的 这就导致 如果我们传递一个指针进去的话 就很类似于C++中的引用传参
所以说结构体作为函数传参有两种情况
- 值传递
- 引用传递 (底层还是传地址 指针)
可见性
Go语言对关键字的增加非常的吝啬 (这也可能是为了易学习性考虑)其中没有 private public 这些关键字
我们要让某个符号对其他包可见的话 需要将该符号定义以大写字母开头
比如说
type Student Struct //对其他包可见
type student Struct //对其他包不可见 因为首字母小写