Golang学习:基础知识篇(二)—— 数组及切片
- 前言
- 什么是Golang?
- Go语言的基础语法
- 数组
- 声明数组
- 初始化数组
- 访问数组
- 知识点补充
- 切片
- 定义切片
- 切片初始化
- len() 和 cap() 函数
- 空(nil)切片
- 切片截取
- append() 和 copy() 函数
- 知识点补充
前言
很久之前就想学Go语言了,但是一直有其他东西要学,因为我学的是Java嘛,所以后面学的东西一直是跟Java相关的。
最近来到公司实习,需要用到Go语言,所以就趁着这个机会把Go学了。
什么是Golang?
简单来说就是由Google公司的Robert Griesemer,Rob Pike和Ken Thompson设计的一种静态类型、编译型语言。它在2009年正式对外公开,目标是解决大规模软件工程中的问题。Go语言的语法简洁清晰,易于学习和使用,编译速度快,具有垃圾回收功能,并且拥有强大的标准库。
Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。Go语言设计最本质的初衷就是简单,希望程序员的工作量最小化,利用Go本身少量的特性,并通过组合的方式去解决实际问题。
Go语言的基础语法
前一章马马虎虎地把数组之前的一些东西简单梳理了一遍,今天讲一些数组之后的知识。
推荐学习教程:菜鸟教程 | Go语言
我会更侧重讲一些网上教程没有,或者没讲清楚的一些东西。
数组
声明数组
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var arrayName [size]dataType
其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中元素的数据类型。
以下定义了数组 balance 长度为 10 类型为 float32:
var balance [10]float32
初始化数组
以下演示了数组初始化:
以下实例声明一个名为 numbers 的整数数组,其大小为 5,在声明时,数组中的每个元素都会根据其数据类型进行默认初始化,对于整数类型,初始值为 0。
var numbers [5]int
还可以使用初始化列表来初始化数组的元素:
var numbers = [5]int{1, 2, 3, 4, 5}
以上代码声明一个大小为 5 的整数数组,并将其中的元素分别初始化为 1、2、3、4 和 5。
另外,还可以使用 := 简短声明语法来声明和初始化数组:
numbers := [5]int{1, 2, 3, 4, 5}
以上代码创建一个名为 numbers 的整数数组,并将其大小设置为 5,并初始化元素的值。
注意:在 Go 语言中,数组的大小是类型的一部分,因此不同大小的数组是不兼容的,也就是说 [5]int 和 [10]int 是不同的类型。
如果数组长度不确定,可以使用 … 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
如果设置了数组的长度,我们还可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
访问数组
其实和Java一样,注意一下Go语言的语法格式就行。然后就是Go中也有一些操作数组的函数,例如
len(arr):返回数组 arr 的长度。range:常与 for 循环一起使用,用于遍历数组。它返回两个值:索引和该索引处的值
其实,Go 语言中的切片(slices)提供了更多的内置函数,如 append 和 copy。切片是对数组的抽象,使用得更为广泛。
知识点补充
这里简单对range遍历数组2进行一个说明,后续实验要是碰到了再做记录
在 Go 语言中,range
关键字用于 for
循环中迭代数组、切片、通道或集合的元素。在数组和切片中,它返回元素的索引和索引对应的值。
以下是一个使用 range
遍历数组的例子:
nums := []int{2, 3, 4}
for i, num := range nums {fmt.Printf("index: %d, value: %d\n", i, num)
}
在这个例子中,i
是索引,num
是索引 i
对应的值。这段代码会打印出数组 nums
的每个元素及其索引。
你也可以只获取索引或值。如果只想获取索引,可以这样写:
for i := range nums {fmt.Printf("index: %d\n", i)
}
如果只想获取值,可以这样写:
for _, num := range nums {fmt.Printf("value: %d\n", num)
}
在这两个例子中,我们使用了空白标识符 _
来忽略 range
返回的某个值。
注意事项:
range
在每次迭代时都会返回两个值(索引和值),即使你只需要其中一个。如果你不需要其中一个值,你可以使用空白标识符_
来忽略它。- 在 Go 语言中,数组的大小是类型的一部分,因此不同大小的数组是不兼容的²。
- 当使用
range
遍历数组或切片时,返回的值是元素的副本,而不是元素本身。这意味着如果你在循环体内修改了元素,原数组并不会被改变。
切片
切片是Go语言中一个新的概念:Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用。
于是Go 提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
你可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度。
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len)也可以简写为slice1 := make([]type, len)
也可以指定容量,其中 capacity 为可选参数。
make([]T, length, capacity)
这里 len 是数组的长度并且也是切片的初始长度。
切片初始化
直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3 (这两玩意是啥后面会讲)。
s :=[] int {1,2,3 }
初始化切片 s,是数组 arr 的引用。
s := arr[:]
将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
s := arr[startIndex:endIndex]
默认 endIndex 时将表示一直到arr的最后一个元素。
s := arr[startIndex:]
默认 startIndex 时将表示从 arr 的第一个元素开始。
s := arr[:endIndex]
通过切片 s 初始化切片 s1。
s1 := s[startIndex:endIndex]
通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片。
s :=make([]int,len,cap)
len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
这里讲一下长度和容量,这是是两个不同的概念。
-
长度:切片的长度是指它所包含的元素个数。可以通过
len(s)
函数来获取切片的长度。 -
容量:切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。可以通过
cap(s)
函数来获取切片的容量。
容量必须大于或等于长度,因为长度表示切片当前的元素数量,而容量表示切片可以访问的元素数量。换句话说,容量表示了切片底层数组的大小,而长度则表示了当前正在使用的元素的数量。
当你向切片添加元素时,如果切片的容量不足以容纳更多的元素,Go语言会自动为切片分配一个新的底层数组,并将原有的元素复制到新数组中。这就是为什么我们说切片是动态的:它们可以根据需要增长和缩小。
所以说容量这个引入还是很有必要的,以下是一些关于容量的重要点:
- 性能优化:当你知道你的切片可能会增长到多大时,你可以在创建切片时就设置一个足够大的容量。这样,当你向切片添加元素时,Go语言就不需要频繁地分配新的底层数组和复制元素,从而提高性能。
- 避免不必要的内存分配:如果你没有预先设定一个足够大的容量,那么每次向切片添加元素时,如果容量不足,Go语言都会为切片分配一个新的底层数组。这可能会导致大量的内存分配和释放,从而降低程序的性能。
- 控制切片的增长:通过管理切片的容量,你可以更好地控制切片的增长。例如,你可以通过限制切片的容量来防止它无限制地增长。
考一下,下面这种写法是否正确?
s :=make([]int,4,8)
s[6]:=3
这样写是不正确的。在Go语言中,当你创建一个切片时,例如s := make([]int, 4, 8),这个切片的长度是4,容量是812。
虽然容量是8,但是你只能访问长度内的元素,也就是索引0到3的元素。
所以,当你试图访问或修改索引为6的元素,比如s[6] = 3,将会得到一个运行时错误:index out of range……。
如果你想要扩展切片的长度,你可以使用append函数实现增加元素。
空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0。示例如下:
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)if(numbers == nil){fmt.Printf("切片是空的")}
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
以上实例运行输出结果为:
len=0 cap=0 slice=[]
切片是空的
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],可以省略前面或者后面,或者干脆都省了。(前面应该有出现过具体说明,就不细讲了)
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
知识点补充
问:改变切片的值会影响到原来数组的值吗?切片可以大于原数组吗
在Go语言中,切片是对数组的引用。因此,如果改变了切片中的元素,那么原数组中对应的元素也会被改变。这是因为切片的内部包含一个指向底层数组的指针,所以函数对切片的修改也会反应到底层数组上。
且切片可以大于原数组。当向切片添加元素时,如果切片的容量不足以容纳更多的元素,Go语言会自动为切片分配一个新的底层数组,并将原有的元素复制到新数组中。
这就意味着,尽管切片开始时是基于原数组创建的,但在扩容过程中,它可能会脱离原数组,使用一个全新的底层数组。(是不是很神奇 @_@😉)
问:那么当切片大于原数组,并分配了一个新的底层数组后,对切片进行修改还会影响到原来的数组吗?
当切片扩容并分配了一个新的底层数组后,对切片进行修改不会影响到原来的数组。这是因为在扩容过程中,Go语言会为切片分配一个新的底层数组,并将原有的元素复制到新数组中。
此时,切片已经不再引用原数组,而是引用新的底层数组。因此,对切片的修改不会影响到原数组。
问:切片和数组的使用场景有哪些,什么时候我们会需要用到切片?
切片和数组在Go语言中都是重要的数据结构,但它们有一些关键的区别:
-
长度:数组的长度是固定的,而切片的长度是可变的。数组在定义时就需要指定长度和元素类型,例如:
[4]int
表示一个包含四个整数的数组,数组的大小是固定的。切片则可以在定义时长度可以为空,也可以指定一个初始长度。 -
值类型与引用类型:数组是值类型,而切片是引用类型。这意味着当数组被赋值给另一个变量时,会创建一个新的数组副本,而切片则是共享底层数组的¹²³⁴。
-
内存分配:数组的内存空间是在定义时分配的,其大小是固定的;切片的内存空间是在运行时动态分配的,其大小是可变的。
关于使用场景,数组和切片各有其优势:
-
数组:由于数组长度固定,所以在知道确切元素数量且不需要动态改变时,使用数组是个不错的选择。例如,在实现一个俄罗斯方块游戏时,随机生成的积木其实就是一个二维数组。
-
切片:相比之下,切片更加灵活且常用。它支持自动扩容,并且可以快速地操作一块数据集合。当你需要处理一个元素数量未知或需要动态改变的数据集合时,使用切片会更加方便。
问:切片的扩容机制是怎么样的?
在Go语言中,切片的扩容机制是这样的:
-
新扩容容量大于当前容量的2倍:如果新的扩容容量大于当前容量的2倍,那么新的容量就会被设置为新的扩容容量。
-
旧容量小于256:如果旧的容量小于256,那么新的容量会变为旧的容量的2倍。
-
旧容量大于等于256:如果旧的容量大于等于256,那么新的容量会不断地增加,每次增加的速率是1.25倍,直到新的容量大于或等于目标容量。
-
初始容量为0:如果初始的容量为0,那么新的容量就会被直接设置为目标容量。
然而,这只是理论上的计算。在实际操作中,Go语言还会进行一些优化操作,比如内存对齐等。因此,实际的扩容结果可能会比理论计算结果稍微大一些。
那么以上就是本次的内容了,在学习的过程中,可以多去敲敲代码,熟悉一下。也可以去菜鸟教程上面多看看,对新人挺友好的,也是我的公司同事推荐的教程网站。
后续也会不定期更新学习记录和一些学习实验。🤠