Skip to content

数组切片与Map ​

七、数组 ​

7.1、命名 ​

go
var 数组名 [数组大小]数组类型

7.2、初始化 ​

go
第一种:
var arr1 [3]int = [3]int{1,2,3}
第二种:
var arr2 = [3]int{4,5,6}
第三种:
var arr3 = [...]int{12,34,45,65,67}
第四种:
var arr4 = [...]int{2:22, 1:11, 3:33, 0:55}

7.3、类型 ​

数组长度属于数组类型。

7.4、二维数组 ​

遍历

方式一:for循环遍历

方式二:for-range遍历

go
func main() {
	var arr [3][3]int = [3][3]int{
		{1, 2, 3},
		{4, 5, 6},
		{7, 8, 9},
	}
	fmt.Println(arr)

	// 方式一:for循环遍历
	for i := 0; i < len(arr); i++ {
		for j := 0; j < len(arr[i]); j++ {
			fmt.Printf("arr[%d][%d] = %d \t", i, j, arr[i][j])
		}
		fmt.Println()
	}

	fmt.Println("-----------------")

	// 方式二:range循环遍历
	for i, v := range arr {
		for j, v2 := range v {
			fmt.Printf("arr[%d][%d] = %d \t", i, j, v2)
		}
		fmt.Println()
	}
}

八、切片---左闭右开 ​

切片是建立在数组之上

go
func main() {
	var arr [6]int = [6]int{3, 5, 6, 7, 8, 9}
	// 切片是构建在数组之上
	var slice []int = arr[2:5]
	fmt.Println(slice)
}

切片(slice) 是一种动态数组。切片提供了便捷、灵活的数组操作方式,底层结构可以动态增长或缩小。

切片的底层结构 ​

在 Go 中,切片的底层是基于数组实现的,其底层结构由一个 sliceHeader 管理,定义在 reflect 包中,包含以下三部分:

  1. 指针(Pointer):指向切片底层数组的起始位置。
  2. 长度(Length):表示切片当前的长度,即切片实际存储的元素个数。
  3. 容量(Capacity):表示从切片起始位置到底层数组末尾的最大可用空间。

因此,切片可以表示为以下结构:

go
type sliceHeader struct {
    ptr    *elementType // 指向底层数组的指针
    length int          // 当前切片长度
    cap    int          // 当前切片的容量
}

内存布局示意图 ​

假设有以下代码:

go
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]

在这段代码中,arr 是一个长度为 5 的数组,而 s 是一个切片,表示 arr 中从索引 1 到 3 的部分。底层内存布局如下:

go
底层数组: [1, 2, 3, 4, 5]
              ↑     ↑
           起始位置   长度为3
切片 s:   指向 arr[1](值为2),长度为3,容量为4(从arr[1]到arr[4])
  • s.ptr 指向 arr[1] 的位置。
  • s.length 为 3,表示 s 包含的元素数量 [2, 3, 4]。
  • s.cap 为 4,因为从 arr[1] 开始到数组末尾一共有 4 个元素。

切片扩容 ​

Go 的切片具有动态扩容能力,当切片使用 append 操作超出其容量时,Go 会分配一个新的、更大容量的底层数组,并将原切片的数据复制到新数组中。扩容机制一般遵循以下原则:

  1. 小切片(小于 1024 元素):每次扩容时,容量翻倍。
  2. 大切片(大于等于 1024 元素):每次扩容增加原来容量的 1/4 左右。

举例:

go
s := make([]int, 2, 2) // 创建一个长度为2,容量为2的切片
s = append(s, 1)       // 超出容量,触发扩容

在 append 操作中,由于超出容量,Go 会分配一个新的切片,长度为 3,容量为 4,并将数据 [0, 0, 1] 复制到新切片。

切片的共享与独立 ​

在使用切片时要注意切片可能会共享同一个底层数组。例如:

go
arr := []int{1, 2, 3, 4, 5}
s1 := arr[1:4]
s2 := arr[2:5]
s1[1] = 10
fmt.Println(arr) // 输出:[1, 2, 10, 4, 5]

在这里,s1 和 s2 都指向了 arr 的一部分,因此修改 s1 的值会影响 s2 和 arr。要独立地使用切片,可以使用 copy 函数创建切片的副本。

切片的拷贝 ​

通过 copy 函数来实现对切片的拷贝,copy 会复制切片的内容,但不会共享底层数组。这意味着拷贝后的切片与原切片独立存储,修改其中一个切片不会影响另一个。

copy 函数的用法 ​

copy 函数的语法为:

go
copy(dst, src []Type) int
  • dst 是目标切片(拷贝到的切片),src 是源切片(被拷贝的切片)。
  • copy 返回复制的元素个数。它只会复制 dst 和 src 之间较小长度的部分。

示例代码 ​

go
package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, len(src))

    // 使用 copy 函数将 src 拷贝到 dst
    count := copy(dst, src)

    fmt.Println("拷贝的元素个数:", count) // 输出:拷贝的元素个数: 5
    fmt.Println("源切片:", src)        // 输出:源切片: [1 2 3 4 5]
    fmt.Println("目标切片:", dst)       // 输出:目标切片: [1 2 3 4 5]

    // 修改目标切片
    dst[0] = 10
    fmt.Println("修改后的源切片:", src) // 输出:修改后的源切片: [1 2 3 4 5]
    fmt.Println("修改后的目标切片:", dst) // 输出:修改后的目标切片: [10 2 3 4 5]
}

结果说明 ​

  1. copy(dst, src) 将 src 切片的内容逐一复制到 dst 切片中,两个切片的底层数据完全独立。
  2. 修改 dst 的元素不会影响到 src,因为 dst 中的数据是新拷贝的副本。

九、映射map ​

Go 语言中,map 是一种内置的数据结构,用于实现键值对(key-value)存储。它可以高效地通过键快速查找、插入、和删除数据。map 是无序的,键的顺序在迭代时不保证固定。

基本语法与使用 ​

  1. 声明和初始化

    使用 make 函数初始化一个 map:

    go
    m := make(map[string]int) // 键是字符串类型,值是整数类型

    或者在声明的同时初始化:

    go
    m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}
  2. 基本操作

    • 添加或更新键值对:

      go
      m["apple"] = 5 // 更新或添加键 "apple" 的值为 5
    • 访问元素:

      go
      value := m["apple"] // 获取键 "apple" 对应的值,若不存在则返回类型的零值
    • 删除键值对:

      使用 delete 函数从 map 中移除指定的键:

      go
      delete(m, "banana") // 删除键 "banana" 对应的键值对
    • 检测键是否存在:

      在访问 map 中的某个键时,可以使用双重赋值的形式来检查键是否存在:

      go
      value, ok := m["apple"]
      if ok {
          fmt.Println("键存在,值为:", value)
      } else {
          fmt.Println("键不存在")
      }

map 的底层实现原理 ​

Go 的 map 是基于哈希表实现的,通过哈希函数将键映射到特定的位置。每个位置可以存放一个或多个键值对,以应对哈希冲突。

  1. 哈希桶:

    map 将键值对分组存储在多个桶(bucket)中,每个桶中可以容纳若干个键值对。

  2. 哈希冲突:

    由于不同的键可能会被映射到相同的位置,Go 使用链表或开放寻址来解决冲突。在某些情况下,Go 会将冲突较多的桶再分成小桶进行分散存储。

  3. 动态扩容:

    当 map 中的键值对增加较多时,哈希表会自动扩容并重新分布桶,以保证查询和插入的性能。

特性与限制 ​

  1. 无序性:

    map 中的元素是无序的,迭代时每次的顺序可能会不同,Go 不保证 map 中的键的顺序。

  2. 并发安全性:

    map 不是并发安全的,在多线程环境下,多个 goroutine 同时读写 map 会导致竞态条件,通常需要使用 sync.RWMutex 进行加锁操作来保证安全。

  3. 键的类型:

    map 的键可以是任何可比较的类型,比如 int、string、struct(无切片、映射字段),但不能使用切片、映射、函数等不可比较的类型作为键。

示例代码 ​

以下代码展示了 map 的基本使用:

go
package main

import "fmt"

func main() {
    // 初始化一个 map
    m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}

    // 添加或更新元素
    m["date"] = 4
    m["apple"] = 5

    // 检查某个键是否存在
    value, ok := m["banana"]
    if ok {
        fmt.Println("banana 存在,值为:", value)
    } else {
        fmt.Println("banana 不存在")
    }

    // 删除元素
    delete(m, "cherry")

    // 遍历 map
    for key, value := range m {
        fmt.Printf("%s -> %d\n", key, value)
    }
}

map 的零值和空 map ​

  • 一个未初始化的 map 的零值是 nil,操作一个 nil map 不会引发错误,但插入操作无效。

    go
    var m map[string]int
    fmt.Println(m == nil) // 输出: true

map 的使用场景 ​

map 非常适合需要快速查找的数据结构,如:

  • 统计字符或单词的出现频率
  • 记录对象的映射关系(如用户 ID 到用户信息)
  • 缓存查询结果以提高效率