青训营X豆包MarsCode 技术训练营 - Golang 语法解析 - 3 | 豆包MarsCode AI 刷题

104 阅读9分钟

前言

本文记录训练营语法基础课部分的相关内容,对于课程中讲的不够充分的地方,结合了go入门指南中的详细介绍进行了补充。笔记同步更新在我的博客

本篇讲述了arrayslicemap

数组

概述

go在数组和切片的设计上明显收到python的影响。

以  []  符号标识的数组类型几乎在所有的编程语言中都是一个基本主力。Go 语言中的数组也是类似的,只是有一些特点。Go 没有 C 那么灵活,但是拥有切片(slice)类型。这是一种建立在 Go 语言数组类型之上的抽象,要想理解切片我们必须先理解数组。数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷

数组是具有相同  唯一类型  的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int [10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的。

数组元素可以通过  索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大内存用量 2Gb (256MB , 大约2^28int32)

遍历

可以使用 for的两种基本写法进行数组遍历。

数组变量的类型

var arr1 = new([5]int)

arr1的类型是 *[5]int , 以c++的方式理解,是个指针(引用)类型。

var arr2 [5]int

arr2的类型是 [5]int , 是一种值类型。

深浅拷贝

浅拷贝

非常好理解,因为拷贝了指向数组组的引用。

var arr1 = new([5]int)
arr1[3] = 100
var arr2 = arr1 // shallow copy
arr2[3] = 99
fmt.Println("%d %d", arr1[3], arr2[3])

深拷贝

var arr3 [5]int = [...]int{1, 2, 3, 4, 5}
arr3[3] = 100
var arr4 = arr3 // deep copy
arr4[3] = 99
fmt.Println("%d %d", arr3[3], arr4[3])

参数传递

package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }

func main() {
	var ar [3]int
	f(ar) 	// passes a copy of ar
	fp(&ar) // passes a pointer to ar
}

初始化

写法一

var arrAge = [5]int{18, 20, 15, 22, 16}

支持部分初始化,类似[10]int {1 , 2 , 3} 未初始化的位置都为零。

写法二

var arrLazy = [...]int{5, 6, 7, 8, 22}

类似于一种解包操作。

写法三

var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}

key-value语法,赋值特定的位置。

切片

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。

切片是可索引的,并且可以由 len() 函数获取长度。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度。

声明

var identifier []type (不需要说明长度)。

初始化

var slice1 []type = arr1[start:end] (左闭右开)。

通过数组创建切片

s := [3]int{1,2,3}[:] , s := []int{1,2,3} , var x = []int{2, 3, 5, 7, 11} 这些写法是等价的。

切片的增长

切片只能向后移动

slice1 = slice1[0:cap(slice1)] // 从原来的起始位置增长到原始数组的末尾
slice1 = slice1[1:] // 将slice的头部往前移动一位,尾部不变
slice1 = slice1[1:len(slice1)+1] // 将slice向后滑动一位 可能会溢出

传递参数

如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个 切片引用并传递给该函数。这里有一个计算数组元素和的方法:

func sum(a []int) int {
	s := 0
	for i := 0; i < len(a); i++ {
		s += a[i]
	}
	return s
}

func main() {
	var arr = [5]int{0, 1, 2, 3, 4}
	sum(arr[:])
}

使用make创造切片

slice1 := make([]type ,  len)

这里len是底层数组长度,也是slice的初始长度。

slice1 := make([]type , len , cap)

len是切片长度,cap是底层数组长度, 切片的首个元素将和数组的首个元素对齐。

底层数组是在堆上开辟的,不会直接暴露为变量。

make()new()

二者都是在堆上分配内存,但是它们的行为不同,也适用于不同的类型。

make()

  1. 用于创建 slice、map 和 channel 这三种引用类型的数据结构。
  2. make() 返回的是已初始化、内存已分配的数据结构。
  3. make() 接受两个参数,第一个参数是类型,第二个参数是长度、容量等初始化参数。
  4. make()会初始化数据结构,并根据需要分配内存。

例子

slice := make([]int , 5, 10)

new()

  1. 用于分配内存,返回的是指向零值的指针。
  2. new() 只有一个参数,是类型,返回一个指向该类型的零值的指针。
  3. new() 只分配内存,不进行初始化,返回的指针指向零值。

例子

ptr := new(int)

map

概述

mapgo中的哈希表。

key 是可哈希的内置对象(string、int、float),或者实现了hash()方法的自定义对象。

value可以是任意类型。

性能

map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。通过 keymap 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍。

创建

map 是 引用类型 的: 内存用 make 方法来分配。

map 的初始化:var map1 = make(map[keytype]valuetype)。 或者简写为:map1 := make(map[keytype]valuetype)

用法示例

package main

import "fmt"

func main() {
	m := make(map[string]int) // create
	m["one"] = 1              // insert
	m["two"] = 2

	fmt.Println(m)                  // formart printer
	fmt.Println(m["one"], m["two"]) // retrieve
	fmt.Println(m["unknown"])       // 0

	r, ok := m["unknown"]
	fmt.Println(r == ok) // false

	delete(m, "one")

	m2 := map[string]int{"one": 1, "two": 2}
	var m3 = map[string]int{"one": 1, "two": 2}
	fmt.Println(m2, m3)

	// traverse
	for item, idx := range m2 {
		fmt.Println(item, idx)
	}
}

输出

map[one:1 two:2]
1 2
0
false
map[one:1 two:2] map[one:1 two:2]

map 容量

和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]valuetype, cap)。例如:

map2 := make(map[string]float32, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

用切片作为 map 的值

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

处理一个健对应多个值的情况。

遍历

map支持遍历,可以和for-range配合使用,遍历的顺序是随机的。

  • 遍历桶:遍历时,Go 首先会遍历 map 中的每个桶。遍历的顺序并不是线性的,而是随机的,以避免程序依赖遍历顺序。
  • 遍历桶内元素:在每个桶内,Go 遍历存储的键值对。如果一个桶有溢出桶,则继续遍历溢出桶中的键值对。
  • 随机化遍历顺序:每次遍历时,Go 会使用一个随机的顺序。这是为了避免程序依赖遍历顺序,确保遍历顺序不会随着键的插入顺序改变而固定。

map的切片

假设我们想获取一个 map 类型的切片,我们必须使用两次 make() 函数,第一次分配切片,第二次分配 切片中每个 map 元素。

这样理解这件事情,比如需要在C++中分配一个vector<int>数组。写法为

vector<int> va[10];

数组部分的内存

std::vector<int> va[10];

这段代码创建了一个长度为 10 的数组 va,其中每个元素是一个 std::vector<int> 类型的对象。这个数组本身是在栈上分配的,因此,va 数组的内存是固定且静态的,大小是 10。

  • va 数组中每个元素(即 std::vector<int>)是一个对象,它的内存会在栈上分配,类似于普通的对象,但它是一个 包含指向动态内存的指针的类

vector<int> 内部的内存管理

std::vector<int> 是一个动态数组,它的内部机制是动态分配内存来存储元素。具体来说,std::vector<int> 存储的数据是通过 动态分配的,而不是直接存储在栈上。因此,虽然 va 数组本身在栈上分配内存,但是每个 std::vector<int> 内部的数据是动态分配的。

再回头看map的切片,也是两次构造的过程。首先需要为切片的底层数组分配内存并且创建切片,再为切片中的每个map分配内存。这里map是个引用类型,这种方式的内存分配方式和`vector是一致的。

package main
import "fmt"

func main() {
	// Version A:
	items := make([]map[int]int, 5)
	for i:= range items {
		items[i] = make(map[int]int, 1)
		items[i][1] = 2
	}
	fmt.Printf("Version A: Value of items: %v\n", items)

	// Version B: NOT GOOD!
	items2 := make([]map[int]int, 5)
	for _, item := range items2 {
		item = make(map[int]int, 1) // item is only a copy of the slice element.
		item[1] = 2 // This 'item' will be lost on the next iteration.
	}
	fmt.Printf("Version B: Value of items: %v\n", items2)

输出

Version A: Value of items: [map[1:2] map[1:2] map[1:2] map[1:2] map[1:2]]
Version B: Value of items: [map[] map[] map[] map[] map[]]

这里range 函数的特性是返回元素的拷贝,所以是创建行为B是不成功的。

map的排序

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序。

如果想为 map 排序,需要将 key 拷贝到一个切片,再对切片排序。