go语言之数组和切片使用及说明
作者:可能只会写BUG
Array(数组)
在 Go 语言中,数组是一种固定大小的数据结构,用于存储同类型的元素。数组的大小在编译时确定,定义后不能更改。
数组的注意事项:
数组是值类型,当将数组传递给函数时,实际上是传递了数组的副本。如果希望函数能够修改原数组,可以使用指向数组的指针。
- 数组的长度是数组类型的一部分,因此 [5]int 和 [10]int 是不同类型。
- 数组在 Go 语言中是一个基本的数据结构,用于存储固定大小的同一类型元素。
- 虽然 Go 提供了数组的支持,但在实际开发中,切片(slice)通常更受欢迎,
- 因为它们更灵活(可以动态调整大小),更易于使用。数组主要用于需要固定大小的场景。
数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T
表示指针数组,[n]T
表示数组指针
定义数组
数组的定义语法如下:
var arrayName [size]dataType arrayName 是数组的名称。 size 是数组的长度(固定的)。 dataType 是数组中元素的数据类型。
声明和初始化数组 package main import "fmt" func main() { // 声明一个长度为 5 的整数数组 var numbers [5]int // 初始化数组 numbers[0] = 1 numbers[1] = 2 numbers[2] = 3 numbers[3] = 4 numbers[4] = 5 fmt.Println("数组内容:", numbers) }
声明并初始化数组 可以在声明数组时直接初始化它: package main import "fmt" func main() { // 声明并初始化 colors := [3]string{"红", "绿", "蓝"} fmt.Println("颜色数组:", colors) }
使用简短声明 用简短声明也可以创建数组: package main import "fmt" func main() { fruits := [...]string{"苹果", "香蕉", "橙子"} // 根据初始化的元素数量来确定数组长度 fmt.Println("水果数组:", fruits) // 输出: 水果数组: [苹果 香蕉 橙子] fmt.Println("长度:", len(fruits)) // 输出: 3 }
数组的访问 可以通过索引访问数组的元素,索引从 0 开始: package main import "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 main import "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 main import "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.Pointer len int cap int } array: 指向底层数组的指针。 len: 切片当前的长度(元素个数)。 cap: 切片的容量(底层数组的总大小)。
切片的定义
var 变量名 []切片中元素类型
package main import "fmt" func main() { // 声明切片类型 var a0 []string //声明一个字符串切片 此时没有初始化,是nil var 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) //true fmt.Println(b == nil) //false fmt.Println(c == nil) //false //fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较 }
从数组创建切片
package main import "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,但容量为2 t := 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):4 fmt.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):1 fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2)) a[1] = 10 fmt.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 main import "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 main import "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 中取值初始化 newSlice 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] }
使用内置函数 make 创建切片
package main import "fmt" func main() { slice := make([]int, 5) // 创建一个长度为5的整数切片 fmt.Println("切片:", slice) // 输出: [0 0 0 0 0] // 可以指定初始容量 sliceWithCap := make([]int, 5, 10) // 长度为5,容量为10 fmt.Println("切片,容量:", len(sliceWithCap), cap(sliceWithCap)) // 输出: 5 10 }
使用字面量创建切片
package main import "fmt" func main() { slice := []string{"苹果", "香蕉", "橙子"} fmt.Println("切片:", slice) // 输出: [苹果 香蕉 橙子] }
判断切片是否为空
1. 检查切片的长度
切片的长度可以通过len函数获取。如果切片的长度为0,则说明切片是空的。
package main import "fmt" func main() { var a []int // 声明一个零值切片(nil切片) b := []int{} // 空切片的初始化 fmt.Println("a is empty:", len(a) == 0) // 输出: a is empty: true fmt.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 main import "fmt" func main() { var a []int // 声明一个零值切片(nil切片) b := []int{} // 一个空切片(已初始化) // 判断a是否为nil if a == nil { fmt.Println("Slice a is nil.") } else { fmt.Println("Slice a is not nil.") } // 判断b是否为nil if 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 main import "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)) // true fmt.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] = 100 fmt.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 main import "fmt" func main() { slice := []int{10, 20, 30, 40} fmt.Println("切片的第一个元素:", slice[0]) // 输出: 10 }
修改元素 可以直接通过索引修改切片中的元素
package main import "fmt" func main() { slice := []int{1, 2, 3} slice[1] = 5 // 修改第二个元素 fmt.Println("修改后的切片:", slice) // 输出: [1 5 3] }
追加元素 使用 append 函数可以向切片追加元素。
package main import "fmt" func main() { slice := []int{1, 2, 3} slice = append(slice, 4, 5) // 追加多个元素 fmt.Println("追加后的切片:", slice) // 输出: [1 2 3 4 5] }
切片的切割 可以通过切片操作来获取切片的子切片
package main import "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:0xc0000ae000 s := a[0:3] fmt.Println(s) //[1 2 3] fmt.Printf("打印的是数组的地址 s:%p\n", s) // s:0xc0000ae000 s1 := 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:0xc0000081b0 fmt.Printf("打印切片的地址 &s1:%p\n", &s1) // &s1:0xc0000081f8 }
切片本身的大小
package main import ( "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 main import "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 main import "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 main import "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] }
以上示例展示了从切片中删除元素的几种不同方法
在使用这些方法时,注意以下几点:
- 指针和引用:切片是引用类型,删除元素的过程通常通过新切片引用来实现,并不会改变原切片本身的内存结构。
- 性能考虑:在删除大量元素时,要考虑性能,循环和过滤可能会导致较大开销,如果只需要删除一个元素,使用简单的切割方式会更高效。
- 负索引检查:确保在进行删除操作时对索引或元素值进行有效性检查,避免运行时错误。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。