- Array(数组)
- 定义数组
- 数组的长度
- 多维数组
- 切片(slice)
- 切片的基本概念
- 切片的定义
- 从数组创建切片
- 从数组创建切片注意
- 如何不受限地通过数组创建切片
- 使用内置函数 make 创建切片
- 使用字面量创建切片
- 判断切片是否为空
- 1. 检查切片的长度
- 2. 检查切片是否为nil
- 空切片与nil切片:
- 切片不能直接比较
- 切片的赋值拷贝
- 切片遍历
- 访问元素
- 修改元素 可以直接通过索引修改切片中的元素
- 追加元素 使用 append 函数可以向切片追加元素。
- 切片的切割 可以通过切片操作来获取切片的子切片。
- 切片的注意事项
- 切片的底层数组地址
- 切片本身的大小
- append()方法为切片添加元素详解
- 切片的扩容策略
- 从切片中删除元素
- 以上示例展示了从切片中删除元素的几种不同方法。在使用这些方法时,注意以下几点:
Array(数组)
在 Go 语言中,数组是一种固定大小的数据结构,用于存储同类型的元素。数组的大小在编译时确定,定义后不能更改。
数组的注意事项
数组是值类型,当将数组传递给函数时,实际上是传递了数组的副本。如果希望函数能够修改原数组,可以使用指向数组的指针。
数组的长度是数组类型的一部分,因此 [5]int 和 [10]int 是不同类型。
数组在 Go 语言中是一个基本的数据结构,用于存储固定大小的同一类型元素。
虽然 Go 提供了数组的支持,但在实际开发中,切片(slice)通常更受欢迎,
因为它们更灵活(可以动态调整大小),更易于使用。数组主要用于需要固定大小的场景。
数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T
表示指针数组,[n]T
表示数组指针
定义数组
数组的定义语法如下:
var arrayName [size]dataType
arrayName 是数组的名称。
size 是数组的长度(固定的)。
dataType 是数组中元素的数据类型。
声明和初始化数组
package mainimport "fmt"func main() {// 声明一个长度为 5 的整数数组var numbers [5]int// 初始化数组numbers[0] = 1numbers[1] = 2numbers[2] = 3numbers[3] = 4numbers[4] = 5fmt.Println("数组内容:", numbers)
}
声明并初始化数组
可以在声明数组时直接初始化它:package mainimport "fmt"func main() {// 声明并初始化colors := [3]string{"红", "绿", "蓝"}fmt.Println("颜色数组:", colors)
}
使用简短声明
用简短声明也可以创建数组:package mainimport "fmt"func main() {fruits := [...]string{"苹果", "香蕉", "橙子"} // 根据初始化的元素数量来确定数组长度fmt.Println("水果数组:", fruits) // 输出: 水果数组: [苹果 香蕉 橙子]fmt.Println("长度:", len(fruits)) // 输出: 3
}
数组的访问
可以通过索引访问数组的元素,索引从 0 开始:package mainimport "fmt"func main() {days := [7]string{"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"}for i := 0; i < len(days); i++ {fmt.Printf("索引: %d, 值: %s\n", i, days[i])}// 方法2:for range遍历for i, day := range days {fmt.Printf("索引: %d, 值: %s\n", i, day)}}
数组的长度
使用内置的 len() 函数可以获取数组的长度:
package mainimport "fmt"func main() {
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println("数组长度:", len(numbers))
}
我们还可以使用指定索引值的方式来初始化数组
func main() {a := [...]int{1: 1, 3: 5}fmt.Println(a) // [0 1 0 5]fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}
多维数组
Go 语言支持多维数组,常用的如二维数组。二维数组可以被看作是数组的数组:
package mainimport "fmt"func main() {// 声明一个 3x3 的整数二维数组matrix := [3][3]int{{1, 2, 3},{4, 5, 6},{7, 8, 9},}// 遍历二维数组for i := 0; i < len(matrix); i++ {for j := 0; j < len(matrix[i]); j++ {fmt.Print(matrix[i][j], " ")}fmt.Println()}
}
切片(slice)
切片(Slice)是 Go 语言中一个非常重要且常用的数据类型,它是对数组的一个轻量级抽象。
切片可以动态地调整大小,灵活性更高,操作也更加简便。切片本质上是对底层数组的一个引用。
切片的基本概念
切片由三部分组成:
指针:指向切片的第一个元素的地址(指向底层数组的某个位置)。
长度:切片中元素的数量。
容量:切片从其第一个元素开始到底层数组的长度。
切片的底层结构定义在 runtime 包中,具体结构如下
type slice struct {array unsafe.Pointerlen intcap int
}
array: 指向底层数组的指针。len: 切片当前的长度(元素个数)。cap: 切片的容量(底层数组的总大小)。
切片的定义
var 变量名 []切片中元素类型
package mainimport "fmt"func main() {// 声明切片类型var a0 []string //声明一个字符串切片 此时没有初始化,是nilvar a = []string{} //声明一个字符串切片 并初始化为空切片var b = []int{} //声明一个整型切片并初始化var c = []bool{false, true} //声明一个布尔切片并初始化//var d = []bool{false, true} //声明一个布尔切片并初始化if a0 == nil {fmt.Println("a0 is nil")}if a == nil {fmt.Println("a is nil")}fmt.Println(a) //[]fmt.Println(b) //[]fmt.Println(c) //[false true]fmt.Println(a == nil) //truefmt.Println(b == nil) //falsefmt.Println(c == nil) //false//fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
从数组创建切片
package mainimport "fmt"func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 创建从索引1到索引3的切片fmt.Println("切片:", slice) // 输出: [2 3 4]
}func main() {a := [5]int{1, 2, 3, 4, 5}t := a[1:3:3] //意思是从索引1开始,到索引3结束不包括3,但容量为2t := a[1:3:5] //意思是从索引1开始,到索引3结束不包括3,但容量为4 容量是从切片的起始索引1到原数组的最大索引(在这里是5)之间的元素数量,包括a[4]。所以从索引1到4的元素有效。fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}
从数组创建切片注意
切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。
切片表达式中的low 和high 表示一个索引范围(左包含,右不包含),
对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a), 而不是长度。
常量索引必须是非负的,并且可以用int类型的值表示;
对于数组或常量字符串,常量索引也必须在有效范围内。
如果low和high两个指标都是常数,它们必须满足low <= high。
如果索引在运行时超出范围,就会发生运行时panic
//切片是对底层数组的一个视图,并不会复制整个数组。当你从一个数组创建切片时,
//切片只是创建了一个指向原始数组的引用,因此切片对原始数组的修改会影响到原始数组,反之亦然。
func main() {a := [5]int{1, 2, 3, 4, 5} //定义一个数组s := a[1:3] // s := a[low:high]//s:[2 3] len(s):2 cap(s):4fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))//切片是对底层数组的一个视图,并不会复制整个数组。当你从一个数组创建切片时,//切片只是创建了一个指向原始数组的引用,因此切片对原始数组的修改会影响到原始数组,反之亦然。s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)//s2:[5] len(s2):1 cap(s2):1fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))a[1] = 10fmt.Printf("a:%v len(a):%v cap(a):%v\n", a, len(a), cap(a))fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))}
如何不受限地通过数组创建切片
1. 使用 copy 函数(推荐)
copy 函数可以用来复制一个切片的内容到另一个切片。这样你就能得到一个不受原切片影响的新切片。
package mainimport "fmt"func main() {a := [5]int{1, 2, 3, 4, 5}s := a[1:3] // 创建切片 s,内容为 [2, 3]// 创建一个新的切片,并使用 copy 复制内容newSlice := make([]int, len(s)) // 创建一个新的切片,长度与 s 相同copy(newSlice, s) // 复制 s 的内容到 newSlice// 现在,newSlice 是 s 的一个副本fmt.Printf("newSlice before modification: %v\n", newSlice)// 修改原数组a[3] = 10// 输出结果fmt.Printf("Original array a: %v\n", a) // a:[1 2 3 10 5]fmt.Printf("Slice s: %v\n", s) // s: [2 3]fmt.Printf("Copied slice newSlice: %v\n", newSlice) // newSlice: [2 3]
}2.手动创建切片
另一个方法是直接手动创建一个新的切片,并使用原始切片的元素来初始化它
package mainimport "fmt"func main() {a := [5]int{1, 2, 3, 4, 5}s := a[1:3] // 创建切片 s,内容为 [2, 3]// 手动创建新切片newSlice := []int{s[0], s[1]} // 直接从 s 中取值初始化 newSlicefmt.Printf("newSlice before modification: %v\n", newSlice)// 修改原数组a[3] = 10// 输出结果fmt.Printf("Original array a: %v\n", a) // a:[1 2 3 10 5]fmt.Printf("Slice s: %v\n", s) // s:[2 3]fmt.Printf("Copied slice newSlice: %v\n", newSlice) // newSlice:[2 3]
}
使用内置函数 make 创建切片
package mainimport "fmt"func main() {
slice := make([]int, 5) // 创建一个长度为5的整数切片
fmt.Println("切片:", slice) // 输出: [0 0 0 0 0]// 可以指定初始容量sliceWithCap := make([]int, 5, 10) // 长度为5,容量为10fmt.Println("切片,容量:", len(sliceWithCap), cap(sliceWithCap)) // 输出: 5 10
}
使用字面量创建切片
package mainimport "fmt"func main() {
slice := []string{"苹果", "香蕉", "橙子"}
fmt.Println("切片:", slice) // 输出: [苹果 香蕉 橙子]
}
判断切片是否为空
1. 检查切片的长度
切片的长度可以通过len函数获取。如果切片的长度为0,则说明切片是空的。
package mainimport "fmt"func main() {var a []int // 声明一个零值切片(nil切片)b := []int{} // 空切片的初始化fmt.Println("a is empty:", len(a) == 0) // 输出: a is empty: truefmt.Println("b is empty:", len(b) == 0) // 输出: b is empty: true// 还可以直接检查长度if len(a) == 0 {fmt.Println("Slice a is empty.")}if len(b) == 0 {fmt.Println("Slice b is empty.")}
}
2. 检查切片是否为nil
如果一个切片没有被初始化(即没有指向任何底层数组),它的值将是nil。你可以通过直接比较切片与nil来判断
package mainimport "fmt"func main() {var a []int // 声明一个零值切片(nil切片)b := []int{} // 一个空切片(已初始化)// 判断a是否为nilif a == nil {fmt.Println("Slice a is nil.")} else {fmt.Println("Slice a is not nil.")}// 判断b是否为nilif b == nil {fmt.Println("Slice b is nil.")} else {fmt.Println("Slice b is not nil.") // 这个会被执行,因为b是一个空切片,已初始化}
}
空切片与nil切片:
一个空切片(如b)虽然长度为0,但它已经被初始化,因此b != nil。
一个未初始化的切片(如a)被视为nil,所以a == nil。
判断切片是否为空时,通常建议同时检查长度和是否为nil,以避免潜在的意外。
package mainimport "fmt"func isEmpty(slice []int) bool {return len(slice) == 0 && slice == nil
}func main() {var a []int // nil切片b := []int{} // 空切片fmt.Println("Is slice a empty?", isEmpty(a)) // truefmt.Println("Is slice b empty?", isEmpty(b)) // false
}
切片不能直接比较
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。
切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组
,一个nil
值的切片的长度和容量都是0。
但是我们不能说一个长度和容量都是0的切片一定是nil
切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容
func main() {s1 := make([]int, 3) //[0 0 0]s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组s2[0] = 100fmt.Println(s1) //[100 0 0]fmt.Println(s2) //[100 0 0]
}
切片遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历。
func main() {s := []int{1, 3, 5}for i := 0; i < len(s); i++ {fmt.Println(i, s[i])}for index, value := range s {fmt.Println(index, value)}
}
访问元素
package mainimport "fmt"func main() {slice := []int{10, 20, 30, 40}fmt.Println("切片的第一个元素:", slice[0]) // 输出: 10
}
修改元素 可以直接通过索引修改切片中的元素
package mainimport "fmt"func main() {slice := []int{1, 2, 3}slice[1] = 5 // 修改第二个元素fmt.Println("修改后的切片:", slice) // 输出: [1 5 3]
}
追加元素 使用 append 函数可以向切片追加元素。
package mainimport "fmt"func main() {slice := []int{1, 2, 3}slice = append(slice, 4, 5) // 追加多个元素fmt.Println("追加后的切片:", slice) // 输出: [1 2 3 4 5]
}
切片的切割 可以通过切片操作来获取切片的子切片。
package mainimport "fmt"func main() {slice := []int{1, 2, 3, 4, 5}subSlice := slice[1:4] // 获取子切片fmt.Println("子切片:", subSlice) // 输出: [2 3 4]
}
切片的注意事项
切片是引用类型,这意味着多个切片可以共享同一个底层数组的部分或全部。
对一个切片的修改可能会影响到其他切片。
切片的容量会随着元素的增加而自动增长,但每次增长会分配新的底层数组。
如果频繁地使用 append,可以先为切片分配一个足够大的容量,以减少内存分配的开销。
使用 copy 函数可以复制切片中的元素到另一个切片中。
切片的底层数组地址
func main() {a := [5]int{1, 2, 3, 4, 5} //定义一个数组//打印地址fmt.Printf("打印的是数组的地址 a:%p\n", &a) // a:0xc0000ae000s := a[0:3]fmt.Println(s) //[1 2 3]fmt.Printf("打印的是数组的地址 s:%p\n", s) // s:0xc0000ae000s1 := a[1:3]fmt.Println(s1) //[2 3]fmt.Printf("打印的是数组的地址 偏移了8个字节 s1:%p\n", s1) // s:0xc0000ae008 (偏移了8个字节) ,不过由于切片从索引1开始,所以地址是数组的第二个元素的地址fmt.Printf("打印切片的地址 &s:%p\n", &s) // &s:0xc0000081b0fmt.Printf("打印切片的地址 &s1:%p\n", &s1) // &s1:0xc0000081f8}
切片本身的大小
package mainimport ("fmt""unsafe"
)func main() {// 声明一个切片var intSlice []int// 获取切片本身的大小sliceSize := unsafe.Sizeof(intSlice)fmt.Printf("切片大小: %d\n", sliceSize)// 切片大小: 24
}
append()方法为切片添加元素详解
append() 函数非常灵活,可以一次添加一个或多个元素。当切片的容量不足以容纳新添加的元素时,append() 自动分配一个新的底层数组。
func append(slice []Type, elems ...Type) []Type
slice 是要添加元素的切片。
elems... 是要添加到切片中的一个或多个元素。
返回值是一个新的切片,包含原切片的所有元素和新添加的元素。
可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){var s []int //通过var声明的零值切片可以在append()函数直接使用,无需初始化。s = append(s, 1) // [1]s = append(s, 2, 3, 4) // [1 2 3 4]s2 := []int{5, 6, 7}s = append(s, s2...) // [1 2 3 4 5 6 7]
}
切片的扩容策略
快速增长:在切片较小的情况下,选择双倍扩容可以较快地满足需求。
渐进增长:当切片已经较大时,使用逐步增加的方式将容量扩增少量,这样可以避免一次分配过大的内存,避免可能的频繁分配和额外的内存压力。
防止溢出:在扩容计算过程中,检查容量是否溢出是很重要的,以此防止出现无限循环或系统崩溃。
newcap := old.cap //newcap 被初始化为当前切片的容量(old.cap)。
doublecap := newcap + newcap //doublecap 是新容量的两倍,可以为切片提供更大的扩容空间。
if cap > doublecap { newcap = cap //如果请求的新容量大于 doublecap,则直接将 newcap 设置为请求的新容量。这是一种确保可以满足用户需求的方式。} else {if old.len < 1024 {newcap = doublecap //当当前切片的长度小于 1024 时,newcap 设置为 doublecap,即双倍扩容。这样可以快速增长容量,适用于较小的切片。} else {//当当前切片的长度大于或等于 1024 时,使用一个循环逐步增加容量://在每次循环中, newcap 增加其自身的四分之一,直到 newcap 大于等于请求的新容量 cap。//这个检查 0 < newcap 是为了防止 newcap 发生溢出,从而导致无限循环。for 0 < newcap && newcap < cap {newcap += newcap / 4}//如果在调整容量时出现溢出(即 newcap <= 0),则将 newcap 设置为请求的新容量 cap。if newcap <= 0 {newcap = cap}}
}
从切片中删除元素
切片是一种动态数组,删除切片中的元素通常涉及到重新创建切片以排除指定的元素。由于切片是引用类型,删除操作并不会改变原始切片的长度和容量,而是通过切片的重新切割来达到节省存储空间的效果。
- 使用切片重组
最简单的方式是通过切片的组合将要删除的元素排除。假设我们有一个整数切片,并希望删除指定索引的元素
package mainimport "fmt"func removeAtIndex(slice []int, index int) []int {// 检查索引是否有效if index < 0 || index >= len(slice) {return slice // 返回原切片}// 将切片分为两部分并组合return append(slice[:index], slice[index+1:]...) // 删除索引 index 处的元素
}func main() {numbers := []int{1, 2, 3, 4, 5}fmt.Println("初始切片:", numbers)// 删除索引为 2 的元素(值为 3)numbers = removeAtIndex(numbers, 2)fmt.Println("删除后的切片:", numbers) // 输出: [1 2 4 5]
}
- 删除多个元素
如果要删除多个元素,可以使用循环并根据条件过滤元素。
package mainimport "fmt"func removeElements(slice []int, value int) []int {result := []int{}for _, v := range slice {if v != value { // 仅保留不等于 value 的元素result = append(result, v)}}return result
}func main() {numbers := []int{1, 2, 3, 4, 3, 5}fmt.Println("初始切片:", numbers)// 删除值为 3 的所有元素numbers = removeElements(numbers, 3)fmt.Println("删除后的切片:", numbers) // 输出: [1 2 4 5]
}
- 使用 copy 函数
有时,我们会希望在删除元素后保留原切片中的数据结构。可以使用 copy 函数来实现。
package mainimport "fmt"func removeAtIndexUsingCopy(slice []int, index int) []int {if index < 0 || index >= len(slice) {return slice // 返回原切片}// 使用 copy 函数copy(slice[index:], slice[index+1:]) // 将后面的元素前移return slice[:len(slice)-1] // 切割到减少后的长度
}func main() {numbers := []int{1, 2, 3, 4, 5}fmt.Println("初始切片:", numbers)// 删除索引为 2 的元素(值为 3)newNumbers := removeAtIndexUsingCopy(numbers, 2)fmt.Println("初始切片:", numbers) // 输出: [1 2 4 5]fmt.Println("删除后的切片:", newNumbers) // 输出: [1 2 4 5]
}
以上示例展示了从切片中删除元素的几种不同方法。在使用这些方法时,注意以下几点:
指针和引用:切片是引用类型,删除元素的过程通常通过新切片引用来实现,并不会改变原切片本身的内存结构。
性能考虑:在删除大量元素时,要考虑性能,循环和过滤可能会导致较大开销,如果只需要删除一个元素,使用简单的切割方式会更高效。
负索引检查:确保在进行删除操作时对索引或元素值进行有效性检查,避免运行时错误。