go语言 数组和切片

Array(数组)

在 Go 语言中,数组是一种固定大小的数据结构,用于存储同类型的元素。数组的大小在编译时确定,定义后不能更改。

数组的注意事项

数组是值类型,当将数组传递给函数时,实际上是传递了数组的副本。如果希望函数能够修改原数组,可以使用指向数组的指针。

数组的长度是数组类型的一部分,因此 [5]int 和 [10]int 是不同类型。

数组在 Go 语言中是一个基本的数据结构,用于存储固定大小的同一类型元素。

虽然 Go 提供了数组的支持,但在实际开发中,切片(slice)通常更受欢迎,

因为它们更灵活(可以动态调整大小),更易于使用。数组主要用于需要固定大小的场景。

  1. 数组支持 "=="、"!=" 操作符,因为内存总是被初始化过的。

  2. [n]*T表示指针数组,[n]T表示数组指针

定义数组

数组的定义语法如下:

c 复制代码
var arrayName [size]dataType
arrayName 是数组的名称。
size 是数组的长度(固定的)。
dataType 是数组中元素的数据类型。
c 复制代码
声明和初始化数组
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)
}
c 复制代码
声明并初始化数组
可以在声明数组时直接初始化它:

package main

import "fmt"

func main() {
    // 声明并初始化
    colors := [3]string{"红", "绿", "蓝"}

    fmt.Println("颜色数组:", colors)
}
c 复制代码
使用简短声明
用简短声明也可以创建数组:

package main

import "fmt"

func main() {
    fruits := [...]string{"苹果", "香蕉", "橙子"} // 根据初始化的元素数量来确定数组长度

    fmt.Println("水果数组:", fruits) // 输出: 水果数组: [苹果 香蕉 橙子]
    fmt.Println("长度:", len(fruits)) // 输出: 3
}
c 复制代码
数组的访问
可以通过索引访问数组的元素,索引从 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() 函数可以获取数组的长度:

c 复制代码
package main

import "fmt"

func main() {
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println("数组长度:", len(numbers))
}

我们还可以使用指定索引值的方式来初始化数组

c 复制代码
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 语言支持多维数组,常用的如二维数组。二维数组可以被看作是数组的数组:

c 复制代码
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 语言中一个非常重要且常用的数据类型,它是对数组的一个轻量级抽象。

切片可以动态地调整大小,灵活性更高,操作也更加简便。切片本质上是对底层数组的一个引用。

切片的基本概念

切片由三部分组成:

指针:指向切片的第一个元素的地址(指向底层数组的某个位置)。

长度:切片中元素的数量。

容量:切片从其第一个元素开始到底层数组的长度。

c 复制代码
切片的底层结构定义在 runtime 包中,具体结构如下
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
array: 指向底层数组的指针。

len: 切片当前的长度(元素个数)。

cap: 切片的容量(底层数组的总大小)。

切片的定义

var 变量名 []切片中元素类型

c 复制代码
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比较
}

从数组创建切片

c 复制代码
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

//切片是对底层数组的一个视图,并不会复制整个数组。当你从一个数组创建切片时,

//切片只是创建了一个指向原始数组的引用,因此切片对原始数组的修改会影响到原始数组,反之亦然。

c 复制代码
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))

}

如何不受限地通过数组创建切片

c 复制代码
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 创建切片

c 复制代码
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
}

使用字面量创建切片

c 复制代码
package main

import "fmt"

func main() {
slice := []string{"苹果", "香蕉", "橙子"}
fmt.Println("切片:", slice) // 输出: [苹果 香蕉 橙子]
}

判断切片是否为空

1. 检查切片的长度

切片的长度可以通过len函数获取。如果切片的长度为0,则说明切片是空的。

c 复制代码
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来判断

c 复制代码
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,以避免潜在的意外。

c 复制代码
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

切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容

c 复制代码
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遍历。

c 复制代码
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)
	}
}

访问元素

c 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20, 30, 40}
    fmt.Println("切片的第一个元素:", slice[0]) // 输出: 10
}

修改元素 可以直接通过索引修改切片中的元素

c 复制代码
package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    slice[1] = 5 // 修改第二个元素
    fmt.Println("修改后的切片:", slice) // 输出: [1 5 3]
}

追加元素 使用 append 函数可以向切片追加元素。

c 复制代码
package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5) // 追加多个元素
    fmt.Println("追加后的切片:", slice) // 输出: [1 2 3 4 5]
}

切片的切割 可以通过切片操作来获取切片的子切片。

c 复制代码
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 函数可以复制切片中的元素到另一个切片中。

切片的底层数组地址

c 复制代码
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

	
	
}

切片本身的大小

c 复制代码
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// 声明一个切片
	var intSlice []int

	// 获取切片本身的大小
	sliceSize := unsafe.Sizeof(intSlice)

	fmt.Printf("切片大小: %d\n", sliceSize)// 切片大小: 24
}

append()方法为切片添加元素详解

append() 函数非常灵活,可以一次添加一个或多个元素。当切片的容量不足以容纳新添加的元素时,append() 自动分配一个新的底层数组。

c 复制代码
func append(slice []Type, elems ...Type) []Type
slice 是要添加元素的切片。
elems... 是要添加到切片中的一个或多个元素。
返回值是一个新的切片,包含原切片的所有元素和新添加的元素。

可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加...)。

c 复制代码
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]
}

切片的扩容策略

快速增长:在切片较小的情况下,选择双倍扩容可以较快地满足需求。

渐进增长:当切片已经较大时,使用逐步增加的方式将容量扩增少量,这样可以避免一次分配过大的内存,避免可能的频繁分配和额外的内存压力。

防止溢出:在扩容计算过程中,检查容量是否溢出是很重要的,以此防止出现无限循环或系统崩溃。

c 复制代码
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
		}
	}
}

从切片中删除元素

切片是一种动态数组,删除切片中的元素通常涉及到重新创建切片以排除指定的元素。由于切片是引用类型,删除操作并不会改变原始切片的长度和容量,而是通过切片的重新切割来达到节省存储空间的效果。

  1. 使用切片重组
    最简单的方式是通过切片的组合将要删除的元素排除。假设我们有一个整数切片,并希望删除指定索引的元素
c 复制代码
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]
}
  1. 删除多个元素
    如果要删除多个元素,可以使用循环并根据条件过滤元素。
c 复制代码
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]
}
  1. 使用 copy 函数
    有时,我们会希望在删除元素后保留原切片中的数据结构。可以使用 copy 函数来实现。
c 复制代码
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]
}

以上示例展示了从切片中删除元素的几种不同方法。在使用这些方法时,注意以下几点:

指针和引用:切片是引用类型,删除元素的过程通常通过新切片引用来实现,并不会改变原切片本身的内存结构。

性能考虑:在删除大量元素时,要考虑性能,循环和过滤可能会导致较大开销,如果只需要删除一个元素,使用简单的切割方式会更高效。

负索引检查:确保在进行删除操作时对索引或元素值进行有效性检查,避免运行时错误。

相关推荐
界面开发小八哥2 分钟前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具
qystca30 分钟前
洛谷 B3637 最长上升子序列 C语言 记忆化搜索->‘正序‘dp
c语言·开发语言·算法
薯条不要番茄酱31 分钟前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
今天吃饺子36 分钟前
2024年SCI一区最新改进优化算法——四参数自适应生长优化器,MATLAB代码免费获取...
开发语言·算法·matlab
努力进修40 分钟前
“探索Java List的无限可能:从基础到高级应用“
java·开发语言·list
Ajiang28247353043 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空3 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10226 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
----云烟----8 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024068 小时前
SQL SELECT 语句:基础与进阶应用
开发语言