云原生探索系列(二):Go基础语法 与 Python 对比(数据结构一)
前言
在之前的文章中,介绍了go语言中的控制结构,并与python语法进行了比较。这篇文章开始数据结构的学习,采用同样的学习手段,与python进行类比了解。这篇 文章将重点介绍切片相关知识点。
变量
变量声明和初始化
var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。 如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。 go声明变量:
func main() {
var name, sex string
name, sex = "may", "女"
fmt.Println(name, sex)
var i, j = 1, 2
fmt.Println(i, j)
}
go还提供一种短变量声明:
- 在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。
- 函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。
func main() {
age, weight := 18, 120
fmt.Println(age, weight)
}
python声明变量: 在 Python 中,变量的声明是通过直接给变量赋值来完成的,而不需要显式地声明变量的类型。
# 声明一个整型变量
age = 25
# 声明一个字符串变量
name = "Alice"
# 声明一个浮点数变量
price = 9.99
# 声明一个布尔型变量
is_active = True
# 声明一个列表(数组)变量
numbers = [1, 2, 3, 4, 5]
# 声明一个字典变量
person = {"name": "Bob", "age": 30}
# 声明一个空变量
my_var = None
# 声明多个变量并赋值
x, y, z = 1, 2, 3
在 Python 中,变量的类型是根据赋给它们的值来确定的。你可以随时改变变量的值和类型。此外,Python 还支持动态类型,这意味着你可以在运行时更改变量的类型。像下面这样,
if __name__ == '__main__':
x = 5 # x 现在是整型
print(x) # 输出:5
x = "Hello" # 现在 x 变成了字符串类型
print(x) # 输出:"Hello"
x 最初被赋值为整数 5,然后它被重新赋值为字符串 "Hello"。Python 在运行时会自动调整变量的类型。 注意:动态类型是python的特性,go是不允许的 可以在go程序中试着更改变量类型,看看报什么错误
func main() {
var x = 5
x = "Hello"
fmt.Println(x)
}
这是错误代码,编译会报错,因为已经声明x为int型。
常量
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。 go定义常量:
func main() {
const LENGTH, WIDTH int = 10, 5
}
python定义常量:
if __name__ == '__main__':
LENGTH, WIDTH = 10, 5
python没有严格的概念,通常约定使用大写字母命名的变量来表示常量,并且在代码中不会对其进行修改。
数组
数组是具有相同类型且长度固定连续内存片段,通过编号访问每个元素。 go定义数组:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr[2])
}
python定义数组: 在 Python 中,没有严格意义上的数组,但是有列表(list)和 NumPy 数组(numpy array)等数据结构可以用来表示和操作类似于数组的数据。
列表(list)是 Python 中最常用的序列类型,可以包含不同类型的元素,并且长度可变。列表使用方括号 []
定义,元素之间用逗号 ,
分隔。
my_list = [1, 2, 3, 4, 5] # 定义一个整数列表
另外,一些三方库(如 NumPy、pandas、array)也提供了类似于数组的数据结构。
切片
-
切片是对数组一个连续片段的引用
-
数组定义中不指定长度即为切片
-
切片在未初始化之前默认为nil,长度为0
小案例:使用切片来操作数组,添加元素和删除切片中元素。
GO语言:
func main() {
myArray := [5]int{1, 2, 3, 4, 5}
mySlice := myArray[1:3]
mySlice = append(mySlice, 6, 7, 8)
fmt.Printf("mySlice %+v\n", mySlice)
fullSlice := myArray[:]
remove3rdItem := deleteItem(fullSlice, 2)
fmt.Printf("remove3rdItem %+v\n", remove3rdItem)
}
func deleteItem(slice []int, index int) []int {
return append(slice[:index], slice[index+1:]...)
}
编译执行代码,输出下面结果:
mySlice [2 3 6 7 8]
remove3rdItem [1 2 4 5]
append
是go提供的增加切片元素的方法,但删除切片元素没有提供对应方法,需要我们自己实现deleteItem
.
在这里,myArray 是一个包含5个整数的数组,mySlice 是从 myArray 中获取的包含索引1和索引2元素的切片,即 [2, 3]。
定义了一个名为 deleteItem 的函数,用于从切片中删除指定索引位置的元素。它的参数包括一个整数切片 slice 和一个要删除的元素的索引 index。该函数使用 append 函数将原切片中指定索引位置的元素删除,然后返回删除元素后的新切片。
... 用于将切片或数组进行解包,将其元素展开成一个参数序列。在 append 函数中,... 让我们可以将多个参数传递给 append,而不是将整个切片作为一个单独的参数。这样,append 就可以接受多个要追加到切片末尾的元素。
除了上面截取方式定义切片,我们还可以通过Make 和 New
- New返回指针地址
- Make返回引用类型的实例,可预设内存空间,避免未来的内存拷贝
New new 用于分配内存并返回指向该类型的指针。但是,它不会初始化内存,只会将其置为零值(对于引用类型,零值为 nil)。因此,如果你使用 new 创建一个切片,你将得到一个指向 nil 的切片指针,而不是一个包含任何元素的实际切片。
func main() {
mySlice := new([]int)
fmt.Printf("len=%d cap=%d slice=%+v\n", len(*mySlice), cap(*mySlice), *mySlice)
}
mySlice 是一个指向切片的指针。*mySlice 表示对该指针进行解引用,获取指针指向的实际切片对象。 mySlice 是通过 new([]int) 创建的,这意味着它只是一个指向 nil 切片的指针,因此解引用后得到的切片是一个空切片。因此,len(*mySlice) 和 cap(*mySlice) 的结果都是0,而 *mySlice 的值为 []。
Make make 返回的是该引用类型的实例。例如,make([]int, 5) 会创建一个包含5个整数的切片,并分配内存来存储这些整数。
func main() {
mySlice2 := make([]int, 10)
mySlice3 := make([]int, 10, 20)
mySlice4 := make([]int, 0)
fmt.Printf("len=%d cap=%d slice=%+v\n", len(mySlice2), cap(mySlice2), mySlice2)
fmt.Printf("len=%d cap=%d slice=%+v\n", len(mySlice3), cap(mySlice3), mySlice3)
fmt.Printf("len=%d cap=%d slice=%+v\n", len(mySlice4), cap(mySlice4), mySlice4)
}
看看输出结果:
len=10 cap=10 mySlice2=[0 0 0 0 0 0 0 0 0 0]
len=10 cap=20 mySlice3=[0 0 0 0 0 0 0 0 0 0]
len=0 cap=0 mySlice4=[]
这段代码创建了三个不同的切片,并打印了它们的长度和容量。
mySlice2
是一个长度为10、容量为10的切片,表示它可以容纳10个元素,同时切片内部的数组也有10个元素的空间。mySlice3
是一个长度为10、容量为20的切片,表示它可以容纳10个元素,但是切片内部的数组有20个元素的空间。这意味着在需要时,切片可以通过增长其长度而不必重新分配内存。mySlice4
是一个长度为0、容量为0的切片,这是一个空切片,没有分配任何内存空间。
Python语言: 在 Go 语言中,切片(slice)是一个动态数组,Python 中也有类似于 Go 语言中切片(slice)的概念,它就是 Python 中的列表(list)。列表在 Python 中是一种有序的集合,可以包含任意数量的元素,并且可以动态增加或减少元素。与切片类似,列表也支持类似于切片的操作,如索引、切片、追加、删除等。
myList = [1, 2, 3, 4, 5] # 创建一个列表
print(myList) # 输出: [1, 2, 3, 4, 5]
# 使用索引访问列表元素
print(myList[0]) # 输出: 1
print(myList[-1]) # 输出: 5
# 切片操作
print(myList[1:3]) # 输出: [2, 3]
# 追加元素
myList.append(6)
print(myList) # 输出: [1, 2, 3, 4, 5, 6]
# 删除元素
del myList[2]
print(myList) # 输出: [1, 2, 4, 5, 6]
可以看到,Python 中的列表提供了类似于 Go 语言中切片的功能,使得对序列数据的处理更加灵活和方便。
切片延伸
切片是Go语言中一个比较重要的知识点,这里我们还是延伸重点学习下。 通过上面切片模块的学习,我们知道,通过调用内建函数len,可以得到它的长度。通过调用内建函数cap,可以得到它的容量。那我们看看如下代码,通过不同方式 创建的切片,长度和容量有何不同呢?
func print(slice []int) {
if slice == nil {
fmt.Printf("切片为nil切片,len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
} else if len(slice) == 0 && cap(slice) == 0 {
fmt.Printf("切片为空切片,len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
} else {
for _, v := range slice {
if v != 0 {
fmt.Printf("切片为已初始化,len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
return
}
}
fmt.Printf("切片为零切片,len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
}
}
func main() {
s1 := make([]int, 5)
print(s1)
s2 := make([]int, 5, 8)
print(s2)
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
print(s4)
s5 := make([]int, 0)
print(s5)
s6 := new([]int)
print(*s6)
var s7 []int
print(s7)
s8 := []int{}
print(s8)
}
先执行看看输出结果:
s1切片为零切片,len=5 cap=5 slice=[0 0 0 0 0]
s2切片为零切片,len=5 cap=8 slice=[0 0 0 0 0]
s4切片为已初始化,len=3 cap=5 slice=[4 5 6]
s5切片为空切片,len=0 cap=0 slice=[]
s6切片为nil切片,len=0 cap=0 slice=[]
s7切片为nil切片,len=0 cap=0 slice=[]
s8切片为空切片,len=0 cap=0 slice=[]
从输出结果,可以看出有三种切片。
-
零切片 切片内部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,使用make创建的、长度、容量都不为0的切片就是零值切片
-
nil切片 nil切片的长度和容量都为0,并且和nil比较的结果为true,采用直接创建切片的方式、new创建切片的方式可以创建nil切片
-
空切片 空切片的长度和容量也都为0,但是和nil的比较结果为false,使用字面量、make可以创建空切片
另外,s4的输出结果中,cap的容量为啥是5呢?
由于s4是通过在s3上截取操作得来的,所以s3的底层数组就是s4的底层数组。又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。注意,切片代表的窗口是无法向左扩展的。也就是说,我们永远无法透过s4看到s3中最左边的那 3 个元素。
切片容量增长
一般情况下,可以简单认为新切片的容量是原切片容量的2倍。编写如下代码,测试一下:
func main() {
s1 := make([]int, 0)
fmt.Printf("s1切片初始容量: %d\n", cap(s1))
for i := 0; i < 10; i++ {
s1 = append(s1, i)
fmt.Printf(" s1=%v len=%d cap=%d\n", s1, len(s1), cap(s1))
}
}
编译执行,看看输出结果:
s1切片初始容量: 0
s1=[0] len=1 cap=1
s1=[0 1] len=2 cap=2
s1=[0 1 2] len=3 cap=4
s1=[0 1 2 3] len=4 cap=4
s1=[0 1 2 3 4] len=5 cap=8
s1=[0 1 2 3 4 5] len=6 cap=8
s1=[0 1 2 3 4 5 6] len=7 cap=8
s1=[0 1 2 3 4 5 6 7] len=8 cap=8
s1=[0 1 2 3 4 5 6 7 8] len=9 cap=16
s1=[0 1 2 3 4 5 6 7 8 9] len=10 cap=16
当原切片长度大于等于1024时,新切片容量以原切片容量的1.25倍作为基准(不断与1.25相乘),直到结果不小于原长度与要追加的元素数量之和。最终,新容量比新长度大一些或者相等。继续编写代码,测试结论:
func main() {
s2 := make([]int, 1024)
fmt.Printf("s2切片初始容量: %d\n", cap(s2))
s2_1 := append(s2, make([]int, 200)...)
fmt.Printf(" s2_1 len=%d cap=%d\n", len(s2_1), cap(s2_1))
s2_2 := append(s2, make([]int, 400)...)
fmt.Printf(" s2_2 len=%d cap=%d\n", len(s2_2), cap(s2_2))
s2_3 := append(s2, make([]int, 600)...)
fmt.Printf(" s2_3 len=%d cap=%d\n", len(s2_3), cap(s2_3))
}
编译执行,看看输出结果:
s2切片初始容量: 1024
s2_1 len=1224 cap=1536
s2_2 len=1424 cap=1536
s2_3 len=1624 cap=2048
如果一次追加的元素过多,使新长度比原容量的2倍还要大,那么新容量以新长度为基准,最终的新容量很多时候比新容量基准要大一些。继续编写代码,测试结论:
func main() {
s3 := make([]int, 10)
fmt.Printf("s3切片初始容量: %d\n", cap(s3))
s3_1 := append(s3, make([]int, 11)...)
fmt.Printf(" s3_1 len=%d cap=%d\n", len(s3_1), cap(s3_1))
s3_2 := append(s3, make([]int, 23)...)
fmt.Printf(" s3_2 len=%d cap=%d\n", len(s3_2), cap(s3_2))
s3_3 := append(s3, make([]int, 45)...)
fmt.Printf(" s3_3 len=%d cap=%d\n", len(s3_3), cap(s3_3))
}
编译执行,看看输出结果:
s3切片初始容量: 10
s3_1 len=21 cap=22
s3_2 len=33 cap=36
s3_3 len=55 cap=56
切片底层数组会被替换吗?
确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。 它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。 请记住,在无需扩容时,append函数返回的是指向原底层数组的原切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。 只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。 写段代码,加深理解:
func main() {
s1 := [7]int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("s1: %v len: %d, cap: %d\n",
s1, len(s1), cap(s1))
s2 := s1[1:4]
fmt.Printf("s2: %v len: %d, cap: %d\n",
s2, len(s2), cap(s2))
for i := 1; i <= 5; i++ {
s2 = append(s2, i)
fmt.Printf("s2 append:%d %v len: %d, cap: %d\n",
i, s2, len(s2), cap(s2))
}
fmt.Printf("s1: %v len: %d, cap: %d\n",
s1, len(s1), cap(s1))
}
编译运行,看看输出结果:
s1: [1 2 3 4 5 6 7] len: 7, cap: 7
s2: [2 3 4] len: 3, cap: 6
s2 append:1 [2 3 4 1] len: 4, cap: 6
s2 append:2 [2 3 4 1 2] len: 5, cap: 6
s2 append:3 [2 3 4 1 2 3] len: 6, cap: 6
s2 append:4 [2 3 4 1 2 3 4] len: 7, cap: 12
s2 append:5 [2 3 4 1 2 3 4 5] len: 8, cap: 12
s1: [1 2 3 4 1 2 3] len: 7, cap: 7
最后
总结一下,这篇文章介绍了Go语言中变量、常量、数组、切片相关知识点,其中着重介绍了切片,占了文章很大一部分内容。因为它着实很重要,且新手使用时会遇到一些困惑。
转载自:https://juejin.cn/post/7373482380738265098