楔子
这一次我们来介绍一下go语言中的派生数据类型,或者说高级数据类型。
指针
只要将数据存储在内存中都会为其分配内存地址,内存地址使用十六进数据表示;内存中的每一个字节都有一个32位或64位编号(与32位或64位处理器相关),对于go中的变量我们可以使用&获取其地址。
package main
import "fmt"
func main() {
var i int = 10
//使用格式化打印变量的内存地址
//%p是一个占位符输出一个十六进制地址格
fmt.Printf("%p\n", &i) //0xc0000a0068
//我们go中的变量没有赋初始值之外, 默认有一个零值, 所以已经分配好内存了
//既然分配好内存了, 那么就一定有地址
var j int
fmt.Printf("%p\n", &j) //0xc0000a00a0
}
如果想将获取的地址进行保存,应该怎样做呢?可以通过指针变量来存储,所谓的指针变量:就是用来存储任何一个值的内存地址。
package main
import "fmt"
func main() {
var a = "hello"
//格式: var 指针变量名 *类型 = &变量
//不过我们说在声明变量的时候, 如果赋初始值的话, 可以省略类型
//这里就表示将a的地址赋值给指针变量p1和p2, 指针变量也是变量, 只不过它保存的是地址, 地址指向对应的值
var p1 *string = &a
var p2 = &a
// p1和p2是同一个变量的地址, 所以它们的值是一样的
fmt.Println(p1, p2) // 0xc0000441f0 0xc0000441f0
//有了指针变量, 那么通过 *指针变量 即可操作地址对应的内存
fmt.Println(*p1) // hello
//将p1指向的值修改之后, p2指向的值、以及a本身都会改变, 因为它们都是同一份内存
*p1 = "world"
fmt.Println(*p2, a) // world world
}
此外还有很重要的一点,那就是go中变量的传递统统都是值传递,go中没有所谓的引用传递。无论是传递值,还是传递指针,都是值传递(即拷贝一份出来)。
package main
import "fmt"
func main() {
var a = 123
var b = a
// 虽然b = a, 但是这只是将a的值拷贝一份给b, 所以这两个变量的地址是不一样的
fmt.Printf("%p %p\n", &a, &b) // 0xc0000a0068 0xc0000a0080
var c = 456
var p1 = &c
var p2 = p1
//因为将p1赋值给了p2, 所以这两个指针变量的值是一样的, 因为存储的都是变量c的地址
//但我们说了, go中任何变量的传递都是值传递, 也就是要拷贝一份出来
//所以p1和p2存储的值一样, 表示它们存储的地址是一样的, 修改*p1会影响*p2, 修改*p2会影响*p1, 因为都指向同一份内存
//但是这两个指针变量本身的地址是不一样的, 指针变量也是有地址的, 只要它有值
fmt.Printf("%p %p\n", &p1, &p2) // 0xc000006030 0xc000006038
//此时修改p1, 不会影响p2; 修改p2不会影响p1, 因为它们是两个不同的变量, 只是存储的值(地址)一样, 而其本身的地址不一样
}
注意:还有很重要的一点,指针变量如果不赋初始值的话,那么它就是一个空指针,也就是nil。
package main
import (
"fmt"
)
func main() {
var p1 *int
var p2 *int
//空指针也是有地址的, 它已经分配好了内存, 因为指针也是有零值的, 而nil就是指针的零值
//打印布尔值用%t, 可以看到这两个指针是一样的, 虽然地址不一样, 但是存储的值一样都是nil; 并且任何类型的指针, 结果为空, 结果都是nil
fmt.Printf("%p %p %t\n", &p1, &p2, p1 == p2) // 0xc000006028 0xc000006030 true
fmt.Println(p1, p2) // <nil> <nil>
//另外空指针的话, 你不能操作其指向的内存, 这是不允许的, 因为空指针没有指向一块合法的内存
//所以虽然这个指针本身被分配了内存, 但是指针指向的内存并没有分配, 所以需要给指针赋一个初始值才可以
//给指针赋值, 一定要通过取变量地址的方式赋值, 直接 var p *int = 0xF000006060 这种方式也是不允许的
}
所以指针变量需要指向一个具体的普通变量,但是除了创建一个变量并取其&符的方式之外,还可以使用用new函数。
package main
import (
"fmt"
)
func main() {
//new函数里面接收一个类型, 会自动创建一个该类型的零值(相当于分配内存), 然后返回其指针
var p1 = new(int)
fmt.Println(*p1) // 0
//所以此时p1这个指针变量就不是空指针nil了, 它是有具体的指向的, 只不过这个位置我们不知道罢了
*p1 = 123
fmt.Println(*p1) // 123
//上面就等价于:
var i int
var p2 = &i
fmt.Println(*p2) // 0
//只不过此时我们知道p2这个指针变量指向谁, 修改其中一个都会影响另一个, 因为都是同一份内存
i = 1
fmt.Println(*p2, i) // 1 1
*p2 = 2
fmt.Println(*p2, i) // 2 2
}
因此new函数就方便很多,会动态分配空间,而且我们不需要关心空间释放,go编译器会自动帮我们做到这一点。
既然说起指针,我们很容易想到C,都说指针是C语言的灵魂,不会指针说明你不懂C,那么在go中是不是这样呢?答案不是的,虽然go中的指针也很重要,但是没有C中的指针那么强大,不过能够获取一个变量的地址,并且能通过地址来改变存储的值,我个人觉得已经足够了。因为go是一个高级语言,它要保证安全性,而指针是很危险的,像其它语言在语法层面上都摒弃指针了。所以一句话:go里面有指针,但是相较于C,go中的指针被弱化了许多,所以既提供了指针的便利性,又保证了安全。那么它都进行了哪些弱化呢?
弱化一:golang中的指针不能进行数学运算。
package main
import (
"fmt"
)
func main() {
var p1 = new(int)
//如果是在C中, p1可以是一个整型数组中的某个元素的地址
//那么 p1++ 即可让p1等于下一个元素的地址, 但是在go中是行不通的, 因为go中的指针不可以运算
fmt.Println(p1) // 0xc0000160c0
//所以 0xc0000160c0 + 1, 行; 但是 p1 + 1, 不行
fmt.Println(0xc0000160c0 + 1) // 824633811137
}
弱化二:golang中不同类型的指针不能进行比较。
package main
import (
"fmt"
)
func main() {
var p1 *int
var p2 *int32
fmt.Println(p1, p2) // <nil> <nil>
fmt.Println(p1 == nil, p2 == nil) // true true
// 我们看到p1和p2都是nil, 而且它们都可以和nil进行比较, 并且相等
// 但是 p1 和 p2 两者本身不可以比较, 因为它们是不同类型的指针, 所以go语言对类型的要求真的非常严格
// 但是nil是一个特例, 任何类型的指针都可以和nil进行比较, 用来判断指针是否为空
// 如果是空指针, 那么和nil相等, 不是空指针, 和nil不相等
}
弱化三:golang中不同类型的指针不能进行转化或者赋值。
//这个很好理解, 比较都不行, 更何况转化和赋值
但是随着学习的深入,我们会了解到如何使用unsafe包来突破这一限制,当然这都是后话了。总之记住:go中的指针是类型安全的,可以大胆放心使用。
从终端中读取数据
既然学习的指针,那么应该也要了解如何从终端中读取数据了:
package main
import "fmt"
func main() {
var name string
var age int
// 这里会将屏幕中的值读到name和age中
// 注意: 这种方法会默认以空格进行分隔, 然后按下回车即可
fmt.Scanln(&name, &age)
// 比如: 输入mashiro 16即可
fmt.Println(name, age) // mashiro 16
// Scanln是只要按下了回车, 那么读取就结束了, 没有收到值的则默认为零值
// 比如: 输入mashiro, 那么结果就打印 mashiro 0; 如果无法解析的话也是为零值, 比如: mashiro xxx, 那么age依旧是0
// 除了Scanln, 还有一个Scanf, 它的语法格式是: Scanf("%s %d", &name, &age)
// 这会按照你的占位符进行解析
// 最后还有一个Scan, 和Scanln用法一样, 但区别是Scanln一旦回车就结束读取, 不管有没有读取指定数量的元素, 不够的用对应类型的零值填充
// Scan的话, 依旧以空格分割, 如果读取的元素不够、即使换行也不会结束, 而是会一直卡住, 直到读取完指定个数的元素
}
说实话这个不是很好演示,可以自己试一下,很简单的。不过还遗留了一个问题:
package main
import "fmt"
func main() {
var word string
fmt.Scan(&word)
// 这个时候我输入 hello mashiro 的话
// 我们看到打印的是 hello, 遇见空格停止了
fmt.Println(word) // hello
}
那么如何才能让它读取完一整行呢?可以使用bufio这个包。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 这个包后面会说
r := bufio.NewReader(os.Stdin)
// 执行的时候会在这里卡住, 输入 hello mashiro
word, err := r.ReadString('\n')
if err != nil {
fmt.Println("出错啦,", err)
} else {
// 会发现一整行都读取了, 因为我们上面是读取到换行符才结束的
fmt.Println(word) // hello mashiro
}
}
数组
数组:一组具有相同数据类型在内存中有序存储的数据集合,数组的声明方式如下:
var 变量名 [数组元素个数]元素类型
我们来测试一下:
package main
import "fmt"
func main() {
// 这里没有赋初始值, 那么里面的元素默认为0
var arr1 [5]int
fmt.Println(arr1) // [0 0 0 0 0]
// 如果是二维数组的话
var arr2 [4][2]int
// 外层数组有四个元素, 每个元素都是[2]int
fmt.Println(arr2) // [[0 0] [0 0] [0 0] [0 0]]
// 也可以赋上初始值
var arr3 [3]int = [3]int{1, 2, 3}
fmt.Println(arr3) // [1 2 3]
// 赋上初始值的话, 那么类型是可以省略的
var arr4 = [4]int{2, 3, 4}
// 我们看到元素不够的话, 默认使用零值进行填充; 比如: var arr = [100]int{1, 2, 3}, 那么数组中的前三个元素是1 2 3, 后面97个元素都是0
// 但是数组的元素不可以多, 比如: [2]int{1, 2, 3}是不合法的, 此时会报错
fmt.Println(arr4) // [2 3 4 0]
// 里面元素要是比较多, 而我们又不想数的话, 那么可以指定为..., 这个时候编译器会帮我们数
// 本来数组长度以[]里面指定的数字为准, 但是我们指定为...的话, 那么数组里面有几个元素, 长度就是几;
var arr5 = [...]int{1, 1, 1, 2, 3}
fmt.Println(arr5)
// 数组最大的特点就是元素的类型一致, 并且长度不可以变
// 而且数组的个数也会体现在类型中, 比如: [3]int 和 [4]int 是不同的类型
// 不过在C语言中, 数组的长度不体现在类型中, 我们可以将长度声明为1, 而具体多长则以实际使用时为准, 但是在go中数组的长度必须一开始就确定好
// 所以在声明数组时, 指定的数组长度一定要是一个常量, 比如下面就是不合法的
/*
var count = 10
var arr [count]int // 不合法, 因为数组的元素个数必须是个常量
*/
// 我们来查看一下数组类型, 查看类型使用 %T
fmt.Printf("%T %T %T %T %T\n", arr1, arr2, arr3, arr4, arr5) // [5]int [4][2]int [3]int [4]int [5]int
// 我们看到指定的数组元素个数也是类型的一部分
// 此外声明数组还有一个更简单的方式
// 这里表示索引为3的元素是22, 说明什么, 数组中至少有4个元素, 所以长度为4
var arr6 = [...]int{3: 22}
fmt.Println(arr6) // [0 0 0 22]
var arr7 = [5]int{3: 33}
fmt.Println(arr7) // [0 0 0 33 0]
/*
但是注意:
var arr = [...]int{1, 2, 3, 2:1}
这种方式是不对的, 因为索引为2的地方已经有数字3了, 然后再指定 2:1 的话, 就会报出索引重复的错误
下面也不对:
var arr = [5]int{5:1}
因为数组长度为5, 索引最大为4, 因此指定 5:1 的话会报出索引越界的错误
*/
}
此外数组的元素类型也可以是指针,当然默认值也是nil、即空指针,因为指针本身的内存编译器会自动分配、并且指针变量也是有地址的,但是它指向的内存就不会自动分配了,需要你显式的设置其指向的值。
package main
import (
"fmt"
)
func main() {
var arr [3]*int
fmt.Println(arr) // [<nil> <nil> <nil>]
}
此外数组一旦创建,大小就不可以再变了,然后我们可以像其它编程语言那样通过索引进行取值和赋值;
package main
import "fmt"
func main() {
var arr [3]*int
var a, b, c = 22, 33, 44
arr[0], arr[1], arr[2] = &a, &b, &c
fmt.Println(arr) // [0xc0000160c0 0xc0000160c8 0xc0000160d0]
fmt.Println(*arr[0], *arr[1], *arr[2]) // 22 33 44
a, b, c = 222, 333, 444
fmt.Println(arr) // [0xc0000160c0 0xc0000160c8 0xc0000160d0]
fmt.Println(*arr[0], *arr[1], *arr[2]) // 222 333 444
// 当然数组也可以使用new, 这里创建一个含有3个整型的数组、返回它的地址
var arr2 = new([3]int)
fmt.Println(*arr2) // [0 0 0]
// *arr2[1] 相当于 *(arr2[1]), 显然这是不对的, 我们需要将*arr2用括号括起来表示一个整体
(*arr2)[1] = 123
fmt.Println(*arr2) // [0 123 0]
}
切片
我们说数组长度是不可以变的,显然这极大的限制了数组的灵活性,于是我们需要切片(slice)。切片非常灵活,它的操作方式和数组一样,但是可以动态扩容;当然切片本质上也是使用了数组,因为切片本身是一个结构体(后面会说),内部有一个指针,这个指针指向一个数组。
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 长度
cap int // 容量
}
我们看到slice底层是一个结构体,有三个字段,分别是指向底层数组的指针、长度、容量。而显然这三者都占8个字节(64位机器上),那么这就意味着无论什么样的切片,大小都是24个字节,不信我们来看一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
// []里面什么都不写的话, 表示创建一个切片
var s1 = []int{1, 2, 3}
var s2 = []string{"1, 2, 3"}
var s3 = []float64{1.1, 2.2, 3.3}
// 查看变量所占内存的话, 可以使用unsafe.Sizeof
fmt.Println(unsafe.Sizeof(s1)) // 24
fmt.Println(unsafe.Sizeof(s2)) // 24
fmt.Println(unsafe.Sizeof(s3)) // 24
}
所以slice底层还是使用数组存储的,slice只是一个结构体,保存了底层数组的首地址、元素的个数、以及容量。长度指的是元素的个数,而容量是指底层数组的长度。我们往切片里面添加元素,实际上是往底层数组添加元素,当底层数组满了的时候,那么会申请一个更大的数组。假设申请一个长度为3,容量为5的切片:
底层数组虽然长度是5,但是对于切片而言只能看到前3个,我们代码演示一下:
package main
import "fmt"
func main() {
// 如果是通过 []int{}方式创建, 那么默认情况下长度和容量是一样的
// 但是我们可以使用make创建, 这个语法后面会说
s := make([]int, 3, 5)
// 创建[]int类型的切片, 长度为3, 容量为5; 如果不指定容量, 那么容量和长度一致
fmt.Println(s) // [0 0 0]
// 此时打印的s就是底层数组中的元素, 虽然长度为5, 但是打印出来我们能看到的只有3个
// 事实上底层数组是[0 0 0 0 0], 因为默认都是零值; 但是对于切片而言, 它只能看到3个元素
// 我们可以像操作底层数组一样操作切片, 因为操作切片本质上也是操作底层数组
s[0], s[1], s[2] = 1, 2, 3
// 注意: 如果使用s[3], 那么会索引越界; 虽然底层数组有5个元素, 但是对于切片而言, 它只能看到3个
fmt.Println(s) // [1 2 3]
// 然后我们可以使用append函数进行添加, 注意: 此时必须用一个值进行接收, 但是前后的地址是不会变的
s = append(s, 11) // 所以使用同一个变量进行接收, 可以在append前后打印一下s的地址, 是一样的
fmt.Println(s) // [1 2 3 11]
s = append(s, 22)
fmt.Println(s) // [1 2 3 11 22]
// 此时底层数组就变成了[1 2 3 11 22], 但我们说底层数组长度为5
// 因为创建切片的时候指定的容量是5, 所以底层数组长度也是5, 可现在已经5个元素了, 如果继续添加的话
s = append(s, 33)
fmt.Println(s) // [1 2 3 11 22 33]
// 虽然结果和我们想象的一样, 而且s还是原来的s, 但是底层数组却不是原来的底层数组了
// 因为原来的底层数组长度不够了, 所以这个时候会申请一个更大的底层数组
// 然后把原来数组的元素依次拷贝过去, 再让切片内部的指针指向新的数组
// 因此数组扩容显然是一个比较耗费资源的操作, 因此每次扩容时都会将底层数组申请的大一些
// 至于新的底层数组相比之前到底有多大, 可以认为当切片长度比较小(小于1024)的时候, 在扩容时容量(底层数组长度)直接翻倍
// 切片长度比较大的时候, 扩容时容量增加百分之25
// 查看切片长度可以使用len函数, len函数也可以作用于数组, 我们在介绍数组的时候没有说, 当前也可以作用于字符串
fmt.Println(len(s)) // 6
// 而查看切片的容量(底层数组的长度), 可以使用cap函数
// 我们看到直接翻倍了
fmt.Println(cap(s)) // 10
}
所以最终的示意图就是这样:
我们访问切片的元素等价于访问底层数组的元素,修改切片里面元素的值等价于修改底层数组里面元素的值。但是注意:底层数组是可以被多个slice同时指向的,因此修改一个其中一个slice,也会影响其他的slice,因为这些切片指向的都是同一个底层数组。
创建slice
创建slice有很多种方式:
1. 直接声明
package main
import (
"fmt"
)
func main() {
// 这种方式只是声明了一个切片, 当然对于结构体而言, 如果没有赋值, 那么里面的每个成员默认也是零值
// 所以长度和容量都是0, 并且内部的指针是一个空指针、没有指向任何的底层数组, 因此它是一个nil slice
var s []int
// 如果指针为空, 那么s和nil是相等的
fmt.Println(s == nil) // true
// 但是我们看到指针明明没有指向底层数组, 居然也能append
// 这是因为使用append的话,如果没有分配底层数组的话,那么会自动先帮你分配一个大小、容量都为0的底层数组,然后再把元素append进去
s = append(s, 123)
fmt.Println(s) // [123]
// 当然也可以直接创建, 此外也支持索引的方式
var s1 = []int{1, 5:1, 3}
fmt.Println(s1) // [1 0 0 0 0 1 3]
// 5:1 表示索引为5的元素是1, 索引为1 2 3 4的元素都是0, 最后是元素3
}
2. 使用new的方式
package main
import "fmt"
func main() {
// 我们知道new是创建一个类型的零值, 然后返回其指针
var s = new([]int)
// 所以这种方式的话, 会创建切片本身,但是切片对应的底层数组是不会被创建的,内部的指针是一个 nil、长度和容量都是 0
// 不过使用 append 的话会自动创建
*s = append(*s, 1, 2, 3, 4) // 可以一次性添加多个元素
fmt.Println(s) // &[1 2 3 4]
fmt.Println(*s) // [1 2 3 4]
}
3. 使用make的方式,这是最常用的方式
package main
import "fmt"
func main() {
// 创建长度为3、容量为3的切片
var s1 = make([]int, 3)
// 创建长度为3、容量为5的切片
var s2 = make([]int, 3, 5)
// 直接返回切片本身
fmt.Println(s1) // [0 0 0]
fmt.Println(s2) // [0 0 0]
}
4. 从数组中截取
package main
import "fmt"
func main() {
//创建元素个数为6的数组
var arr = [...]int{5:1}
fmt.Println(arr) // [0 0 0 0 0 1]
//通过切片的方式从数组中拷贝一个切片, [start: end]和其它高级语言类似, 从索引为start的地方截取到索引为end - 1的地方
//start可以省略, 默认从头开始; end也可以省略, 表示截取到尾; 都不写则从头截取到尾
s := arr[0: 1]
s[0] = 123
fmt.Println(s) // [123]
fmt.Println(arr) // [123 0 0 0 0 1]
//我们看到对切片的修改是会影响底层数组的
//如果我们不是手动从已存在的数组拷贝的话, 而是使用其他方式创建的话, 那么go会默认给你分配一个底层数组
//只不过这个数组我们看不到罢了, 但它确实是分配了; 如果从已经存在的数组中拷贝, 那么这个数组就是拷贝出来的切片的底层数组
//并且我们拷贝切片的时候, 还可以指定容量
//注意: 第三个6可不是步长, 而是用来指定容量的, 但它又不完全等于容量
s1 := arr[2:4:6]
//arr[start:end:cap], 此时 cap - start 才是容量, 所以这里的容量是6-2=4
//所以这里的cap无论何时都不能超过底层数组的元素个数, 并且不能小于结束位置, 关于这一点我们马上会继续说, 现在觉得懵的话没关系
fmt.Println(len(s1), cap(s1)) //2 4
}
slice的截取
我们如果使用make创建、或者直接声明切片的话,那么会默认给我们创建一个底层数组。但是问题就在于,我们很多时候是会从数组中拷贝的,而这里面会隐藏着一些玄机。
package main
import "fmt"
func main() {
//此时数组共有8个元素,元素的最大索引为7
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
//此时s1和s2都指向了arr,只不过它们指向了不同的部分。
//我们看到s1的第一个元素,就是s2的第二个元素
s1 := arr[1: 2]
s2 := arr[0: 2]
s2[1] = 111 // 将s2的第一个元素改掉
// 我们看到s1也被改了,而且底层数组也被改了
fmt.Println(s1) // [111]
fmt.Println(arr) // [0 111 2 3 4 5 6 7]
//很好理解,因为我们可以把切片看成是底层数组的一个映射,修改切片等价于修改底层数组
//s1和s2映射同一个底层数组
//当然我们知道切片内部是只有一个指针和两个整型, 最终的操作都会体现在底层数组上
}
上面的很好理解,然后我们再来看看下面的例子:
package main
import "fmt"
func main() {
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[1: 2]
fmt.Println(s1[3: 6]) // [4 5 6]
fmt.Println("----------我是分界线----------")
fmt.Println(s1[3])
/*
[4 5 6]
----------我是分界线----------
panic: runtime error: index out of range [3] with length 1
*/
}
惊了,s1里面只有一个元素,我们居然能够通过s1[3: 6]访问,但是后面访问s1[3]却报错了。所以这就是切片的可扩展性,其实我们上面的图画的不是很准确。
因此切片实际上是可扩展的,如果对切片进行索引的话,那么最大索引就是切片的长度减去1。但是如果对切片进行切片的话(reslice),那么是根据底层数组来的。我们看到s[3: 6]对应底层数组是[4, 5, 6],所以是不会报错的。尽管s1只有一个元素,但是它记得自己的底层数组,并且是可扩展的。并且这个扩展只能是向后扩展,可以看到后面的底层数组的元素。但是不能向前扩展,比如底层数组的0,通过s1的话是无论如何都获取不到的。
但是如果我们在截取的时候指定了容量呢?
package main
import "fmt"
func main() {
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
//对于从数组截取切片的话, s1 := arr[1: 2]等价于s1 := arr[1: 2: len(arr)]
//表示s1的容量为len(arr) - 1, 相当于切片能够从当前位置扩展到arr的尽头
//但是这里我们指定5, 表示最多只能扩展4个元素
s1 := arr[1: 2: 5]
fmt.Println(s1[2: 4]) // [3 4]
fmt.Println("----------我是分界线----------")
fmt.Println(s1[3: 5])
/*
[3 4]
----------我是分界线----------
panic: runtime error: slice bounds out of range [:5] with capacity 4
*/
}
因此我们看到此时访问[2: 4]是可以的,但是访问[3: 5]就报错了,因为我们这里指定了容量。
s1的容量是5 - 1 = 4,向后扩展最多只能扩展三个元素,那么访问[3: 5]肯定就报错了。
另外我们知道,由数组创建两个切片,对任何一个切片进行修改都会影响底层数组,进而影响另一个切片。如果我创建了一个切片s1,然后根据s1再创建出s2,那么对s2修改同样会影响底层数组,进而影响s1。因为它们指向的都是同一个底层数组,并且s2依旧是可以向后扩展的,至于能向后扩展多少就根据它的容量来决定了,总之不能超过底层数组。
package main
import "fmt"
func main() {
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[1: 3]
s2 := s1[3: 6]
fmt.Println(s1) // [1 2]
fmt.Println(s2) // [4 5 6]
// [:]这种方式的话只会获取当前切片的可以看到的元素, 换句话说等于切片的长度
fmt.Println(s2[:]) // [4 5 6]
fmt.Println(s2[: 4]) // [4 5 6 7]
}
此时我们修改,s[2] = 222, 那么底层数组会有何变化呢?显然是arr[6]也变成了222
s2[2] = 222
fmt.Println(arr) // [0 1 2 3 4 5 222 7]
切片的扩容
切片的扩容,实际上就是对底层数组的扩容,假设我们申请的切片容量是3,那么对应的底层数组的大小就是3。我们知道切片是可以进行append的,如果容量不够的话,怎么办呢?显然就要进行扩容了。
package main
import "fmt"
func main() {
var s = make([]int, 0, 3)
s = append(s, 1)
fmt.Printf("%p\n", &s[0]) //0xc000060140
s = append(s, 2)
fmt.Printf("%p\n", &s[0]) //0xc000060140
s = append(s, 3)
fmt.Printf("%p\n", &s[0]) //0xc000060140
//我们知道此时如果再append,那么容量肯定不够了
s = append(s, 4)
fmt.Printf("%p\n", &s[0]) //0xc00008c030
}
我们看到扩容之前,s[0]的地址时不变的。但是扩容之后,地址变了。说明golang中切片的扩容是在底层申请一个更大的数组,让s指向这个新的数组,并把对应元素依次拷贝过去(所以&s[0]会变),那么原来var s = make([]int, 0, 3)所指向的底层数组怎么办呢?这个不用担心,golang的垃圾回收机制会自动销毁它。
package main
import "fmt"
func main() {
var arr = []int{1, 2, 3}
s1 := arr[1: 3]
s2 := s1[: 2]
fmt.Println(s1, s2) // [2 3] [2 3]
fmt.Println(&s1[0], &s2[0]) //0xc000060148 0xc000060148
//此时s1和s2都是[2, 3],下面给s2扩容
s2 = append(s2, 4)
fmt.Println(&s1[0], &s2[0]) //0xc000060148 0xc000060180
fmt.Println(s1[0], s2[0]) //2 2
}
惊了,我们看到s2的第一个元素的地址变了,而s1的第一个元素的地址没有变。因此可以猜测扩容之后,s2指向了新的数组,但是s1还是指向了原来的数组。事实上也确实如此,因为对s2添加元素,发现底层数组容量不够了,那么就申请一个更大的,让s2重新指向,但是s1还是指向原来的底层数组。而且既然s1引用的还是原来的数组的话,那么原来的数组则不会被gc回收了,并且我们再对s1做任何操作都不会影响s2了,因为这两个切片指向的不再是同一个底层数组了。
package main
import "fmt"
func main() {
var arr = []int{1, 2, 3}
s1 := arr[1: 3]
s2 := s1[: 2]
//对s1操作,此时会影响s2
s1[0] = 111
fmt.Println(s2) //[111 3]
//扩容之后,s2指向新的数组
s2 = append(s2, 4)
//再对s1操作,不会影响s2
s1[0] = 333
//s2[0]还是之前被影响的111,不会是s1新设置的333
fmt.Println(s2) //[111 3 4]
}
而且申请更大的底层数组的时候,并不是把原来数组中所有元素都拷贝过去,而是把切片对应的底层数组的元素拷贝过去。因为切片无法向前扩展,那么不好意思,前面的元素就不会拷贝了。
切片的拷贝
有一个copy函数,可以专门实现切片的拷贝。
package main
import "fmt"
func main() {
var s1 = []int{1, 2, 3, 4, 5}
var s2 = []int{6, 7, 8}
// 将s1拷贝到s2中, 是从头开始
copy(s2, s1)
fmt.Println(s2) // [1 2 3]
var s3 = []int{1, 2, 3}
var s4 = []int{4, 5, 6, 7, 8}
copy(s4, s3) // 将s3拷贝到s4中
fmt.Println(s4) // [1 2 3 7 8]
var s5 = []int{1, 2, 3, 4, 5}
var s6 = make([]int, 1, 3)
copy(s6, s5)
// 我们看到copy切片不会影响底层数组
fmt.Println(s6) // [1]
fmt.Println(s6[: 3]) // [1 0 0]
var s7 = []int{1, 2, 3}
var s8 = []int{3, 4, 5}
// 如果想将一个切片的元素拷贝到另一个元素里面去要怎么做呢
s7 = append(s7, s8[1:]...)
// 因为append接收一个 切片 和 任意个相同类型的元素
// 所以通过...的方式可以将切片里面的元素展开, 用过Python的话就类似于里面的 *list
fmt.Println(s7) // [1 2 3 4 5]
}
切片和数组的区别
slice的底层数据结构是数组,slice是对数组的一个封装,它描述数组的一个片段,两者都可以通过下表来访问单个元素。
数组是定长的,长度定义好之后不能再更改。在go中,数组不常用,因为长度也是类型的一部分,限制了它的表达能力,比如[3]int和[4]int就是不同的类型。
而切片则非常灵活,它可以动态扩容,并且类型和长度无关。
map
map(映射)类似于Python中的dict,它是使用哈希表实现的,是一种基于key-value形式的、无序的数据集合。那么如何创建一个映射呢?
package main
import "fmt"
func main() {
// 此时我们就声明了一个map, 但是注意: 此时是为nil的
var m map[string]int
fmt.Println(m) // map[]
fmt.Println(m == nil) // true
// 虽然显示的是map[], 但其实结果是nil, 所以此时是不能操作的
// 我们必须要先赋值
m = map[string]int{"math": 95, "english": 88, "history": 77}
fmt.Println(m) // map[english:88 history:77 math:95]
// 获取元素的方式类似于json, 直接通过中括号的方式获取即可
fmt.Println(m["math"], m["history"]) // 95 77
// 获取一个不存在的元素, 那么结果为零值
fmt.Println(m["not exists"]) // 0
// 但是问题来了, 如果有的值刚好为0, 该怎么办呢? 答案是用两个变量来接收
// 如果只用一个变量, 那么key存在、则返回对应value; 不存在则返回零值
// 如果用两个变量, 那么key存在、则返回对应value和true; 不存在则返回零值和false
val, flag := m["math"]
fmt.Println(val, flag) // 95 true
// 上面已经创建了val 和 flag, 所以下面直接赋值就可以了
val, flag = m["not exists"]
fmt.Println(val, flag) // 0 false
// 添加一个键值对, key存在则更新, 不存在则添加
m["not exists"] = 123
fmt.Println(m["not exists"]) // 123
// 删除一个键值对
delete(m, "not exists")
fmt.Println(m) // map[english:88 history:77 math:95]
// 使用len函数可以查看键值对个数
fmt.Println(len(m)) // 3
}
需要注意的是,map基于哈希表实现,这就意味着key必须是可哈希的;像字典、切片就不可以作为key,因为它们支持动态修改,导致哈希值会变;当然 通道和函数 也不可以。
另外map也是会动态扩容的,但不会缩容,即使你删除了全部的key,容量占用依旧摆在那里。
相比上面那种创建方式,我们会更倾向于使用make,没错又是make。
像切片、map,这类数据结构它们是可以动态改变的,因此对应的结构本身并没有真正地存储数据;而是通过一个指针,指向一块内存(真正存储数据),如果直接声明的话,那么指针会是nil。所以go中针对这样数据结构,专门设置了make函数,会自动创建内存并把地址交给指针。同理我们后面要学习的channel也是如此,总之:切片、map、通道,这三者都使用make创建即可。
package main
import "fmt"
func main() {
var m = make(map[int]int)
m[1] = 11
m[2] = 22
m[3] = 33
fmt.Println(m) // map[1:11 2:22 3:33]
}
怎么样,是不是很方便呢。
小结
这一次我们把go中的一些高级数据类型介绍了一部分,其实总的来说go还是比较简单的。不过简单,也就意味着很多操作都需要我们自己写,比如:如何往切片中间插入一个元素我们没有说,原因就在于go中没有现成的方法,所以该怎么做你肯定清楚。