一、数组
数组是一个长度
固定
数据类型,用户存储一段具有相同类型的元素的连续块,存储的数据类型可以是内置类型,如整型int或者字符串,也可以是某种结构类型;
1、声明和初始化
1.1、var关键字声明
var关键字声明数组时需要指定存储的数据类型以及存储的元素个数,显式声明
//关键字 变量名 [数组长度] 类型
var array [5]int //声明一个包含5个元素的整型数组
*注意:
-
数组一旦声明之后,长度不可变且存储的数据类型也不能改变
-
数组声明之后,会使用对应类型的零值对数组元素进行初始化
-
在
Go
中,数组是值类型,赋值和函数传参操作都会复制整个数组的数据
1.2、初始化声明数组
通过赋值
:=
array := [5] int {10,20,30,40,50}//声明一个包含5个元素的整型数组
如果使用...替代数组的长度,go语言会根据初始化数组元素的数量来确定该数组的长度
array := [...] int {10,20,30,40,50}
声明数组并指定数组下标的值
array :=[5]int {1:10,2:30}//声明一个数组,指定数组下标为1的元素值为10,下标为2的元素值为30,其他的默认赋零值
2、数组的使用
2.1、通过下标访问
因为数组的内存布局是连续的,所以数组是效率很高的数据结构。在访问数组里任何元素的时候,这种高效都是数组的优势。要访问数组里某个单独的元素,使用[]运算符
array :=[5]int {10,20,30,40,50} array[2]=25//数组值为 10,20,25,40,50
2.2、 数组相互赋值
同样类型的赋值给另外一个数组,只是值的复制,下面的代码片段可以看出,数组是 值类型
array :=[4]string{"red","Blue","Green","Yello"}
var array2 [4]string
array2=array
for index,value:=range array{
fmt.Printf("下标:%d,值为:%s \r\n",index,value)
}
array[0]="张三"//重新赋值
array2[1]="李四"//重新赋值
for index,value:=range array{
fmt.Printf("下标:%d,值为:%s \r\n",index,value)
}
fmt.Println(&array2[0])
temp:=&array2[0]
*temp="abc" //修改array
for index,value:=range array2{
fmt.Printf("下标:%d,值为:%s 内存地址: %s \r\n",index,value,&(array2[index]))
}
数组的相互赋值,只有当数组长度和元素的类型,两个部分相同的时候,才能相互赋值
var array1 [6]string
var array3 [4]string
array2:=[]string{"red","blue","green","yellow","pink"}
array1=array2
array1=array3
/*
.\test_array2.go:11:8: cannot use array2 (type []string) as type [6]string in assignment
.\test_array2.go:12:8: cannot use array3 (type [4]string) as type [6]string in assignment
*/
指针数组,复制数组指针,只会复制指针的值,而不会复制指针指向的值
var array1 [3] *string
array2:=[3]*string{new (string),new(string),new(string)}
array1=array2
fmt.Println(array2)//[0xc00004c1c0 0xc00004c1d0 0xc00004c1e0]
fmt.Println(array1)//[0xc00004c1c0 0xc00004c1d0 0xc00004c1e0]
*array1[0]="abc" //给数组0号元素的指针指向的地址赋值“abc”
fmt.Println(*array2[0])//array2的0号元素的指针指向地址的值也变为“abc”
多维数组:
2.3 、在函数间传递数组
根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数间传递变量时,总是以值得方式传递,如果这个变量是一个数组,不管多长,都会完整复制,并传递给函数
func main() {
//声明一个需要8M内存的数组
var array [1e6]int=[1e6]int{0}
foo(array)
}
//函数需要接受一个100万个整型值得数组
func foo(s1 [1e6]int) {
}
每次函数foo
被调用时候,必须在栈上分配8MB
的内存。之后整个数组的值都被复制到刚分配的内存里。虽然go语言会处理这个复杂的操作,不过还有一种更好且更有效的方法来处理这个操作,传入数组的指针
func main() {
var array[1e6]int
foo1(&array)
}
func foo1(arr *[1e6]int) {
}
二、切片
1、定义
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置的函数append来实现的。这个函数可以快速且高效的增长切片,还可以通过对切片再次切片缩小一个切片的大小。因为切片的底层内存也是连续块中分配的,所以切片还能获得索引、迭代以及垃圾回收优化的好处。
2、内部实现
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有三个字段的数据结构,这些数据结构包含了go语言需要操作底层数组的元数据;三个字段:指向底层数组的指针、切片访问的元素个数(长度)、切片允许增长的元素个数(容量)
3、创建和初始化、
go语言中有几种方法可以创建和初始化切片
len(slice)//返回切片的长度
cap(slice)//返回切片的容量
3.1、make和切片字面量
通过make函数,需要传递一个参数,指定切片的长度
//创建一个字符串切片,长度和容量都是5个元素
slice1:=make([]string,5)
//如果只指定长度,那么切片的容量和长度也相等
//创建一个字符串切片,长度5,容量为8个元素
slice2:=make([]string,5,8)//初始化指定容量和长度之后,并不能访问所有的数组元素,此次初始化,只能访问前面5个元素
通过字面量来声明切片
slice3 :=[]int{10,20,40}//注意与数组的差别
3.2、索引声明切片
通过索引来声明切片
slice4 :=[]string{99:"abc"}//长度和容量都是100个元素的切片
3.3、nil和空切片
主要在声明时,不做任何初始化,就会创建一个nil切片
var slice []int //声明nil切片,长度0,容量0
利用初始化,可以声明一个空切片
slice :=make([]int ,0)
slice :=[]int{}
4、使用切片
4.1、赋值和切片
对切片里某个索引只想的元素赋值和对数组某个索引指向的元素赋值的方法完全一样。使用[]操作符就可以改变某个元素的值
slice :=[]int{10,20,30,40}
slice[0]=200
fmt.Println(slice)//[200 20 30 40]
切片创建切片
slice :=[]int{10,20,30,40}
slice[0]=200
fmt.Println(slice)[200 20 30 40]
slice2 :=slice[1:3]//创建一个长度为2 容量为3的切片
fmt.Println(slice2)//[20 30]
可以看出来,切片slice和slice2共用一个底层数组,只是slice的底层指针指向数组的首个元素,slice2的底层指针指向的是数组的下标为1的地址;
4.2、切片计算长度和容量
对于底层数组的容量为k的切片 slice[i:j]来算
长度:j - i
容量:k - i
4.3、切片越界
slice :=[] int {10,20,30,40,50}
slice2:=slice[1:3]
slice2[2]=100
fmt.Println(slice2)//panic: runtime error: index out of range
切片只能访问到其长度内的元素,试图访问超出长度的元素会导致语言运行时异常
4.4、切片增长
使用切片的一个好处就是可以按需要增加切片的容量。go语言内置的append函数会处理增加长度时的所有的细节操作
slice:=[]string{"nick","jhon","jack","Lili","Cat"}
slice2:=slice[1:3]
fmt.Println(slice2)
//fmt.Println(len(slice2))
slice2 =append(slice2,"abc")
fmt.Println(slice2)
使用append来追加切片的长度和容量
slice:=[]string{"nick","jhon","jack","Lili","Cat"}
fmt.Println(slice)//[nick jhon jack Lili Cat]
slice2:=slice[1:3]
fmt.Println(slice2)//[jhon jack]
slice2 =append(slice2,"abc")//全新的底层数组
fmt.Println(slice2)//[jhon jack abc]
fmt.Println(slice)//[nick jhon jack abc Cat]
/*
上述代码可以看到,切片slice和slice2都是共用同一个底层数组的,在slice2扩容之后slice2[扩容]和之前的slice2,还有slice还是共用同一个底层数组,所以扩容的时候,当容量没有超过原始切片slice的原始长度
*/
4.5、切片增加长度和容量
append给切片增加长度和容量,当切片的底层数组没有足够可用容量,append函数会创建一个新的底层数组,将被引用的现有的值复制到新的数组,再追加新的值
slice:=[]int{10,20,30,40}
newSlice:=append(slice,50)
fmt.Println(&newSlice[0])//0xc000086080
fmt.Println(&slice[0])//0xc0000560c0
/*
可以看出newSlice是一个全新的底层数组
*/
append扩容机制:在切片的容量小于1000个元素时,总是会成倍的增加容量,一旦元素个数超过1000个,容量为的增长因子会设为1.25,也就是每次增加25%
4.6、使用三个索引创建切片
用第三个索引选项来完成切片的操作,第三个参数限制切片的容量
source :=[]string{"apple","orange","plum","banana","grape"}
slice :=source[2:3:4] //第三个参数限制切片的容量,计算公式
fmt.Println(slice)
-
计算公式 slice[ i : j : k] 或者[ 2 : 3 :4]
长度:j - i
容量:k - i
注意如果第三个参数设置的容量大于已有的容量时候,会报错
source :=[]string{"apple","orange","plum","banana","grape"}
slice :=source[2:3:6] //第三个参数限制切片的容量,计算公式
fmt.Println(slice)//panic: runtime error: slice bounds out of range
4.7、设置长度和容量一样的好处
函数append会先使用可用容量,一旦没有可用容量,会分配一个新的底层数组。这样会导致很容易忘记切片之间正在共用一个底层数组。一旦发生这种情况,对切片进行修改,会导致随机且起卦的问题,对切片内容的修改会影响多个切片,却很难找到问题所在。
但是如果在创建切片时候,设置切片的长度和容量一样,就可以强制让切片的第一个append操作创建新的底层数组,与原有的底层数组进行分离。
source :=[]string{"apple","orange","plum","banana","grape"}
slice :=source[2:3:3]
slice2 := append(slice, "kimi")
fmt.Println(&slice[0])//0xc00008e020
fmt.Println(&slice2[0])//0xc000052400
slice3 :=source[2:3:5]
slice4 := append(slice3, "kimi")
fmt.Println(&slice3[0])//0xc00008e020
fmt.Println(&slice4[0])//0xc00008e020
-
因为使用新的切片slice2拥有自己的底层数组,所以杜绝了可能发生的问题
4.8、切片追加到另外一个切片
append是一个可变参数的函数,如果使用...运算符,可以将一个切片的所有元素,追加到另一个切片里
s0 :=make([]int,10,20)
s1 :=[]int{1,2,3} //切片的长度和容量都是3
s2 :=[]int{4,5}
s3 :=append(s1,s2...)
s4 :=append(s0,s2...)
fmt.Println(&s1[0])//0xc0000560c0
fmt.Println(&s2[0])//0xc000058080
fmt.Println(s3)//[1 2 3 4 5]
fmt.Println(&s3[0])//0xc00007e030
fmt.Println(&s0[0])//0xc00008e000
fmt.Println(&s4[0])//0xc00008e000
4.9、迭代切片
通过for range 来迭代切片的元素
slice :=[]int{1,2,3,4,5,6}
for index,value:=range slice{
fmt.Println(&value)//发现每次地址都是相同的
fmt.Println(index,value)
}
/*
range 提供了每个元素的副本,而不是直接返回对该元素的引用
如果
*/
4.10、使用空白标识符来忽略索引值
_下划线来忽略索引值
slice :=[]int{1,2,3,4,5,6}
for _,value:=range slice{
fmt.Println(value)
}
4.11、通过for来迭代切片
range总是会从切片头部开始迭代,如果想对迭代做更多控制,依旧可以使用传统的for 循环
for index:=2;index<len(slice);index++{
fmt.Println(slice[index])
}
5、多维切片
和数组一样,切片是一维的,不过和数组一样,可以组合多个切片形成多维切片
slice:=[][]int{{10},{100,200}}
for _,value:=range slice{
fmt.Println(&value[0])
//0xc00000a0d8
//0xc00000a0f0
}
for i:=0;i< len(slice);i++{
fmt.Println(&slice[i][0])
//0xc00000a0d8
//0xc00000a0f0
}
//我认为,在一维切片中,for range value是传递的值,二维切片中出外层传递的是地址
6、切片在函数中传递
在函数间传递切片就是要在函数间以值得方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片的成本也很低
func main() {
slice:=make([]int,10000)
fmt.Println(slice[0])//0
testSlice(slice)
fmt.Println(slice[0])//100
}
func testSlice(temp []int) {
temp[0]=100
}
/**
切片的复制,只是复制指针
*/
三、映射map
1、内部实现
映射是一种数据结构,用户存储一系列无序的键值对
2、创建和初始化
2.1、make函数初始化、字面量初始化
dict:=make(map[string]int)
dict1:=map[string]string{"red":"#da12"}//字面量初始化
fmt.Println(dict,dict1["red"])
2.2、字面量声明空映射
//切片,函数具有引用意义,不能作为映射的键
dict := map[[]string]int{}//invalid map key type []string
//但是没有任何理由阻止用户使用切片座位映射的值
dict1 :=map[int][]string{}
2.3、nil映射
声明一个未初始化的nil映射,但是nil映射不能用户存储键值对,否则会产生错误
var colors map[string]string
colors["red"]="#da1337" //panic: assignment to entry in nil map
2.4、判断值是否存在
从映射里面取值两个选择
-
同时获取值,以及一个表示这个键是否存在的标志
-
只返回对应的值,然后判断这个值是不是零值来确定键的存在
color:=map[string]string{"red":"#red","blue":"#blue"}
var color1 map[string]string=map[string]string{"red":"#red","blue":"#blue"}
value,exists :=color["red"]
if(exists){
fmt.Println(value)
}
value1:=color1["blue"]
if value1!=""{
fmt.Println(value1)
}
映射中,如果键不存在,也会返回一个值,对应类型的零值
2.5、删除映射中的值
使用内置的delete函数,删除
colors :=map[string]string{
"AliceBlue":"#f0f8ff",
"Coral" : "#FF7F50",
"White":"",
}
for key,value:=range colors{
fmt.Println(key,value)
}
delete(colors,"White")
for key,value:=range colors{
fmt.Println(key,value)
}
3、在函数中传递映射
在函数间传递映射并不会制造出该映射的副本。世纪上,当传递映射给一个函数,并对这个映射做修改时,所有对这个映射的应用都会觉察到这个修改
文章评论