【go语言】map 和 list

一、map

map 是一种无序的键值对的集合

  • 无序 :map[key]
  • 键值对:key - value

map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。map 是一种集合,所以我们可以像迭代数组和切片那样迭代他。不过,map 是无序的,我们无法决定他的返回顺序。最后 map 也是引用类型。

1.1 map 的定义

package main

import "fmt"

// map 集合,保存数据的一种结构
func main() {
   // 创建一个map,也是一个变量,数据类型是 map
   // map[key]value
   var map1 map[int]string // 只是声明了但是没有初始化,不能使用 nil
   if map1 == nil {
      fmt.Println("map1==nil")
   }
   // 更多的时候使用的是make方法创建
   var map2 = make(map[string]string) // 创建了map
   fmt.Println(map1)
   fmt.Println(map2)
   // 在创建的时候,添加一些基础数据
   // map[string]int nil
   // map[string]int {key:value,key:value,......}
   var map3 = map[string]int{"Go": 100, "Java": 10, "C": 60}
   fmt.Println(map3) 
   // 关于map的类型,就如定义的一般 map[string]int
   // 类型主要是传参要确定
   fmt.Printf("%T\n", map3)
}

1.2 map 的使用

  • 创建并初始化 map
  • map[key] = value,将 value 赋值给对应的 map 的 key
  • 判断 key 是否存在,value, ok = map[key]
  • 删除 map 中的元素,delete(map, key)
  • 新增 map[key] = value
  • 修改 map[key] = value,如果存在这个 key 就是修改
  • 查看 map 的大小,len(map)

1.3 map 的初始化和赋值

在 Go 语言中,map 是一种无序的集合类型,用来存储键值对(key-value pairs)。初始化和赋值操作可以通过不同的方式完成。

1.3.1 初始化 map

1.3.1.1 使用 make 初始化 map

使用 make 函数是最常见的初始化 map 的方式。make 会创建一个指定类型的空 map

Go 复制代码
m := make(map[string]int)

上面的代码会创建一个空的 map,键是 string 类型,值是 int 类型。

1.3.1.2 使用字面量初始化 map

你还可以使用字面量方式在声明时初始化 map,并为 map 指定初始值。

Go 复制代码
m := map[string]int{
    "a": 1,
    "b": 2,
    "c": 3,
}

这会创建一个包含初始键值对的 map,其中键是 string 类型,值是 int 类型。

1.3 使用零值初始化 map

如果你声明了一个 map,但没有进行初始化(即没有使用 make 或字面量),它会有一个零值,零值是 nil,表示该 map 尚未被初始化。此时,不能直接向 map 中添加元素,必须先进行初始化。

Go 复制代码
var m map[string]int
fmt.Println(m == nil)  // true

此时如果尝试对 nilmap 进行赋值,会引发运行时错误。

Go 复制代码
m["a"] = 1  // 运行时错误:assignment to entry in nil map

1.3.2 赋值给 map

可以通过 map[key] = value 的方式来为 map 添加或修改元素。

1.3.2.1 添加元素
Go 复制代码
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
m["c"] = 3
fmt.Println(m)  // map[a:1 b:2 c:3]
1.3.2.2 修改元素

如果 map 中已经存在某个键,直接赋值会修改对应键的值。

Go 复制代码
m["b"] = 10
fmt.Println(m)  // map[a:1 b:10 c:3]
1.3.2.3 删除元素

使用 delete 函数可以从 map 中删除指定的键值对。

Go 复制代码
delete(m, "c")
fmt.Println(m)  // map[a:1 b:10]
1.3.2.4 判断 map 中的键是否存在

可以通过多重赋值来判断一个键是否存在于 map 中。

Go 复制代码
value, ok := m["a"]
if ok {
    fmt.Println("Key exists, value:", value)
} else {
    fmt.Println("Key does not exist")
}
  • valuemap 中键对应的值。
  • ok 是一个布尔值,如果键存在,oktrue,否则为 false

1.4 map 的遍历

package main

import "fmt"

/*
     遍历map
      - key、value 无序的,遍历map,可能每次的结果排序都不一致。
       - "aaa" "aaaaa"
       - "bbb" "bbbbb"
       - "ccc" "ccccc"

1、map是无序的,每次打印出来的map可能都不一样,它不能通过index获取,只能通过key来获取
2、map的长度是不固定的,是引用类型的
3、len可以用于map查看map中数据的数量,但是cap无法使用
4、map的key 可以是 布尔类型,整数,浮点数,字符串
*/
func main() {
   var map1 = map[string]int{"Go": 100, "Java": 99, "C": 80, "Python": 60}
   // 循环遍历,只能通过for range
   // 返回 key 和 value
   for k, v := range map1 {
      fmt.Println(k, v)
   }
}

1.5 map 结合 slice 使用

在遍历的时候,会发现 map 是无序的,所以结合 slice 使用。

package main

import "fmt"

// map 结合 slice 来使用

/*
需求:
1、创建map来存储人的信息,name,age,sex,addr
2、每个map保存一个的信息
3、将这些map存入到切片中
4、打印这些数据
*/
func main() {
   
   user1 := make(map[string]string)
   user1["name"] = "kuangshen"
   user1["age"] = "27"
   user1["sex"] = "男"
   user1["addr"] = "重庆"

   user2 := make(map[string]string)
   user2["name"] = "feige"
   user2["age"] = "30"
   user2["sex"] = "男"
   user2["addr"] = "长沙"

   user3 := map[string]string{"name": "小蓝", "age": "18", "sex": "男", "addr": "火星"}
   fmt.Println(user3)

   // 3个数据有了,存放到切片中,供我们使用
   userDatas := make([]map[string]string, 0, 3)
   userDatas = append(userDatas, user1)
   userDatas = append(userDatas, user2)
   userDatas = append(userDatas, user3)

   fmt.Println(userDatas)

   // 0 map[string]string
   for _, user := range userDatas {
      //fmt.Println(i)
      fmt.Println(user["addr"])
   }

}

1.6 map 不是线程安全的

map 在 go 中不是线程安全的。也就是说,如果多个 goroutine 同时读写一个 map,就可能导致数据竞争、程序崩溃或者不一致的结果。

1.6.1 如何避免并发访问 map 时的安全问题

1. 使用 sync.Mutexsync.RWMutex 来加锁

sync.Mutex 可以确保在任意时刻只有一个 goroutine 可以访问 map,通过加锁和解锁的方式来保证互斥。

Go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m = make(map[string]int)
	var mu sync.Mutex // 创建一个互斥锁

	// 写操作:在对 map 进行写操作之前加锁
	mu.Lock()
	m["a"] = 1
	m["b"] = 2
	mu.Unlock() // 写完后解锁

	// 读操作:在对 map 进行读操作之前加锁
	mu.Lock()
	fmt.Println(m["a"])
	fmt.Println(m["b"])
	mu.Unlock()

	// 其他 goroutine 可以继续执行
}

这里,mu.Lock()mu.Unlock() 保证了在 map 上进行读写时,同一时间只有一个 goroutine 可以操作 map

2. 使用 sync.RWMutex 来优化读写锁

sync.RWMutex 允许多个 goroutine 同时进行读取操作,但在进行写操作时,锁会被独占,其他所有读写操作都会被阻塞。

Go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m = make(map[string]int)
	var mu sync.RWMutex // 创建一个读写锁

	// 写操作
	mu.Lock() // 加写锁
	m["a"] = 1
	m["b"] = 2
	mu.Unlock() // 解锁

	// 读操作
	mu.RLock() // 加读锁
	fmt.Println(m["a"])
	fmt.Println(m["b"])
	mu.RUnlock() // 解锁
}

在这个例子中,mu.Lock() 用于写操作时,其他的读或写操作会被阻塞。而 mu.RLock() 允许多个 goroutine 同时读取 map

3. 使用 sync.Map

Go 1.9 引入了 sync.Map,它是一个专门为并发设计的线程安全 map,在内部使用了高效的同步机制。相比普通的 mapsync.Map 在高并发场景下的性能会更好,因为它针对并发操作进行了优化。

Go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 写操作
	m.Store("a", 1)
	m.Store("b", 2)

	// 读操作
	if value, ok := m.Load("a"); ok {
		fmt.Println("Value of a:", value)
	}

	// 删除元素
	m.Delete("b")

	// 遍历 map
	m.Range(func(key, value interface{}) bool {
		fmt.Println(key, value)
		return true
	})
}

sync.Map 提供了几个重要方法:

  • Store(key, value):存储元素。
  • Load(key):加载元素,返回值和存在标志。
  • Delete(key):删除元素。
  • Range(func):遍历 mapRange 会按并发安全的方式逐一处理每个元素。

sync.Map 的主要优点是在读多写少的场景下,提供了更高的性能和更简便的并发安全保证。

二、list

2.1 list 和 slice 的区别

在 Go 语言中,listslice 是常见的两种数据结构,但它们在实现和用法上有一些重要的区别。这里的"list"通常指的是链表结构(例如 container/list 包中的 List 类型),而 slice 是 Go 中的一种动态数组类型。让我们详细看看它们的区别:

2.1.1 定义和实现方式

Slice(切片)

  • slice 是一个动态数组,它是对底层数组的一个视图。切片的大小可以动态变化,但它始终指向底层的数组。
  • 切片由三部分组成:
    • 指针:指向底层数组的某个位置。
    • 长度:切片中元素的个数。
    • 容量:从切片的起始位置到底层数组的末尾的元素个数。
Go 复制代码
arr := []int{1, 2, 3}
slice := arr[1:3] // slice 包含 arr 中索引 1 到 2 的元素

List(链表)

  • list 是 Go 标准库中 container/list 包提供的双向链表实现。它与数组和切片不同,数据存储在由节点组成的链表中。每个节点包含数据和指向前一个和下一个节点的指针。
  • list 并没有像切片那样的固定容量限制,可以非常灵活地在链表的任意位置进行插入和删除。
Go 复制代码
import "container/list"

l := list.New()
l.PushBack(1) // 将元素插入链表的尾部

2.1.2 存储方式

  • Slice

    • 切片使用的是底层数组,因此它们的存储方式是连续的内存块。由于其基于数组,因此支持通过索引快速访问元素。
    • 切片的内存布局紧凑,性能较好,尤其是对于较大的数据集合,但可能存在内存分配的开销。
  • List

    • 链表是由节点组成的,每个节点包含数据和指向前后节点的指针。因此,它不需要连续的内存块来存储数据。
    • 链表的节点分散存储,内存分配和管理可能较为复杂,并且由于需要存储指针,内存开销较大。

2.1.3 性能差异

  • Slice

    • 切片对随机访问支持非常好,可以通过索引快速访问某个位置的元素,时间复杂度为 O(1)。
    • 切片的添加和删除操作可能会涉及到底层数组的扩容或缩小,尤其是当切片容量不足时,可能会发生重新分配内存,这可能导致性能下降。
    • 适合需要频繁读取和遍历的数据。
  • List

    • 链表的访问操作比切片慢,因为你需要从头节点或尾节点开始遍历链表。获取某个特定位置的元素的时间复杂度是 O(n)。
    • 在链表中插入和删除元素(尤其是在链表的中间)非常高效,时间复杂度为 O(1),因为你只需要改变节点的指针。
    • 适合频繁进行插入和删除操作的数据。

2.2 list 的基本用法

在 Go 语言中,list 通常指的是 container/list 包中的双向链表。container/list 提供了一个双向链表的数据结构,可以进行高效的插入和删除操作。它不支持随机访问,但适合用于频繁修改的场景,比如需要频繁在头部、尾部或中间插入和删除元素时。

2.2.1 创建一个新的链表

可以使用 list.New() 创建一个新的空链表。

Go 复制代码
l := list.New()

2.2.2 插入元素

链表支持多种插入操作:

  • PushBack:向链表的尾部添加元素。
  • PushFront:向链表的头部添加元素。
  • InsertBefore:向指定节点之前插入元素。
  • InsertAfter:向指定节点之后插入元素。
Go 复制代码
// 向链表尾部添加元素
l.PushBack(1)
l.PushBack(2)

// 向链表头部添加元素
l.PushFront(0)

2.2.3 遍历链表

通过 Front()Back() 方法可以获取链表的头部和尾部元素。遍历链表时,使用 Next()Prev() 方法访问下一个或上一个节点。

Go 复制代码
// 从链表头部开始遍历
for e := l.Front(); e != nil; e = e.Next() {
    fmt.Println(e.Value)  // 输出节点的值
}

2.2.4 删除元素

链表提供了几种删除操作:

  • Remove:从链表中删除一个指定的元素。
  • MoveToFront:将一个元素移动到链表的头部。
  • MoveToBack:将一个元素移动到链表的尾部。
Go 复制代码
// 删除链表中的第一个元素
l.Remove(l.Front())

// 删除链表中的最后一个元素
l.Remove(l.Back())
相关推荐
stevewongbuaa1 小时前
一些烦人的go设置 goland
开发语言·后端·golang
撸码到无法自拔1 小时前
MATLAB中处理大数据的技巧与方法
大数据·开发语言·matlab
island13141 小时前
【QT】 控件 -- 显示类
开发语言·数据库·qt
sysu632 小时前
95.不同的二叉搜索树Ⅱ python
开发语言·数据结构·python·算法·leetcode·面试·深度优先
hust_joker2 小时前
go单元测试和基准测试
开发语言·golang·单元测试
wyg_0311133 小时前
C++资料
开发语言·c++
小机学AI大模型3 小时前
关于使用PHP时WordPress排错——“这意味着您在wp-config.php文件中指定的用户名和密码信息不正确”的解决办法
开发语言·php
臻一3 小时前
关于bash内建echo输出多行文本
开发语言·bash
步、步、为营3 小时前
C# 探秘:PDFiumCore 开启PDF读取魔法之旅
开发语言·pdf·c#·.net
小黄人软件4 小时前
【MFC】C++所有控件随窗口大小全自动等比例缩放源码(控件内字体、列宽等未调整) 20250124
开发语言·c++·ui·mfc