likes
comments
collection
share

云原生探索系列(二):Go基础语法 与 Python 对比(数据结构一)

作者站长头像
站长
· 阅读数 34

前言

在之前的文章中,介绍了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=[]

这段代码创建了三个不同的切片,并打印了它们的长度和容量。

  1. mySlice2 是一个长度为10、容量为10的切片,表示它可以容纳10个元素,同时切片内部的数组也有10个元素的空间。
  2. mySlice3 是一个长度为10、容量为20的切片,表示它可以容纳10个元素,但是切片内部的数组有20个元素的空间。这意味着在需要时,切片可以通过增长其长度而不必重新分配内存。
  3. 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
评论
请登录