golang 数组 切片slice append copy 映射map 列表list

37 阅读7分钟

数组

  • 数组的创建后,长度是固定的。
  • 数组是值类型,数组的赋值都是拷贝。
  • 数组的长度也是类型的一部分。[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)

  1. 追加后容量足够:不扩容,复用原底层数组

  2. 容量不足,进入扩容计算:

    • 旧cap < 256:新容量 = 旧cap * 2
    • 旧cap ≥ 256:按 newcap += (newcap + 3*256)/4 逐步增长,直到能放下新元素
  3. 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)