【Go语言】复合数据类型(数组,切片,管道,哈希键值对)
一、数组 Array
数组是快连续的内存空间,在声明的时候必须指定长度,且长度不能改变,所以数组在声明的时候就可以把内存空间分配好,并赋上默认值,即完成了初始化
数组的地址,就是首元素地址
数组的初始化
func Var_array() { var arr1 [5]int = [5]int{} // 声明数组时指定长度和类型,且长度和类型指定后不可修改 var arr2 = [5]int{} //对数组进行初始化赋值时定义长度和类型 var arr3 = [5]int{2, 3} // 给前两个元素赋值 var arr4 = [5]int{2: 15, 4: 30} //指定元素进行赋值 var arr5 = [...]int{3, 2, 4, 15, 32, 22} //根据{}中的元素个数自动定义数组长度 var arr6 = [...]struct { name string age int8 city string }{{"Tom", 23, "ShenZhen"}, {"Janzen", 25, "ShangHai"}, {"Sophia", 24, "ShenZhen"}} // 数组的元素类型由匿名结构体给定 }二维数组初始化
func Var_array2() { var arr1 = [5][3]int{{23, 21, 2}, {2: 21}} //定义一个5行3列的二维数组,前两行元素为 {23,21,2} {0,0,21} var arr2 = [...][5]int{{1, 3}, {3: 3}} // 二维数组,第一维可以使用[...],第二维不可使用[...] }PS F:\go\go_project\test> go run .\main.go arr1 = [[23 21 2] [0 0 21] [0 0 0] [0 0 0] [0 0 0]] arr2 = [[1 3 0 0 0] [0 0 0 3 0]]
数组里的元素访问
- 通过index访问
- 首元素 arr[0]
- 末元素 arr[len(arr)-1]
- 访问二维数组里的元素
- 获取第三行第四列的元素 arr[2][3]
遍历数组
范例:
func Foreach_array() { // 定义一个一维数组,数组的元素类型由匿名结构体给定 var arr1 = [...]struct { name string age int8 city string }{{"Tom", 23, "ShenZhen"}, {"Janzen", 25, "ShangHai"}, {"Sophia", 24, "ShenZhen"}} // 使用index,value 方式遍历数组,如果不需要使用index值,
// 如果不需要 index 值,可写成 for _,ele := range arr1{} ;
// for i := range arr1{} 等价于 for i,_ := range arr1{} for i, ele := range arr1 { fmt.Printf("index=%d, value=%v\n", i, ele) } // 使用下标方式遍历数组 for i := 0; i < len(arr1); i++ { //声明并赋值i, 当i小于arr1长度时,执行循环,代码块完成后执行i++ fmt.Printf("index=%d, value=%v\n", i, arr1[i]) } //定义一个5行3列的二维数组,前两行元素为 {23,21,2} {0,0,21} var arr2 = [5][3]int{{23, 21, 2}, {2: 21}} // 使用双层嵌套循环实现遍历二维数组 for row, array := range arr2 { for col, ele := range array { fmt.Printf("arr2[%d][%d] = %v\n", row, col, ele) } } }输出结果:
cap 和 len
- cap 代表 capacity 容量
- len 代表 length 长度
- len 代表目前数组里有几个元素,cap 代表给数组分配的内存空间可以容纳多少个元素
- 由于数组初始化之后长度不会改变,不需要给它预留内存空间,所以 len(arr) == cap(arr)
数组传参
- 数组的长度和类型都是数组的一部分,函数传递数组类型时这两部分必须吻合(函数内定义接收传参的数组长度和类型,必须与实际传入的数组格式一致)
- go语言没有按引用传参,全部是按值传参,即传参实际传递的是数组的拷贝,当数组长度很大时,仅传参消耗就很大(函数内修改传入的数组,不影响函数外部原数组值)
- 在函数内修改函数外部的数组,可以通过传递数组的指针实现,传参消耗相对较小(数组在内存中的地址)
范例:
// 函数定义传参数组格式,求数组内的平均值 func Avg_arry(arr [5]float64) float64 { var sum float64 for _, ele := range arr { sum += ele } avg := sum / float64(len(arr)) return avg } func main() { arr := [5]float64{3.13, 3, 15, 23, 1} avg := data_type.Avg_arry(arr) fmt.Println(avg) }输出结果:
范例:
// 验证函数传递数组作为参数,在函数内对数组的修改 func Arr_array(arr [5]int) { fmt.Printf("arr's ptr = %p\n", &arr) fmt.Printf("old arr[0] = %d\n", arr[0]) arr[0] += 10 //等价于 (*arr)[0] += 10 fmt.Printf("new arr[0] = %d\n", arr[0]) } func main() { crr := [5]int{13, 3, 15, 23, 1} fmt.Printf("crr's ptr = %p\n", &crr) data_type.Arr_array(crr) fmt.Printf("crr[0] = %d\n", crr[0]) }输出结果:
范例:
// 验证函数传递数组指针作为参数,在函数内对数组的修改 func Ptr_array(arr *[5]int) { fmt.Printf("arr's ptr = %p\n", arr) fmt.Printf("old arr[0] = %d\n", arr[0]) arr[0] += 10 fmt.Printf("new arr[0] = %d\n", arr[0]) } func main() { crr := [5]int{13, 3, 15, 23, 1} fmt.Printf("crr's ptr = %p\n", &crr) data_type.Ptr_array(&crr) fmt.Printf("crr[0] = %d\n", crr[0]) }输出结果:
二、切片 Slice
- 切片具备三个元素:array (unsafe.Pointer)、len (int)、cap(int)
- 切片指针 和 底层数组首元素(底层数组)指针 不是同一个指针
切片的初始化
范例:
func Slice_var() { var s1 []int s2 := []int{} s3 := make([]int, 3) s4 := make([]int, 3, 5) s5 := []int{1, 2, 3, 4, 5} s6 := [][]int{ {1, 2, 3}, {2, 3, 1, 1}, } fmt.Printf("s1 = %v,\tlen = %d,\tcap=%d\n", s1, len(s1), cap(s1)) fmt.Printf("s2 = %v,\tlen = %d,\tcap=%d\n", s2, len(s2), cap(s2)) fmt.Printf("s3 = %v,\tlen = %d,\tcap=%d\n", s3, len(s3), cap(s3)) fmt.Printf("s4 = %v,\tlen = %d,\tcap=%d\n", s4, len(s4), cap(s4)) fmt.Printf("s5 = %v,\tlen = %d,\tcap=%d\n", s5, len(s5), cap(s5)) fmt.Printf("s6 = %v,\tlen = %d,\tcap=%d\n", s6, len(s6), cap(s6)) } func main() { Slice_var() }
输出结果:
append
- 切片相对于数组最大的特点就是可以追加元素,可以自动扩容
- 追加的元素放到预留的内存空间里,同时 len 加 1
- 如果预留空间已用完,则会重新申请一块更大的底层数据内存空间,capacity 变成之前的2倍(cap < 1024)或 1.25倍(cap > 1024)。把原内存空间的数据拷贝过来,在新的内存空间 上执行append操作
范例:
func Append_slice() { s := make([]int, 3, 5) fmt.Printf("s = %v,\tlen = %d,\tcap=%d,\t(%p)\n", s, len(s), cap(s), &s) s = append(s, 100) fmt.Printf("s = %v,\tlen = %d,\tcap=%d,\t(%p)\n", s, len(s), cap(s), &s) }
输出结果:
范例:
func coef_cap(n int) { s := make([]int, 0, 5) prevCap := cap(s) prevPtr := &s[0] for i := 0; i < n; i++ { s = append(s, 100) currCap := cap(s) currPtr := &s[0] if currCap > prevCap { fmt.Printf("cap %d --> cap %d\t", prevCap, currCap) fmt.Printf("ptr %p --> ptr %p\n", prevPtr, currPtr) prevCap = currCap prevPtr = currPtr } } } func main() { coef_cap(5000) }
输出结果:
截取子切片
arr := make([]int, 4, 6) //母切片
crr := arr[1:3] //子切片
- 刚开始,子切片和母切片的底层数据共享内存空间,修改子切片会反映到母切片上,在子切片上执行 append 操作,会把新增的元素放到母切片的预留空间的内存上
- 当子切片不断执行 append,消耗光了母切片的预留内存空间,子切片会跟母切片发生内存分离,此后两个切片没有任何关系
范例:
func sub_slice(n int) { arr := make([]int, 4, 6) arr[3] = 10 fmt.Printf("arr=%v,(%p)\n", arr, &arr[0]) crr := arr[1:3] //前闭后开 crr[0] = 8 crr[1] = 7 fmt.Printf("arr=%v,(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0]) for i := 0; i < n; i++ { crr = append(crr, 1) fmt.Printf("arr=%v(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0]) } arr = append(arr, 2) crr[2] = 99 fmt.Printf("arr=%v(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0]) } func main() { sub_slice(8) }
输出结果:
子切片调用导致的内存泄露情况
由于子切片与母切片共享底层内存空间,所以当子切片被调用时,母切片也处于被子切片调用状态,因此程序不会回收母切片占用空间,就会导致母切片过度占用内存空间引发内存泄露情况。
可能引发内存泄露的代码片段
// 下方程序会导致 foo() 中的 arr 长时间处于被调用状态,导致内存泄露 func foo () []int { arr := make([]int,1<<20) // 占用1M内存的切片 /* arr 进行数据处理 */ return arr[3:10] } func main() { crr := foo() fmt.Println(crr) /* 程序代码片段2 */
}
解决方案:
1、通过将所需调用的子切片重新赋值给一个新的切片,从而减少对原切片的持续占用
func foo () []int { arr := make([]int,1<<20) // 占用1M内存的切片 /* arr 进行数据处理 */ brr := make([]int,0,10) for i:=3;i<10;i++ { brr = append(brr,arr[i]) } return brr } func main() { crr := foo() fmt.Println(crr) /* 程序代码片段2 */ }
2、针对指针切片时,需要舍弃的元素数量较小时,可以将舍弃元素赋值为 nil 实现对舍弃元素的内存空间释放
type user struct { Nmae string Age int8 } func foo2() []*user { arr := make([]*user, 20) // 占用1M内存的切片 /* arr 进行数据处理,舍弃末尾3个元素 arr[:len(arr)-3] */ arr[len(arr)-3] = nil arr[len(arr)-2] = nil arr[len(arr)-1] = nil return arr } func main() { crr := foo2() fmt.Println(crr) /* 程序代码片段2 */ }
切片传参
go语言函数传参,传递的都是值,即传切片会把切片的{array.Pointer,len,cap} 这3个字段拷贝一份到函数内部
由于传递的是底层数组的指针,所以可以直接修改切片底层数组里的元素,会反映到原切片上
范例:
func update_slice(arr []int) { arr[0] = 21 fmt.Printf("arr=%v(%p)\tarray.Pointer = %p\n", arr, &arr, &arr[0]) } func main() { crr := []int{1, 2, 3, 4} fmt.Printf("crr=%v(%p)\tarray.Pointer = %p\n", crr, &crr, &crr[0]) update_slice(crr) fmt.Printf("crr=%v(%p)\tarray.Pointer = %p\n", crr, &crr, &crr[0]) }
输出结果:
三、哈希 map
map 底层原理是 hash table,根据key查找value的时间复杂度为O(1)
map 中的 key 可以是任意能够使用 == 进行比较的类型,不能是 函数、map、切片,以及包含上述3种类型成员变量的 结构体 struct。
map 中的 value 可以是任意类型
slot 槽位中只保存目标数据的指针
冲突:当多个 key值 的 哈希取模 结果相同时,会在发生冲突的槽位上以链表方式保存多个 key 的指针,查询时对冲突槽位进行遍历查询
扩容:当map中出现过多冲突数据时,会发生槽位扩容,当触发槽位扩容时,会对所有数据重新进行哈希取模计算,并重新分配槽位。发生扩容时严重影响系统性能,尽可能避免map发生扩容情况
map的初始化
范例:
func map_var() { var m1 map[string]int // 声明map,指定key 和 value 的类型 fmt.Printf("m1 = %v\n", m1) m2 := make(map[string]int) // 初始化map,容量为0 fmt.Printf("m2 = %v\n", m2) m3 := make(map[string]int, 5) // 初始化map,容量为5 fmt.Printf("m3 = %v\n", m3) m4 := map[string]int{"语文": 95, "数学": 80} //初始化map时直接赋值 fmt.Printf("m4 = %v\n", m4) } func main() { map_var() }输出结果:
添加和删除key
范例:
func map_fixed() { m := map[string]int{"语文": 95, "数学": 80} fmt.Printf("m = %v\n", m) m["英语"] = 99 // key 不存在,直接添加key-value fmt.Printf("m = %v\n", m) m["英语"] = 89 // key 存在,直接更新key-value fmt.Printf("m = %v\n", m) delete(m, "数学") // 删除 key-value fmt.Printf("m = %v, len = %d\n", m, len(m)) } func main() { map_fixed() }
输出结果:
查看key对应的value值
范例:
//不推荐采用 value:=m["key"] 方式获取
func select_vale_1() { m := map[string]int{"语文": 95, "数学": 80} value1 := m["数学"] // key 值存在时,返回对应的value value2 := m["英语"] // key 值不存在时,返回value定义的默认值 fmt.Printf("m[\"数学\"] = %v\n", value1) fmt.Printf("m[\"英语\"] = %v\n", value2) } func main() { // map_var() // map_fixed() select_vale_1() }
输出结果:
范例:
func select_vale_2(key string) { m := map[string]int{"语文": 95, "数学": 80} if value, exists := m[key]; exists { fmt.Printf("m[\"%s\"] = %d\n", key, value) } else { fmt.Printf("m[\"%s\"] 不存在\n", key) } } func main() { select_vale_2("语文") select_vale_2("英语") }
输出结果:
遍历map
map 的 key值 排布相对顺序是固定的,但每次遍历出来的排序次序可能存在不一致(环形排列,随机起始)
范例:
func map_for() { m := make(map[string]int, 5) m["A"] = 1 m["S"] = 2 m["D"] = 3 m["W"] = 4 fmt.Printf("m = %v\n", m) for key, value := range m { fmt.Printf("key = %s , value = %d\n", key, value) } fmt.Println("=================") for key, value := range m { fmt.Printf("key = %s , value = %d\n", key, value) } for key := range m { fmt.Printf("key = %s\n", key) } for _, value := range m { fmt.Printf("value = %d\n", value) } } func main() { map_for() }
输出结果:
![]()
范例:
func main() { m := map[string]int{"A": 21, "B": 23, "F": 65, "T": 31} fmt.Println(m) // 不会实际修改 m 里的内容 for key, value := range m { value += 2 fmt.Printf("value = %d , key-value = %d\n", value, m[key]) } fmt.Println(m) // 遍历修改 m 里的内容 for key := range m { m[key] += 2 } fmt.Println(m) }
输出结果:
四、管道 channl
- 管道的底层是一个环形队列(先进先出),send(插入)和 recv(取走)从同一个位置沿同一个方向顺序执行
- sendx 表示最后一次插入的元素位置,recvx 表示最后一次取走的元素位置
channl 初始化
范例:
func channl_var() { var ch chan int // 声明管道 fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) ch = make(chan int, 8) //初始化,环形队列中可容纳8个int fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) } func main() { channl_var() }输出结果:
send 和 recv
范例:
func channl_send_recv() { ch := make(chan int, 8) fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) ch <- 1 // 往管道中写入(send)元素 fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) v := <-ch // 往管道中取走(recv)元素 fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) fmt.Printf("v type is %T, v = %v\n", v, v) ch <- 2 ch <- 3 fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) v = <-ch v = <-ch fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch) } func main() { channl_send_recv() }输出结果:
当管道内的元素,达到最大容量时(len=cap),再进行元素插入会发生阻塞导致后续的代码执行失败
当管道内的元素,全部被取完时(len = 0),再进行元素取出也会发生阻塞导致后续的代码执行失败
范例:
func channl_send_out() { ch := make(chan int, 8) for i := 0; i < 8; i++ { ch <- 10 } fmt.Printf("ch type is %T, len = %d, cap = %d\n", ch, len(ch), cap(ch)) ch <- 11 fmt.Printf("ch type is %T, len = %d, cap = %d\n", ch, len(ch), cap(ch)) } func main() { channl_send_out() }输出结果:
遍历管道
范例:
func channl_for_1() { ch := make(chan int, 8) for i := 0; i < cap(ch); i++ { ch <- i } L := len(ch) for i := 0; i < L; i++ { ele := <-ch fmt.Printf("ele = %v, ch len=%d\n", ele, len(ch)) } fmt.Printf("len ch = %d\n", len(ch)) } func channl_for_2() { ch := make(chan int, 8) for i := 0; i < cap(ch); i++ { ch <- i } close(ch) //使用此种方法遍历管道之前需要先关闭管道,禁止写入新的元素 for ele := range ch { fmt.Printf("ele = %v, ch len=%d\n", ele, len(ch)) } fmt.Printf("len ch = %d\n", len(ch)) } func main() { fmt.Println("========for 1========") channl_for_1() fmt.Println("\n========for 2========") channl_for_2() }输出结果:
引用类型
- slice、map、channl 都是go中的3种引用类型,都可以通过 make 函数进行初始化(申请内存分配)
- 因为它们都包含一个指向底层数据结构的指针,所以称之为“引用”类型
- 引用类型未初始化时都是 nil ,可以对它们使用 len() 函数,返回 0