壹 介绍
在Go语言中,还存在着一些复杂的类型(引用类型):数组、切片和集合。
贰 数组array
2.1 简介
Go 语言提供了数组类型的数据结构。数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。相对于去声明 number0, number1, ..., number99
的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99]
更加方便且易于扩展。数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的底层就是就是指针。
注意:需要区分数组与切片,数组在方括号里面是有值的,切片是没有值的,切片后面会讲。
2.2 声明数组
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
// 一维数组
var 数组变量名 [元素数量] 数组类型
// 多维数组
var 数组变量名 [元素数量1][元素数量2]...[元素数量N] 数组类型
例子:
// 定义了一维数组 balance 长度为 10 类型为 float32
var balance [10]float32
// 声明了二维的整型数组
var double [5][10]int
// 声明了三维的整型数组
var threedim [5][10][4]int
二维数组是最简单的多维数组,二维数组本质上是由一维数组组成的,三维数组也是如此。
二维数组可认为是一个表格,x 为行,y 为列,下图演示了一个二维数组 a 为三行四列:
以此类推三维数组就有点像一个立方体。
2.3 初始化数组
为什么要初始化?举个例子,我们脑海里想了一个玩具,这个玩具,现在是幻想出来,也就是编程里面的定义,那要想完这个玩具,那我们就要买这个玩具或者制造这个玩具,这个叫初始化或者叫实例化。所以任何的类型变量都需要定义,然后初始化,才可以使用。
- 一维数组初始化:
```go
// 数组初始化
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 如果数组初始化不赋值那么会自动赋为0|False|空字符串
var testArray [3]int
// 也可以通过字面量在声明数组的同时快速初始化数组
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 如果数组长度不确定,可以使用…代替数组的长度,编译器会根据元素个数自行推断数组的长度
var balance = […]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := […]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 如果设置了数组的长度,我们还可以通过指定下标来初始化元素,将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
// 初始化数组中{}中的元素个数不能大于[]中的数字。如果忽略[]中的数字不设置数组大小,Go语言会根据元素的个数来设置数组的大小
balance[4] = 50.0
// 还可以利用循环初始化
var strarr [3]int
for i := 0; i < 3; i++ {
strarr[i] = i
fmt.Printf(“数组第%d个元素是%d\n”, i, strarr[i])
}
// 如果是以下定义,那么是定义切片,后面会讲
var str := []int{}
//可以看到格式类似
var 数组变量名 = [数组长度]数组类型{值}
上面的例子结果如下:

- 二维数组初始化:
多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:
```go
//可以看出二维数组由两个大括号组成,最里面的括号相对于外面的括号是一个值,以此类推,多级数组
a := [3][4]int{
{0, 1, 2, 3}, // 第一行索引为0
{4, 5, 6, 7}, // 第二行索引为1
{8, 9, 10, 11}, // 第三行索引为2
}
//我们还可以不计算元素个数初始化
a := [...][4]int{
{0, 1, 2, 3}, // 第一行索引为0
{4, 5, 6, 7}, // 第二行索引为1
{8, 9, 10, 11}, // 第三行索引为2
}
//注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。以下不准确:
// a := [3][...]int{
// {0, 1, 2, 3},
// {4, 5, 6, 7},
// {8, 9, 10, 11},
// }
以此类推,三维数组则是三个大括号叠加。
2.4 访问数组元素
数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。
- 访问一维数组:
例如:
数组名[索引值]
例子:
package main
import "fmt"
func main() {
array := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
for i := 0; i < 10; i++ {
fmt.Printf("%d值为:%v\n", i, array[i])
}
}
- 访问二维数组:
二维数组通过指定坐标来访问。如数组中的行索引与列索引,例如:
数组名[索引值1][索引值2]
例子:
package main
import "fmt"
func main() {
// 创建空的二维数组
animals := [][]string{}
// 创建三个一维数组,各数组长度不同
row1 := []string{"fish", "shark", "eel"}
row2 := []string{"bird"}
row3 := []string{"lizard", "salamander"}
// append()函数主要用于给某个切片追加元素
// 使用 append() 函数将一维数组添加到二维数组中
animals = append(animals, row1)
animals = append(animals, row2)
animals = append(animals, row3)
// 循环输出
for i := range animals {
fmt.Printf("Row: %v\n", i)
fmt.Println(animals[i])
}
}
注意:
- a.数组是值类型,所以在执行赋值和传参操作时是将复制整个数组到另一个变量中。因此改变复制后数组的值,不会改变原先数组的值。
- b.数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
- c.
[n]*T
表示指针数组,*[n]T
表示数组指针 。
叁 切片slice
3.1 简介
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,Go 中提供了一种灵活,功能强悍的内置类型切片(”动态数组”)——Go 语言切片,Go 语言切片是对数组的抽象,是一个拥有相同类型元素的可变长度的序列,切片的底层就是一个数组。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容,是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。
切片s2 := a[3:6]
,相应示意图如下:
3.2 定义切片
你可以声明一个未指定大小的数组来定义切片:
var 切片名 []切片类型
//name:表示变量名
//type:表示切片中的元素类型
//切片不需要说明长度
//可以使用make()函数来创建切片:
var name []type = make([]type, len)
//也可以简写为:
name := make([]type, len)
切片拥有自己的长度和容量,可以指定容量, length是当前容量,capacity是最大容量,其中capacity为可选参数,默认等于length。我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
make([]T, length, capacity)
为什么切片需要容量?
- a.可以隐藏数组中暂时不使用的空间
- b.当使用数组达到闸值时,重新分配空间,实现动态数组效果
3.3 切片初始化
切片的初始化比较灵活:
//直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3,一般使用这种方式定义
s :=[]int{1,2,3}
//初始化切片 s,是数组 arr 的引用
s := arr[:]
//将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
//默认 endIndex 时将表示一直到arr的最后一个元素,默认为切片操作数的长度
s := arr[startIndex:]
//默认 startIndex 时将表示从 arr 的第一个元素开始,默认为0
s := arr[:endIndex]
//通过切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
//通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片
s :=make([]int,len,cap)
// 利用循环初始化
// 需要注意这个初始化的时候是在make的容量内,如果超过容量范围需要进行增加操作,后面会讲
strarr := make([]int, 3)
for i := 0; i < 3; i++ {
strarr[i] = i
fmt.Printf("数组第%d个元素是%d\n", i, strarr[i])
}
make函数使用:链接
3.4 空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
package main
import "fmt"
func main() {
var numbers []int
fmt.Printf("len=%d cap=%d slice=%v\n", len(numbers), cap(numbers), numbers)
if numbers == nil {
fmt.Printf("切片是空的")
}
}
3.5 切片截取与遍历
可以通过设置下限、上限和容量(cap
表示实际的容量,可以忽略)来设置截取切片 [lower-bound:upper-bound:[cap]]
,实例如下:
package main
import "fmt"
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
func main() {
// 创建切片
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
printSlice(numbers)
// 打印原始切片
fmt.Println("numbers ==", numbers)
// 打印子切片从索引1(包含) 到索引4(不包含)
fmt.Println("numbers[1:4] ==", numbers[1:4])
// 默认下限为0
fmt.Println("numbers[:3] ==", numbers[:3])
// 默认上限为len(s),而不是cap(s)
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int, 0, 5)
printSlice(numbers1)
// 打印子切片从索引0(包含)到索引2(不包含)
number2 := numbers[:2]
printSlice(number2)
// 打印子切片从索引 2(包含) 到索引 5(不包含)
number3 := numbers[2:5]
printSlice(number3)
// 对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式
// numbers[low : high : max]
}
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历,这里就不进行赘述。
3.6 切片的其他操作
- a.增加
关于切片增加操作是使用函数append
:链接 - b.复制
关于切片复制操作的说明:链接 - c.删除
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
, 代码如下:package main import "fmt" func main() { // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) fmt.Println(a) //[30 31 33 34 35 36 37] }
- d.切片不能直接比较
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
,因为切片可以包含0,所以要判断一个切片是否是空的,要是用len(s) == 0
来判断,不应该使用s == nil
来判断。
肆 集合map
Go语言中提供的映射关系容器为map
,Map 是一种无序的基于key-value
的数据结构,其内部使用散列表(hash)
实现,我们无法决定它的返回顺序。Map是引用类型,必须初始化才能使用。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它,所以map
底层就是指针。
4.1 定义 map
首先我们来定义map,使用 map 关键字来定义 map:
map[KeyType]ValueType
//KeyType:表示键的类型
//ValueType:表示键对应的值的类型
//map类型的变量默认初始值为nil
需要使用make()
函数来分配内存,map
类型的变量默认初始值为nil
,需要使用make()
函数来分配内存。语法为:
map_name := make(map[key_type]value_type, [cap])
//其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
如果不初始化 map
,那么就会创建一个 nil map
。nil map
不能用来存放键值对。
4.2 map初始化
Map的初始化比较灵活,直接使用map插入key - value对:
package main
import "fmt"
func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap["France"] = "巴黎"
countryCapitalMap["Italy"] = "罗马"
countryCapitalMap["Japan"] = "东京"
countryCapitalMap["India "] = "新德里"
/* map也支持在声明的时候填充元素 */
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
fmt.Println(userInfo)
fmt.Println(countryCapitalMap)
}
4.3 map的遍历
package main
import "fmt"
func main() {
// 定义一个map
userInfo := map[string]string{
"username": "A7cc",
"password": "123456",
}
// 使用for range遍历map
for k, v := range userInfo {
fmt.Println(k, "值是:", v)
}
// 只遍历key
for k := range userInfo {
fmt.Println(k, "值是:", userInfo[k])
}
}
可以看到直接遍历时,map是按照首字符的ASCII码输出的。
4.4 判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
举个例子:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在,ok为true,v为对应的值;不存在,ok为false,v为0
if v, ok := scoreMap["张三"];ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
// 可以看到上面的v, ok := scoreMap["张三"]在go语言中是可以放到if的判断语句中,个人感觉if判断语句前面有表达式就是为这种情况而生的
}
4.5 元素为map类型的切片
下面的代码演示了切片中的元素为map类型时的操作:
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
4.6 值为切片类型的map
下面的代码演示了map中值为切片类型的操作:
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
4.7 map的其他操作
- a.删除
关于map删除操作的说明:链接
伍 通用函数
5.1 make函数
内建函数 make
用来为 slice
,map
或 chan
类型分配一块内存空间和初始化(注意:只能用在这三种类型上)。
格式:
valname := make(Type, len)
- Type:就是定义的类型,即slice、map、chan其中一个
- len:就是分配该类型的长度,但在slice中可以在后面加个cap代表容量
- 返回值为创建的类型,个人感觉就是这块地址的指针,这个概念后面会讲,这里只是用做理解
实例:
package main
import "fmt"
func main() {
// 申请一个长度为5,容量为10的切片
mySlice := make([]int, 5, 10)
// 申请一个长度为3的map
mymap := make(map[string]string, 3)
fmt.Println(mySlice) //[0 0 0 0 0]
fmt.Println(mymap) //map[],因为map没有初始化,所以没有值
}
5.1 len函数
切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
格式:
vallen := len(v)
- v:就是要返回长度的对象
- 返回值为该对象长度
实例:
package main
import (
"fmt"
)
func main() {
s1 := [...]int{1, 2, 3}
s2 := []int{1, 2, 3}
s3 := make(map[string]string)
s3["France"] = "巴黎"
s3["Italy"] = "罗马"
s3["Japan"] = "东京"
s3["India"] = "新德里"
fmt.Printf("%T,%T,%T\n", s1, s2, s3)
fmt.Printf("%d,%d", cap(s1), cap(s2))
}
5.2 cap函数
cap()
函数返回的是数组切片分配的空间大小。
格式:
valcao := cap(v)
- v:就是要返回长度的对象,一般是切片
- 返回值为该对象容量
实例:
package main
import "fmt"
func main() {
// 申请一个长度为5,容量为10的切片
mySlice := make([]int, 5, 10)
fmt.Println("len(mySlice):", len(mySlice))
fmt.Println("cap(mySlice):", cap(mySlice))
}
5.3 append函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
//len=0 cap=0 slice=[]
// 通过var声明的零值切片可以在append()函数直接使用,无需初始化。
// 允许追加空切片
numbers = append(numbers, 0)
printSlice(numbers)
//len=1 cap=1 slice=[0
// 向切片添加一个元素
numbers = append(numbers, 1)
printSlice(numbers)
//len=2 cap=2 slice=[0 1]
// 同时添加多个元素
numbers = append(numbers, 2, 3, 4)
printSlice(numbers)
//len=5 cap=6 slice=[0 1 2 3 4]
//底层原理:
//1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组:
//2.创建一个新数组,将老数组中的0,1复制到新数组中,在新数组中追加2, 3, 4
//3.numbers底层数组的指向,指向的是新数组
//4.往往我们在使用追加的时候其实想要做的效果直接给numbers追加
//5.但是实际上是通过切片间接操作追加的
// 同时添加切片
numbers1 := []int{2, 3, 4}
numbers = append(numbers, numbers1...) // 切片后加..., 相当于拆包成单个元素
printSlice(numbers)
//len=8 cap=12 slice=[0 1 2 3 4 2 3 4]
// 创建切片 numbers1 是之前切片的两倍容量
numbers2 := make([]int, len(numbers), (cap(numbers))*2)
// 拷贝 numbers 的内容到 numbers1
copy(numbers2, numbers)
printSlice(numbers2)
//len=8 cap=24 slice=[0 1 2 3 4 2 3 4]
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行扩容,此时该切片指向的底层数组就会更换。扩容操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
举个例子:
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}
append()函数将元素追加到切片的最后并返回该切片。
切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
5.4 copy函数
由于切片是引用类型,如果直接将一个切片使用=
赋值给另一个变量(注意是变量不是切片,切片间不能赋值),那么这两个其实都指向了同一块内存地址。修改其中一个变量的同时另一个变量的值也会发生变化。Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中。
5.4.1 格式
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
//srcSlice: 数据来源切片
//destSlice: 目标切片
5.4.2 例子
package main
import "fmt"
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
// c必须有内存,才能使用copy函数,不能声明完就直接使用copy,会返回一个空的切片。
// 这里涉及到指针问题,可以到后面学完指针再来看,其原因个人感觉是因为c在声明时是声明了一个空指针,指针是只能存放地址,如果直接使用copy函数,那么切片将复制给c所指向的空的地址,这是数据就没有用了
// 而make是申请一块地址,将地址给c,此时c就索引了一块确实存在的地址
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}
5.5 delete函数
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。
5.5.1 格式
delete()
函数的格式如下:
delete(map, key)
//map:表示要删除键值对的map
//key:表示要删除的键值对的键
5.5.2 例子
package main
import "fmt"
func main() {
/* 创建map */
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
fmt.Println("原始地图")
/* 打印地图 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap[country])
}
/*删除元素*/ delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
fmt.Println("删除元素后地图")
/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap[country])
}
}
5.6 Go 语言范围Range
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}