青训营go语言基础总结一:数组、切片、Map、List| 豆包MarsCode AI 刷题

79 阅读11分钟

go语言中的数组

在Go语言中数组是具有固定长度的同类型元素的序列。与其他语言不同在Go语言中,数组长度是类型的一部分。例如[5]int和[4]int 不是同一种类型,相同类型的数组可以直接比较是否相等,不需要跟其他语言一样遍历去比较值。
以下是一些数组的初始化方式及其使用方法:

数组的初始化

1.使用 var 声明并初始化

    // 声明一个长度为5的int类型数组
    var arr1 [5]int
    fmt.Println("未初始化的数组:", arr1)
    // 单独设置值
    arr1[0] = 1
    arr1[1] = 2
    arr1[2] = 3
    fmt.Println("设置值后的数组:", arr1)

2. 使用数组字面量初始化

    // 使用数组字面量初始化
    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Println("初始化的数组:", arr2)

3. 自动推断数组长度

// 让编译器自动推断数组长度 arr3 := [...]int{1, 2, 3, 4, 5} 
fmt.Println("自动推断长度的数组:", arr3)

4. 部分初始化

// 只初始化部分元素,未初始化的元素将默认设置为零值 
arr4 := [5]int{1, 2} 
fmt.Println("部分初始化的数组:", arr4)

数组的使用

1. 访问和修改数组元素

arr := [5]int{1, 2, 3, 4, 5} 
// 访问数组元素 
fmt.Println("数组的第三个元素:", arr[2]) 
// 修改数组元素 
arr[2] = 10 
fmt.Println("修改后的数组:", arr)

2. 遍历数组
使用 for 循环

arr := [5]int{1, 2, 3, 4, 5}
// 使用索引遍历数组 
for i := 0; i < len(arr); i++ {
fmt.Println("元素", i, ":", arr[i]) 
}

使用 for range 循环

arr := [5]int{1, 2, 3, 4, 5}
// 使用 range 遍历数组 
for index, value := range arr { 
fmt.Println("索引", index, "的值:", value) 
}

多维数组

在Go语言中,多维数组可以使用嵌套数组的形式来实现

// 定义二维数组 
var arr2D [3][3]int = [3][3]int{ 
{1, 2, 3}, 
{4, 5, 6}, 
{7, 8, 9},
}
// 部分初始化的二维数组 
var arr2D = [3][3]int{ 
{1, 2}, // 第三个元素默认为0 
{4, 5, 6}, // 完全初始化 
{}, // 所有元素默认为0 
}

// 未初始化的二维数组 
var arr2D [3][3]int

// 定义一个2x3的二维数组 
var arr2D [2][3]int 
arr2D[0][0] = 1 
arr2D[0][1] = 2 
arr2D[0][2] = 3 
arr2D[1][0] = 4 
arr2D[1][1] = 5 
arr2D[1][2] = 6 
// 遍历二维数组 
for i := 0; i < 2; i++ { 
for j := 0; j < 3; j++ { 
fmt.Print(arr2D[i][j], " ") 
   } 
fmt.Println() 
  }

数组比较

Go语言中的数组可以直接使用 == 运算符进行比较,但要注意:只有相同类型和相同长度的数组才能进行比较。

arr1 := [3]int{1, 2, 3} 
arr2 := [3]int{1, 2, 3} 
arr3 := [3]int{4, 5, 6} f
mt.Println("arr1 == arr2:", arr1 == arr2) // true 
fmt.Println("arr1 == arr3:", arr1 == arr3) // false 
}

go语言中的切片

在Go语言中,切片(slice)是一种比数组更灵活、更强大的数据类型,切片底层是一个结构体,结构体包含一个指向数组的指针,提供了更灵活且强大的接口,具有可变长度的精度。

切片的定义方式

1.切片的声明
声明一个切片不指定长度,此时只可以用append追加,不可以直接赋值 例如:s[1] = 10
var s []int
2.使用内置函数 make 构造切片

s := make([]int, 5) // 创建一个长度为5的切片,初始值为零值 
s := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片

3.通过字面量初始化切片 s := []int{1, 2, 3, 4, 5} 4.基于数组创建切片 在基于数组创建切片的时候,本质上切片和数组还是共用一块内存空间,如果对数组或者切片修改,另一方也会修改,除非切片追加发生扩容,扩容之后就不再共用一块内存空间。

arr := [5]int{1, 2, 3, 4, 5} 
s := arr[1:4] // s的内容是[2 3 4]

切片的使用

1.访问和修改切片元素

s := []int{1, 2, 3}
fmt.Println(s[0])  // 访问第一个元素,输出: 1
s[1] = 10          // 修改第二个元素
fmt.Println(s)     // 输出: [1 10 3]

2.追加元素

s := []int{1, 2, 3}
s = append(s, 4, 5)  // 追加元素4和5
fmt.Println(s)       // 输出: [1 2 3 4 5]

3.切片操作
这里新老切片会共用一块内存,除非有一方扩容了。

s := []int{1, 2, 3, 4, 5}
s1 := s[1:3]    // 创建新切片,包含s的第1到第2个元素(不包括第3个元素)
fmt.Println(s1) // 输出: [2 3]

4.复制切片 这里的复制是真的底层复制,两个切片是分开指向不同的内存。

src := []int{1, 2, 3}
dest := make([]int, len(src))
copy(dest, src)       // 将src复制到dest
fmt.Println(dest)     // 输出: [1 2 3]

5.切片的删除 由于 Go 中的切片并没有提供直接的删除方法,所以我们通常通过切片的重新分片操作来实现元素的删除。

1.删除指定索引的元素

1.使用append函数 使用切片的重新分片和 append 函数来实现删除。

s = append(s[:index], s[index+1:]...)

... 是表示变参(variadic parameter),即把 s[index+1:] 切片中的所有元素都作为单独的参数传递给 append 函数。
2.使用copy函数

s := []int{1, 2, 3, 4, 5}
index := 2
copy(s[index:], s[index+1:])
s = s[:len(s)-1]
fmt.Println(s) // 输出: [1 2 4 5]

这种方法适合在性能敏感的场景,避免了 append 的开销

2.删除切片中的多个元素

1.删除连续的函数 假设我们要删除从索引 start 到 end (不包括 end) 的元素:

s := []int{1, 2, 3, 4, 5, 6, 7}
start, end := 2, 5
s = append(s[:start], s[end:]...)
fmt.Println(s) // 输出: [1 2 6 7]

2.删除不连续的元素 如果需要删除的元素不连续,可以遍历切片并生成一个新的切片:

s := []int{1, 2, 3, 4, 5, 6, 7}
toDelete := map[int]bool{1: true, 3: true, 5: true}

result := s[:0]  // 保持相同的底层数组
for i, v := range s {
    if !toDelete[i] {
        result = append(result, v)
    }
}

s = result
fmt.Println(s) // 输出: [1 3 5 7]

切片的重要特性和需要注意的点

1.切片的长度和容量

  • 切片具有长度(len)和容量(cap)。长度表示切片中的元素个数,容量表示从切片起始位置到底层数组末尾的元素个数。
s := []int{1, 2, 3, 4, 5}
fmt.Println(len(s)) // 输出: 5
fmt.Println(cap(s)) // 输出: 5

2.切片共享底层数组

  • 切片是对底层数组的引用,多个切片可以共享相同的底层数组。
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4]
s2 := arr[2:5]
s1[1] = 10          // 修改s1也会影响s2,因为它们共享底层数组
fmt.Println(s1)     // 输出: [2 10 4]
fmt.Println(s2)     // 输出: [10 4 5]

3.切片在追加元素时可能引发重新分配

  • 当使用append函数向切片追加元素时,如果切片的容量不足,会导致底层数组重新分配,并复制原切片中的元素到新的数组中。
s := []int{1, 2, 3}
s = append(s, 4, 5, 6, 7, 8, 9)
fmt.Println(s)  // 输出: [1 2 3 4 5 6 7 8 9]

4.避免切片的内存泄漏

  • 因为切片持有对底层数组的引用,如果不小心引用了数组的大部分但只用了其中的一部分,可能会导致内存泄漏问题。
  • 切片删除操作不会自动回收底层数组的空间,如果需要释放多余的内存,可以考虑将数据复制到新的切片中。
arr := make([]int, 10000)
largeSlice := arr[:1000]
smallSlice := arr[:3]   // smallSlice 会引用整个底层数组,这样会影响垃圾回收
// 解决办法是复制需要的部分到新的切片中
smallSliceCopy := make([]int, len(smallSlice))
copy(smallSliceCopy, smallSlice)  

go语言中的map

在 Go 语言中,map 是一种内建的数据类型,用于存储键值对。

定义和初始化方式

1.使用 make 函数初始化

m := make(map[string]int) // 创建一个键为 string 类型,值为 int 类型的 map

2.直接初始化

m := map[string]int{
    "one": 1,
    "two": 2,
}
m1:=map[string]int{}//初始化一个空的

3.声明而不初始化
需要注意的是,这种方式声明的 map 在使用之前必须先初始化,否则会发生panic错误。

var m map[string]int

基本使用方法

1.插入元素

m := make(map[string]int)
m["one"] = 1
m["two"] = 2

2.读取元素

val := m["one"]

3.检查键是否存在

val, ok := m["one"]
if ok {
    fmt.Println("Key exists:", val)
} else {
    fmt.Println("Key does not exist")
}

4.删除元素

delete(m, "one")

5.遍历 map

for key, value := range m {
    fmt.Println(key, value)
}

注意事项

1.零值 声明但未初始化的 map 的零值是 nil。对未初始化的 map 进行读、写操作会导致运行时错误(panic)。

m["one"] = 1 // 运行时错误

2.并发读写 Go 中的 map 不是线程安全的。因此,如果在多个 goroutine 中读写同一个 map,必须使用同步机制(如 sync.Mutex)保护它。

var mu sync.Mutex
m := make(map[string]int)

go func() {
    mu.Lock()
    m["one"] = 1
    mu.Unlock()
}()

go func() {
    mu.Lock()
    val := m["one"]
    mu.Unlock()
    fmt.Println(val)
}()

3.存储值类型 map 的值可以是任意类型,包括另一个 map、切片等复杂数据类型,但是要注意值类型的零值和默认初始化问题
4.不能作为 map 键的类型 以下类型不能作为 map 的键:

  1. 切片(slice)
  2. 映射(map)
  3. 函数(func)

这些类型都是引用类型,它们在 Go 中无法使用等号(==)进行比较,因此不能用作 map 的键。

go语言中的List

在 Go 语言中,list 数据结构没有像数组和切片那样的内建类型。但 Go 标准库中提供了 container/list 包,可以用来实现双向链表。

1.定义和初始化方式

1.作import声明

要使用 list,首先要导入 container/list 包:

import (
    "container/list"
)

2.创建一个新列表 使用 list.New() 函数或直接声明并初始化 list.List 结构体:

  var l list.List

2.基本使用方法

1.插入元素 在列表末尾插入元素:

l.PushBack("element")

在列表头部插入元素:

l.PushFront("element")

在指定元素后插入:

e := l.PushBack("first")
l.InsertAfter("second", e)

在指定元素后插入:

e := l.PushBack("first")
l.InsertAfter("second", e)

在指定元素前插入:

e := l.PushBack("first")
l.InsertBefore("second", e)

2.读取元素 遍历列表中的所有元素:

for e := l.Front(); e != nil; e = e.Next() {
    fmt.Println(e.Value)
}

3.删除元素 删除列表中的指定元素需要持有元素的指针 *list.Element

e := l.PushBack("element")
l.Remove(e)

4.访问头和尾元素 获取列表的头元素:

head := l.Front()
fmt.Println(head.Value)

获取列表的尾元素:

tail := l.Back()
fmt.Println(tail.Value)

注意事项

1.安全性
在遍历或修改列表时,需要注意并发安全性。如果一个列表会被多个 goroutine 并发访问,需要使用同步机制(如 sync.Mutex)保护它。
2.列表与切片选择
Go 语言内建的切片通常更高效,且更容易使用。如果不特别需要链表的特性(如在列表的两端频繁插入或删除),优先使用切片。
3.类型安全
container/list 是一个非类型安全的通用容器,因此 list.Element 的 Value 字段是 interface{} 类型。使用时需做好类型断言和检查,避免程序错误。

总结

数组(Array)

  • 定义:固定长度的同类型元素集合。
  • 初始化:长度在定义时就确定并且不能改变。
  • 优点:内存连续分配,访问速度快。
  • 缺点:长度固定,使用不够灵活。
  • 适用场景:元素数量固定且不需要改变长度的场景。

切片(Slice)

  • 定义:可变长度的数组视图,底层是数组。
  • 初始化:可以基于数组、字面量或使用 make 函数创建。
  • 优点:灵活、动态调整长度,支持切片操作。
  • 缺点:底层数组在扩容时可能会分配新的存储空间,引起性能开销。
  • 适用场景:需要动态调整大小的集合集合操作。

Map

  • 定义:键值对存储的哈希表。
  • 初始化:使用 make 函数或字面量创建。
  • 优点:快速查找、插入和删除操作。
  • 缺点:键必须是可比较类型,并发读写需要加锁。
  • 适用场景:需要通过键快速查找和存储值的场景。

List(链表)

  • 定义:双向链表(在 container/list 包中实现)。
  • 初始化:使用 list.New() 函数创建。
  • 优点:在已知位置插入和删除元素效率高。
  • 缺点:顺序访问性能较低,元素类型非类型安全(存储为 interface{})。
  • 适用场景:需要在头尾频繁插入和删除元素的场景。

切片 vs Map

  • 访问方式:切片通过索引访问,保持顺序;Map 通过键访问,无序。
  • 适用场景:切片适用于顺序数据;Map 适用于键值对存储。

提醒:

切片是值类型,不是引用类型,切片的底层是一个结构体,结构体包含一个指向数组的指针。 go语言只有值传递,没有引用传递。 当我们去获取一个不在map中的key时,得到的是value的零值。