数组
- 数组的创建后,长度是固定的。
- 数组是值类型,数组的赋值都是拷贝。
- 数组的长度也是类型的一部分。[10]int,[20]int是不同的类型
package main
import (
"fmt"
)
func main() {
// 定义数组,数组是包含了数组长度和类型。
// 所以下面是两种不通的数组类型
var array1 [3]int
var array2 [4]int
// 数组的初始化
array1[0] = 10
array2[0] = 1
array2[1] = 2
// 使用数组的字面量创建数组
// 虽然长度是3,但是可以值给2个赋值,剩下的第三个元素默认为0
array3 := [3]int{100, 200}
// 可以推断数组长度,可以省略
array4 := [...]int{100, 200}
fmt.Println(array1, array2, array3, array4)
// 也可以根据索引,只给其中的某些指定的索引位赋值
array5 := [10]int{0: 12, 9: 100}
fmt.Println(array5)
// 如果自动推断数组长度,则根据最大的索引为数组的最大长度
// 这里最大的索引为3,则数组的长度为4
array6 := [...]int{0: 20, 3: 1}
fmt.Println(array6)
}
数组的遍历,多维数组
package main
import (
"fmt"
)
func main() {
// 遍历数组
var array1 = [10]int{}
for i := 0; i < 10; i++ {
array1[i] = i * 10
}
for i := 0; i < 10; i++ {
fmt.Println(array1[i])
}
// 多维数组
var array2 [3][2]int
// 这里大括号前面还得写类型
array2 = [3][2]int{{1, 2}, {2, 3}, {3, 4}}
for i := 0; i < 3; i++ {
for j := 0; j < 2; j++ {
fmt.Printf("%v", array2[i][j])
}
fmt.Println()
}
array3 := [3][2]int{{1, 2}, {2, 3}, {3, 4}}
for i := 0; i < 3; i++ {
for j := 0; j < 2; j++ {
fmt.Printf("%v", array3[i][j])
}
fmt.Println()
}
}
Slice
切片是引用类型;内部header包含:底层数组地址、长度(len)、容量(cap)。 ✅ 可以自动扩容(
append);没有原生自动缩容。
1. 从数组/切片生成新切片
切片表达式 s[low:high],左闭右开:包含low,不包含high,新切片和原切片共享底层数组。
var a = []int{1, 2, 3}
var slice1 = a[1:] // low=1,省略high → 取到末尾
var slice2 = a[1:2] // [1,2),只取下标1
// a=[1 2 3], slice1=[2 3], slice2=[2]
fmt.Println(a, slice1, slice2)
// 通过索引读取元素,和数组语法一致
var lastValue = slice1[len(slice1)-1]
fmt.Println(lastValue) // 3
注意:共享底层数组,修改新切片元素会影响原切片。
2. 直接声明切片
// 仅声明切片header,底层数组为nil;len=0,cap=0
var slice1 []int
// 切片字面量,创建空切片,底层数组非nil;len=0,cap=0
var slice2 = []int{}
区分:
var slice1 []int是nil切片;[]int{}是空非nil切片。 nil切片也可以直接append,append内部会自动分配底层数组。
3. make 创建切片
语法:make([]T, len, cap)
-
len:有效元素个数,初始化默认零值
-
cap:底层数组容量,可省略;省略时
cap = len预估数据量时,提前设置cap,减少多次扩容拷贝开销
// len=2,cap=2,两个元素默认int零值0
slice1 := make([]int, 2)
fmt.Println(slice1) // [0 0]
4. 切片字面量创建
直接初始化元素,cap 默认等于len
slice1 := []int{1, 2, 3}
slice2 := []string{"a", "b", "c"}
fmt.Println(slice1) // [1 2 3]
fmt.Println(slice2) // [a b c]
5. append 追加元素 & 扩容机制
append 返回新切片;不扩容时底层数组不变;触发扩容会分配全新底层数组,内存地址改变,拷贝旧数据。
var slice1 []int
var slice2 []int
preCap := -1
for i := 0; i < 100000; i++ {
slice1 = append(slice1, i)
currentCap := cap(slice1)
if currentCap != preCap {
preCap = currentCap
slice2 = append(slice2, currentCap)
}
}
fmt.Println(slice2)
// [4 8 16 32 64 128 256 512 848 1280 1792 2560 3408 5120 7168 9216 12288 16384 21504 27648 34816 44032 55296 69632 88064 110592]
扩容规则(Go1.18+ growslice)
-
追加后容量足够:不扩容,复用原底层数组
-
容量不足,进入扩容计算:
- 旧cap < 256:新容量 = 旧cap * 2
- 旧cap ≥ 256:按
newcap += (newcap + 3*256)/4逐步增长,直到能放下新元素
-
roundupsize内存对齐(⭐容易忽略)-
上面算出的是理论元素数量;
-
计算总字节:
总字节 = newcap * 元素大小 -
向上对齐到 runtime
size-class固定内存档,得到实际分配内存 -
真实cap = 对齐后的总字节 / 单个元素字节
示例:oldCap=512 理论newcap = 512 + (512+768)/4 = 832 832 * 8 = 6656B;查表大于等于6656最小size-class是6784B 6784 /8 = 848 → 最终cap=848
-
runtime size-class:
Go内存分配器把小对象(≤32KB)划分成多档固定规格,每一档叫size-class,减少内存碎片,方便GC。
// class | bytes/obj
// 47 6528
// 48 6784 ← 本例用到
// 49 6912
// 50 8192
6. copy 切片拷贝
copy(dst, src)
- 返回值:成功复制元素个数 =
min(len(dst), len(src)) - ✅ copy会创建独立副本,是唯一能脱离原底层数组,真正缩减底层内存占用的方式。
slice1 := []int{1, 2, 3}
var slice2 []int
count := copy(slice2, slice1)
fmt.Println(count, slice2) // 0 [],dst长度为0,无法拷贝
var slice3 = make([]int, 10)
count = copy(slice3, slice1)
fmt.Println(count, slice3) // 3 [1 2 3 0 0 0 0 0 0]
7. 切片删除元素
⚠️ 切片截取 s[:n] 只是修改切片header的len,底层数组不变,cap不变,内存不会释放! 新切片和原切片依然共享底层数组,修改会互相影响。
slice1 := []int{1, 2, 3, 4, 5}
fmt.Println(len(slice1), cap(slice1)) // 5 5
// 截取,缩短有效长度,底层数组不变
slice1 = slice1[:2]
fmt.Println(slice1) // [1 2]
fmt.Println(len(slice1), cap(slice1))// 2 5
slice2 := []int{1, 2, 3, 4, 5}
slice2_2 := slice2[:2]
fmt.Println(slice2_2, slice2) // [1 2] [1 2 3 4 5]
fmt.Println(cap(slice2), cap(slice2_2)) //5 5
// 共享底层数组,修改互相影响
slice2_2[0] = 100
fmt.Println(slice2[0]) // 100
内存泄漏风险重点笔记
如果从一个超大底层数组切出一小段切片保存,大底层数组不会被GC回收,只有所有引用该底层数组的切片全部置nil,底层数组才会GC。 想要释放多余内存:
copy到新的小切片,抛弃原大切片。
映射map
package main
import (
"fmt"
)
func main() {
// 声明map
var map1 map[string]string
//初始化map
map1 = make(map[string]string)
map2 := map[string]string{"name": "libai", "addr": "china"}
fmt.Println(map1)
fmt.Println(map2)
// map的遍历
for index, value := range map2 {
fmt.Println(index, value)
}
// 删除元素
delete(map2, "name")
fmt.Println(map2)
}
列表list
package main
import (
"container/list"
"fmt"
)
func main() {
// 列表,内部实现是双链表
var list1 = list.New()
// 此时处于零值状态(延迟初始化)
var list2 list.List
// 【输出1】打印初始的空链表 list1 (指针类型,长度为0)
fmt.Println(list1)
// &{{0x5108dba780f0 0x5108dba780f0 <nil> <nil>} 0}
// 【输出2】打印零值状态的 list2 (非指针,内部全为nil,长度为0)
fmt.Println(list2)
// {{<nil> <nil> <nil> <nil>} 0}
hello := list1.PushBack("Hello") // 尾部插入 "Hello"
hi := list1.PushFront("hi") // 头部插入 "hi"
// 【输出3】打印插入两个元素后的 list1 (长度变为2)
fmt.Println(list1)
// &{{0x5108dba781b0 0x5108dba78180 <nil> <nil>} 2}
// 第一次遍历链表并打印元素的 Value
for i := list1.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
// 【输出4】按插入顺序打印:先插的 hi 在头,后插的 Hello 在尾
// hi
// Hello
// 在 hello 节点之后插入 "China"
list1.InsertAfter("China", hello)
// 在 hi 节点之前插入 "你好"
list1.InsertBefore("你好", hi)
// 第二次遍历链表并打印元素的 Value
for i := list1.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
// 【输出5】最终链表顺序:你好 -> hi -> Hello -> China
// 你好
// hi
// Hello
// China
}
删除元素
//参数为element,每次插入会后返回这个element
list.Remove(hi)