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.length3,表示 s 包含的元素数量 [2, 3, 4]
  • s.cap4,因为从 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]

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

切片的拷贝

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

copy 函数的用法

copy 函数的语法为:

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

示例代码

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 的键可以是任何可比较的类型,比如 intstringstruct(无切片、映射字段),但不能使用切片、映射、函数等不可比较的类型作为键。

示例代码

以下代码展示了 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 到用户信息)
  • 缓存查询结果以提高效率