有基础转Go语言学习笔记(2. 基本数据结构篇)
有基础转Go语言学习笔记(2. 基本数据结构篇)
1. 数组和切片(Array & Slice)
在Go语言中,数组(Array)和切片(Slice)是基础且常用的数据结构,它们有相似之处,但也有关键的区别。
数组(Array)
数组是具有固定大小的数据结构,用于存储同类型的元素集合。数组的长度在声明时确定,并且不能改变。
var arr [5]int // 声明一个包含5个整数的数组
arr[0] = 1 // 数组的索引从0开始
特点
- 固定长度:数组的长度是其类型的一部分,一旦定义就不能改变。
- 值类型:在Go中,数组是值类型,这意味着当它们被赋值给一个新变量或作为参数传递时,将进行整个数组的复制。
- 内存分配:数组在编译时分配内存,大小固定。
切片(Slice)
切片是对数组的抽象,提供了更加灵活、强大和方便的接口来处理数据序列。切片本身不存储任何数据,它只是对底层数组的引用。
slice := []int{1, 2, 3, 4, 5} // 创建一个切片
newSlice := slice[1:3] // 创建一个新的切片,包含元素slice[1], slice[2]
特点
- 动态大小:切片的长度是可变的,可以随时添加或删除元素(在其容量范围内或通过重新分配来扩展容量)。
- 引用类型:切片是引用类型,当它们被赋值给一个新变量时,两个变量将引用相同的底层数组。
- 自动管理容量:切片的容量可以根据需要自动增长。
append
函数用于添加元素到切片中,如有必要会自动扩容。
遍历
在Go语言中,遍历数组和切片主要有以下几种方法:
1. 使用传统的 for
循环
使用传统的 for
循环,通过索引来访问每个元素。
slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
这种方法在需要索引时非常有用,例如,当你需要修改原始数组或切片时。
3. 使用切片操作
在某些情况下,你可能想要遍历切片的一部分。这时,可以结合切片操作和 for
循环。
for _, value := range slice[1:4] {
fmt.Println(value)
}
这段代码将遍历切片中的第二个到第四个元素。
2. 使用 range
关键字
range
提供了一种更简洁的方式来遍历数组和切片。
for index, value := range slice {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
如果你只需要值,可以忽略索引:
for _, value := range slice {
fmt.Println(value)
}
同样,如果只需要索引:
for index := range slice {
fmt.Println(index)
}
使用 range
通常是更为“惯用”的Go代码风格。
4. 使用递归
虽然不常见,但在某些特定场景下,你可能会通过递归的方式遍历数组或切片。
func recursivePrint(slice []int, index int) {
if index < len(slice) {
fmt.Println(slice[index])
recursivePrint(slice, index+1)
}
}
recursivePrint(slice, 0)
这种方法在处理树形结构或需要特殊遍历逻辑时可能会有用。
传参
在Go语言中,将数组作为参数传递给函数时,需要注意几个关键点:
1. 数组是值类型
数组在Go中是值类型,这意味着当数组作为参数传递给函数时,实际上传递的是数组的副本,而不是它的引用。
func modifyArray(arr [5]int) {
arr[0] = 10 // 这只会修改数组的副本
}
func main() {
array := [5]int{1, 2, 3, 4, 5}
modifyArray(array)
fmt.Println(array) // 输出: [1 2 3 4 5],原始数组未被修改
}
由于数组是值传递,所以任何在函数内部对数组的修改都不会影响原始数组。
2. 使用指针传递数组
为了在函数内部修改数组本身,你可以传递一个指向数组的指针。
func modifyArray(arr *[5]int) {
arr[0] = 10 // 现在这会修改原始数组
}
func main() {
array := [5]int{1, 2, 3, 4, 5}
modifyArray(&array)
fmt.Println(array) // 输出: [10 2 3 4 5]
}
这种方式允许你在不复制整个数组的情况下修改数组。
3. 使用切片代替数组
在Go中,切片比数组更常用,因为它们更加灵活且易于使用。当你传递切片到函数时,实际上传递的是切片的引用,这意味着在函数内部对切片的修改会影响原始切片。
func modifySlice(s []int) {
s[0] = 10
}
func main() {
slice := []int{1, 2, 3, 4, 5}
modifySlice(slice)
fmt.Println(slice) // 输出: [10 2 3 4 5]
}
2. 切片(Slice)
在Go语言中,切片(Slice)是一个灵活、功能强大的数据结构,用于表示可变长的序列。切片是对数组的封装,提供了更高层次的抽象。以下是切片的多种声明方式和使用方法:
声明切片
-
使用
make
函数:slice := make([]int, 5) // 创建一个长度和容量都是5的切片
-
直接声明并初始化:
slice := []int{1, 2, 3, 4, 5} // 创建并初始化切片
-
声明一个nil切片:
var slice []int // 声明一个nil切片
-
使用现有数组或切片创建新切片:
array := [5]int{1, 2, 3, 4, 5} slice := array[1:4] // 基于数组创建切片,包含元素2、3、4
使用切片
-
添加元素:
使用append
函数向切片添加元素。如果切片的容量不够,append
会自动扩容。slice = append(slice, 6) // 添加元素到切片
-
遍历切片:
使用for
循环和range
关键字遍历切片。for index, value := range slice { fmt.Println(index, value) }
-
修改切片元素:
直接通过索引修改。slice[0] = 10 // 修改第一个元素
-
切片截取:
切片可以被进一步切割。subSlice := slice[1:3] // 创建一个新的切片,包含原切片的第2个和第3个元素
-
切片的容量和长度:
使用len
获取切片的长度,cap
获取切片的容量。length := len(slice) capacity := cap(slice)
len 与 cap
切片(Slice)是一个非常灵活的数据结构,它提供了对底层数组的引用。理解切片的长度(len
)和容量(cap
)对于正确地使用切片至关重要。
切片的长度(Length)
- 定义:切片的长度是切片中当前元素的数量。
- 获取方式:使用内置的
len
函数获取,如len(slice)
。 - 特点:
- 长度反映了切片中元素的实际数量。
- 长度永远不会超过容量。
- 可以通过
append
函数增加切片的长度(在容量允许的范围内)。 - 切片长度的修改不会影响底层数组的大小。
切片的容量(Capacity)
- 定义:切片的容量是从切片的第一个元素开始到其底层数组末尾的元素数量。
- 获取方式:使用内置的
cap
函数获取,如cap(slice)
。 - 特点:
- 容量表示切片可以增长到的最大元素数量,不需要重新分配内存。
- 当使用
append
函数增加切片长度超过其容量时,系统会分配一个新的底层数组,并复制原有元素到新数组中(通常是原容量的两倍),从而增加切片的容量。 - 切片的容量在创建时确定,之后可以通过重新分配底层数组来改变。
- 切片操作可以创建新的切片,新切片的容量取决于原切片的容量和切片操作的起始位置。
示例
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:3] // 创建一个新切片,包括数组的第2个和第3个元素
fmt.Println(len(slice)) // 输出: 2
fmt.Println(cap(slice)) // 输出: 4
在上述示例中,切片 slice
的长度为2(包含两个元素),而其容量为4,因为它从底层数组的第二个元素开始,直到数组的末尾。
func main() {
slice := make([]int, 2, 3) // 初始长度为2,容量为3
fmt.Println("Initial Slice:", slice, "Length:", len(slice), "Capacity:", cap(slice))
// 向切片中添加元素,直到超出其容量
for i := 0; i < 5; i++ {
slice = append(slice, i)
fmt.Println("Updated Slice:", slice, "Length:", len(slice), "Capacity:", cap(slice))
}
}
在这个示例中,我们首先创建了一个长度为2,容量为3的切片。然后,我们通过循环向切片中添加元素,并在每次追加后打印切片的当前状态,包括其长度和容量。
Initial Slice: [0 0] Length: 2 Capacity: 3
Updated Slice: [0 0 0] Length: 3 Capacity: 3
Updated Slice: [0 0 0 1] Length: 4 Capacity: 6
Updated Slice: [0 0 0 1 2] Length: 5 Capacity: 6
Updated Slice: [0 0 0 1 2 3] Length: 6 Capacity: 6
Updated Slice: [0 0 0 1 2 3 4] Length: 7 Capacity: 12
- 初始状态:切片的长度是2,容量是3。
- 当切片长度达到容量(即
len
等于cap
)时,再向切片添加新元素将导致切片的容量增加(通常翻倍),以便容纳更多的元素。
切片截取
Go语言中的切片截取和Python中的差不多。这样可以从现有切片或数组中生成新的切片。切片截取的基本语法是 slice[low:high]
,其中 low
是起始索引(包含),high
是终止索引(不包含)。
在Go语言中,切片的截取是通过指定起始索引和终止索引来完成的,这样可以从现有切片或数组中生成新的切片。切片截取的基本语法是 slice[low:high]
,其中 low
是起始索引(包含),high
是终止索引(不包含)。
基本示例
originalSlice := []int{1, 2, 3, 4, 5}
// 截取索引1(包含)到索引3(不包含)
subSlice := originalSlice[1:3] // 结果是[2, 3]
示例:省略边界
// 从开始到索引3(不包含)
subSlice1 := originalSlice[:3] // 结果是[1, 2, 3]
// 从索引2(包含)到结束
subSlice2 := originalSlice[2:] // 结果是[3, 4, 5]
// 完整的切片
subSlice3 := originalSlice[:] // 结果是[1, 2, 3, 4, 5]
注意事项
- 切片截取是基于原切片的,它们共享相同的底层数组。因此,修改新切片中的元素也会影响原切片。
- 超出原切片边界的截取将导致运行时错误。
- 切片截取不会复制数据,它仅仅创建了一个新的切片头,指向原来的底层数组。
深拷贝
深拷贝(deep copy)指的是创建一个新的对象,其内容是对原对象的完整复制。与浅拷贝(shallow copy)不同,深拷贝确保原始数据和复制数据不会共享任何子对象。在Go中,对于基本数据类型,赋值操作本身就是深拷贝。但对于复合数据类型(如结构体、切片、映射等),需要特别处理以实现深拷贝。
对于切片和数组,可以使用 copy
函数实现深拷贝:
originalSlice := []int{1, 2, 3}
copiedSlice := make([]int, len(originalSlice))
copy(copiedSlice, originalSlice)
深拷贝 + 截取
使用 copy
函数可以实现切片的深拷贝,并且可以结合切片截取来只复制切片的一部分。这种方法可以创建一个新的切片,它包含原始切片指定范围内的元素的副本,与原切片拥有独立的底层数组。
示例:深拷贝切片的一部分
假设我们有一个切片,我们想要复制这个切片的一部分到一个新的切片中:
originalSlice := []int{1, 2, 3, 4, 5}
// 定义要复制的切片范围
start := 1 // 起始索引(包含)
end := 4 // 结束索引(不包含)
// 创建一个新的切片用于存放复制的元素
// 长度是要复制元素的数量
newSlice := make([]int, end-start)
// 使用copy进行深拷贝
copy(newSlice, originalSlice[start:end])
// 输出结果
fmt.Println("Original Slice:", originalSlice)
fmt.Println("Copied Slice:", newSlice)
在这个示例中,我们创建了一个长度为 end-start
的新切片 newSlice
,然后使用 copy
函数将 originalSlice
中从索引 start
到 end
的元素复制到 newSlice
中。
注意事项
- 确保新切片的长度足以容纳要复制的元素。如果新切片太小,
copy
会只复制适合新切片大小的部分元素。 copy
函数返回复制的元素数量,可以用于检查是否所有需要的元素都被复制了。- 如果
start
或end
索引超出原切片的边界,程序将抛出运行时错误。
3. 映射(Map)
映射(Map)是一种内置的数据结构,提供了基于键值对的存储机制。映射在Go中非常有用,用于存储和检索数据。
定义和初始化
-
定义一个Map:
var myMap map[string]int
这里定义了一个键为
string
类型,值为int
类型的映射。 -
初始化:
使用make
函数初始化映射:myMap = make(map[string]int)
-
初始化带有值的Map:
myMap := map[string]int{"one": 1, "two": 2}
增加和修改元素
- 向映射中添加或修改元素:
myMap["three"] = 3
访问元素
-
访问映射中的元素:
value := myMap["three"]
-
访问不存在的键时,将返回值类型的零值。例如,对于
int
类型,零值是0
。
检查元素是否存在
- 使用两个值来接收返回结果,其中第二个值是一个布尔值,表示键是否存在于映射中:
value, ok := myMap["three"]
删除元素
- 使用内置的
delete
函数删除元素:delete(myMap, "three")
遍历Map
- 使用
for
循环和range
关键字遍历映射:for key, value := range myMap { fmt.Println("Key:", key, "Value:", value) }
注意事项
- 映射在Go中是引用类型,意味着当它们被赋值给新的变量或作为参数传递时,新变量和原始映射将引用同一个底层数据。
- 映射不是并发安全的。在多线程环境下使用映射时,需要使用同步机制,如互斥锁。
- 映射的顺序是不确定的,每次遍历映射的顺序可能不同。
4. 结构体(struct)
结构体(Struct)是一种自定义的数据类型,允许你组合不同类型的数据项。结构体在Go中非常重要,通常用于表示具有多个属性的复杂数据结构。以下是有关Go中结构体的基本知识点:
定义结构体
使用 type
和 struct
关键字定义结构体。
type Person struct {
Name string
Age int
}
这里定义了一个 Person
类型的结构体,有两个字段:Name
和 Age
。
创建结构体实例
可以通过多种方式创建结构体实例。
-
使用结构体字面量:
person := Person{"Alice", 30}
-
指定字段名称:
person := Person{Name: "Alice", Age: 30}
-
使用
new
关键字:personPtr := new(Person) // 创建一个指向Person类型的指针
访问和修改字段
访问或修改结构体的字段使用点(.
)操作符。
fmt.Println(person.Name) // 访问
person.Age = 31 // 修改
方法
在Go中,可以给结构体类型定义方法。
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
这里定义了一个 SayHello
方法,它绑定到 Person
类型。
指针和结构体
可以使用结构体指针来避免在函数调用时复制结构体的数据。
func (p *Person) SetAge(age int) {
p.Age = age
}
这个方法使用指针接收器,允许在不复制结构体的情况下修改它。
匿名字段和嵌入结构体
结构体可以包含匿名(或嵌入)字段,这允许一种简单的形式的继承。
type Employee struct {
Person
Position string
}
这里 Employee
结构体嵌入了 Person
,因此继承了 Person
的所有字段。
导出和非导出字段
如果结构体字段的名称以大写字母开头,则该字段被导出(即在包外可见)。以小写字母开头的字段是非导出的,仅在其定义的包内可见。
总结
结构体是Go中处理复杂数据的基石,允许将相关的数据组合在一起。通过使用结构体和其方法,Go语言提供了一种简单而强大的方式来表示和操作复杂的数据结构。