自学内容网 自学内容网

go语言 数组和切片

Array(数组)

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

数组的注意事项

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

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

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

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

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

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

  2. [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
}
}
}



从切片中删除元素

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

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


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

指针和引用:切片是引用类型,删除元素的过程通常通过新切片引用来实现,并不会改变原切片本身的内存结构。
性能考虑:在删除大量元素时,要考虑性能,循环和过滤可能会导致较大开销,如果只需要删除一个元素,使用简单的切割方式会更高效。
负索引检查:确保在进行删除操作时对索引或元素值进行有效性检查,避免运行时错误。


原文地址:https://blog.csdn.net/gopher9511/article/details/142407200

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!