一文详解Golang中的切片数据类型

 更新时间:2022年09月05日 14:27:38   作者:7small7  
这篇文章主要介绍了一文详解Golang中的切片数据类型,切片是一个种特殊的数组。是对数组的一个连续片段的引用,所以切片是一个引用类型

含义

切片是一个种特殊的数组。是对数组的一个连续片段的引用,所以切片是一个引用类型。切片可以是数组中的一部分,也可以是由起始和终止索引标识的一些项的子集。切片有点像C语言里的指针,指针可以做运算,但代价是内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

定义

切片定义分为三中形式。依次从数组中生成、从切片中生成和全新定义一个切片。

三个要素

1.起始位置:切片引用数组的开始位置。

2.大小:切片中的元素个数。切片中的大小不能超过容量数量。可以使用len()函数对切片统计大小。

3.容量:切片最大可存的元素个数。如果空间不足以容纳足够多的元素,切片就会进行动态“扩容”,此时新切片的长度会发生改变。一般切片的扩容是按照扩容前容量的2倍。可以使用cap()函数对切片容量进行统计。

切片与数组的区别

  • 切片是对数组中的连续引用。切片的初始位置指向数组的内存地址,如果切片的值改变,数组对应的值也会对应改变。
  • 切片的长度是动态的,本质上是一个可变的动态数组。数组的长度在定义的时候就决定好了,后期是无法修改数组的长度的。
  • 切片的长度是可以动态扩容的[如上面容量一次提到的]。
  • 切片本身是不保存数据,它只是底层数组的表示。对切片所做的任何修改都将反应到底层数组中。
package main
import (
	"fmt"
)
func main() {
	numa := [3]int{78, 79, 80}
	nums1 := numa[:]
	nums2 := numa[:]
	fmt.Println("array before change 1", numa)
	nums1[0] = 100
	fmt.Println("array after modification to slice nums1", numa)
	nums2[1] = 101
	fmt.Println("array after modification to slice nums2", numa)
}
// output
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]

当多个切片共享一个底层数组时,每个切片的修改都将反映在底层数组中。

示例代码

// 通过数组定义切片
var array1 = [3]int{1, 1, 3}
fmt.Println("数组的元素分别是:", array1)
slice1 := array1[0:2]
slice1[1] = 11111
fmt.Println("数组的元素分别是:", array1)

输出结果;

数组的元素分别是: [1 1 3]
数组的元素分别是: [1 11111 3]

切片内存分布

#yyds干货盘点#带大家认识Golang中的切片数据类型_数据类型

切片定义分类

数组生成切片

定义语法:

slice[起始位置:结束位置]

  • 1.slice:表示切片的对象。例如从一个数组中生成切片则slice就是定义的数组名称。
  • 2.起始位置:从数组中的某个元素的下标开始切,默认中0开始。
  • 3.结束位置:切片的结束位置。也就是数组的某个元素下标位置。<font color=“red”>需要注意的是这里取的是开区间</font>。如果需要取到数组的最后一个元素,结束位置这是数组的长度+1。
  • 4.切片的长度:(切片的结束位置-切片的起始位置)。

示例代码

// 通过数组定义切片
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(0:5)
// 打印结果为
[A B C D E]

// 使用make方式创建切片
slice1 := make([]string, 2, 3)
slice1[0] = "1"
fmt.Println(slice1)

slice2 := make([]string, 2, 3)
fmt.Println(slice2)

fmt.Println("切片的长度为", len(slice1))
fmt.Println("切片的容量为", cap(slice1))
// output
[1 ]
[ ]
切片的长度为 2
切片的容量为 3

切片索引

1.切片的起始位置省略,结束位置省略。则默认从数组的开始位置截取到数组的结束位置+1。得到的是和数组内容一样的切片,表示原切片。

// 切片定义省略开始和结束位置
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("开始位置和结束位置都缺省",array[:])
// output
开始位置和结束位置都缺省 [A B C D E F G H I G K L]

2.切片的起始位置不省略,结束位置不省略。则根据起始位置和结束位置进行切取。

// 起始位置和结束位置都不省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(0:5)
// output
[A B C D E]

3.起始位置省略,结束位置不省略。则默认从数组的最开始位置切取,直到结束位置为止。

// 起始位置省略,结束位置都不省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(:5)
// output
[A B C D E]

4.起始位置不省略,结束位置省略。则默认从数组的指定起始位置窃取到数组的最后以为(位置为数组长度+1)。

// 起始位置不省略,结束位置省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("缺省结束位置:",array[2:])
// 打印结果为
缺省结束位置: [C D E F G H I G K L]

5.切片的起始位置和结束位置,不能超出数组的范围。

array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("切片",array[-1:100])
// 打印结果为
invalid slice index -1 (index must be non-negative)

6.切片的起始位置和结束位置都为0,得到一个空的切片,表示清空切片。一般用于切片的复位。

// 起始位置和结束位置都为0
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("切片",array[0:0])
// 打印结果为
切片: []

直接声明切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明。

定义语法

// 也可以通过一个空的数组形式
var slice []type

1.slice是切片的名称。

2.type是切片的数据类型。

代码示例

// 声明一个整型切片
var slice1 []int
// 初始化一个切片
var slice2 []int = []int{}

使用make定义切片

除了上面的几种方式外,如果需要动态地创建一个切片,可以使用 make() 内建函数。

定义语法:

make([]type, size, cap)

  • 1.type为切片的数据类型。
  • 2.size为切片的大小。
  • 3.cap为切片的容量。

切片的大小不能超过容量,容量表示该切片最大的元素个数,切片的大小表示实际的元素个数。例如,一个教室里面可以坐到30个人,现目前坐了10个人。这里的10就表示size,30就表示cap。

代码示例:

// 定义切片
slice1 := make([]string, 2, 3)
slice1[0] = "1"
fmt.Println(slice1)
// 打印如下结果
[1 ]

如果切片在复制的过程中,对应的下标未分配值,则根据数据类型默认分配一个值。例如上面的slince1定义的时2个长度,但是只给下标为0的分配了值,因此下标为1的根据数据类型时string类型,默认分配一个" "值。

常用操作

长度计算

切片长度使用len()计算。

// 计算切片长度
slice1 := make([]string, 2, 3)
fmt.Println("切片的长度为", len(slice1))
// 打印结果为
切片的长度为 2

容量计算

切片容量使用cap()计算。

// 计算切片长度
slice1 := make([]string, 2, 3)
fmt.Println("切片的长度为", cap(slice1))
// 打印结果为
切片的容量为 3

判断是否为空

在创建变量章节提到, 变量如果创建时未给一个初始化值,编译时会默认分配一个nil的值。因此判断一个切片为空,直接与nil比较。

// 判断空切片
slice3 := make([]string, 2, 3)
if slice3 == nil {
	fmt.Println("slice3是空切片")
} else {
	fmt.Println("slice3不是空切片")
}

var slice4 []string
if slice4 == nil {
	fmt.Println("slice4是空切片")
} else {
	fmt.Println("slice4不是空切片")
}
// output
slice3不是空切片
slice4是空切片
// 错误演示
slice1 := make([]int, 2, 5)
slice2 := make([]int, 2, 5)
if slice1 == slice2 {
	fmt.Println("相等")
} else {
	fmt.Println("不想等")
}
// output
slice1 == slice2 (slice can only be compared to nil)

使用make创建切片时,因为定义了一个长度为2,容量为3的切片。虽然切片内容是[ ],但是实际是有值的,只不过是一个空值。切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

切片追加

追加的定义:

使用append()可以动态的给切片的开始位置,结束位置或者中间位置添加元素。

语法格式

append(slice, element)

1.slice,要追加的切片,必须是一个切片。

2.element,向切片中添加的元素,可以是单个元素、多个元素或着切片。

尾部追加

// 切片的开始位置追加元素
var slice []int = []int {1,2,3}

// 打印原始切片的长度和容量
fmt.Println("原始切片长度和容量分别是", len(slice), cap(slice))

// 向切片后面追加一个元素
slice = append(slice, 1)
fmt.Println(slice)

// 向切片后面追加多个元素
slice = append(slice, 6,7,8,9)
fmt.Println(slice)

// 打印新的切片的长度和容量
fmt.Println("新切片长度和容量分别是", len(slice), cap(slice))
// 打印的内容分别如下
原始切片长度和容量分别是 3 3
[1 2 3 1]
[1 2 3 1 6 7 8 9]
新切片长度和容量分别是 8 12

注意事项:

  • 1.在切片的尾部添加元素,只能是单个元素或者是多个","隔开的元素,而不能是其他的数据类型。
  • 2.如果切片追加元素时,容量不够,切片会自动的扩容。自动扩容的规律是2的倍数。如下代码:
// 验证切片追加元素自动扩容
var numbers []int
for i := 0; i < 10; i++ {
    numbers = append(numbers, i)
    fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
}

// output
len: 1  cap: 1 pointer: 0xc0420080e8
len: 2  cap: 2 pointer: 0xc042008150
len: 3  cap: 4 pointer: 0xc04200e320
len: 4  cap: 4 pointer: 0xc04200e320
len: 5  cap: 8 pointer: 0xc04200c200
len: 6  cap: 8 pointer: 0xc04200c200
len: 7  cap: 8 pointer: 0xc04200c200
len: 8  cap: 8 pointer: 0xc04200c200
len: 9  cap: 16 pointer: 0xc042074000
len: 10  cap: 16 pointer: 0xc042074000

开始位置追加

// 向切片的开始位置追加元素
var slice = []int {1,2,3}
slice = append([]int {0}, slice...) // 在开头添加只有1个元素的切片
slice = append([]int {-3,-2,-1}, slice...) // 在开头添加拥有多个元素的切片
fmt.Println(slice)

// output
[-3 -2 -1 0 1 2 3]

  • 1.在切片的开始位置添加元素,将添加的元素作为append()的第一个参数,第二个参数为原始的切片,需要在原始切片后加"…"。
  • 2.append()的第一个参数必须是切片。
  • 3.在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

中间位置追加

// 向切片的中间追加元素
var slice2 = []int {1,2,3,7,8,9}
slice2 = append(slice2[0:3], append([]int {4,5,6},slice2[3:]...)...)
fmt.Println(slice2)

// output
[1 2 3 4 5 6 7 8 9]

  • 1.向切片的中间追加元素基本格式为<kbd>append(a[:i], append([]int{x}, a[i:]…)…) // 在第i个位置插入x</kbd>
  • 2.每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

复制

复制的定义:

语言的内置函数, copy()可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy( destSlice, srcSlice []T) int

其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且<font color=“red”>来源和目标的数据类型必须一致</font>,copy() 函数的返回值表示实际发生复制的元素个数。

示例代码:

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
fmt.Println(slice2)
// output
[1 2 3]
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
fmt.Println(slice1)
// output
[5 4 3 4 5]

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。示例代码如下:

// 通过循环的方式演示切片
slice1 := make([]int, 2, 110)
slice2 := make([]int, 2, 110)
slice1[0] = 1
slice1[1] = 2
slice2[0] = 5
fmt.Println(slice1)// [1 2]
fmt.Println(slice2)// [5 0]


// 将切片1复制到切片2
for i := 0; i < 2; i++ {
		slice2[i] = slice1[i]
}
fmt.Println(slice2)// [1 2]
// 将切片2复制到切片1
for i := 0; i < 2; i++ {
		slice1[i] = slice2[i]
}
fmt.Println(slice1)// [5 0]

引用和复制

package main
import "fmt"
func main() {
    // 设置元素数量为1000
    const elementCount = 1000
    // 预分配足够多的元素切片
    srcData := make([]int, elementCount)
    // 将切片赋值
    for i := 0; i < elementCount; i++ {
        srcData[i] = i
    }
    // 引用切片数据
    refData := srcData
    // 预分配足够多的元素切片
    copyData := make([]int, elementCount)
    // 将数据复制到新的切片空间中
    copy(copyData, srcData)
    // 修改原始数据的第一个元素
    srcData[0] = 999
    // 打印引用切片的第一个元素
    fmt.Println(refData[0])
    // 打印复制切片的第一个和最后一个元素
    fmt.Println(copyData[0], copyData[elementCount-1])
    // 复制原始数据从4到6(不包含)
    copy(copyData, srcData[4:6])
    for i := 0; i < 5; i++ {
        fmt.Printf("%d ", copyData[i])
    }
}

执行逻辑:

第 8 行,定义元素总量为 1000。
第 11 行,预分配拥有 1000 个元素的整型切片,这个切片将作为原始数据。
第 14~16 行,将 srcData 填充 0~999 的整型值。
第 19 行,将 refData 引用 srcData,切片不会因为等号操作进行元素的复制。
第 22 行,预分配与 srcData 等大(大小相等)、同类型的切片 copyData。
第 24 行,使用 copy() 函数将原始数据复制到 copyData 切片空间中。
第 27 行,修改原始数据的第一个元素为 999。
第 30 行,引用数据的第一个元素将会发生变化。
第 33 行,打印复制数据的首位数据,由于数据是复制的,因此不会发生变化。
第 36 行,将 srcData 的局部数据复制到 copyData 中。
第 38~40 行,打印复制局部数据后的 copyData 元素。

切片的复制,是在内存另外的分配,将被分配的空间分配到目标空间。原空间发生变化,新分配的空间则不会受影响。切片的引用则会收到影响。

切片的删除

切片本身不带删除的函数操作。只能使用切片自身的特性来进行操作。删除切片有如下三中情况,删除开头,删除结尾,删除中间。

删除开头

// 删除切片开头元素
// 1.使用切片的截取方法
slice = []int{1, 2, 3}
slice = slice[1:] // 删除开头1个元素
slice = slice[N:] // 删除开头N个元素

// 2.使用切片中的append()函数
slice = []int{1, 2, 3}
slice = append(slice[:0], slice[1:]...) // 删除开头1个元素
slice = append(slice[:0], slice[N:]...) // 删除开头N个元素

// 3.使用切片的copy()函数
slice = []int{1, 2, 3}
slice = slice[:copy(slice, slice[1:])] // 删除开头1个元素
slice = slice[:copy(slice, slice[N:])] // 删除开头N个元素

使用append()函数,不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)。

删除中间

// 删除中间
slice = []int{1, 2, 3, ...}
slice = append(slice[:i], slice[i+1:]...) // 删除中间1个元素
slice = append(slice[:i], slice[i+N:]...) // 删除中间N个元素
slice = slice[:i+copy(slice[i:], slice[i+1:])] // 删除中间1个元素
slice = slice[:i+copy(slice[i:], slice[i+N:])] // 删除中间N个元素

删除结尾

// 删除结尾
slice = []int{1, 2, 3}
slice = slice[:len(slice)-1] // 删除尾部1个元素
slice = slice[:len(slice)-N] // 删除尾部N个元素

指定位置

// 删除切片的指定位置
 seq := []string{"a", "b", "c", "d", "e"}
// 指定删除位置
index := 2
// 查看删除位置之前的元素和之后的元素
fmt.Println(seq[:index], seq[index+1:])
// 将删除点前后的元素连接起来
seq = append(seq[:index], seq[index+1:]...)
fmt.Println(seq)

排序

// 整型排序
sli := []int{1, 5, 3, 4}
sort.Ints(sli)
for index, value := range sli {
fmt.Println(index, value)
}
fmt.Println("---------------")
// 字符串排序
sliStr :=[]string{"lisi", "zhangsan", "bruce"}
sort.Strings(sliStr)
fmt.Println(sliStr)
fmt.Println("---------------")
// 浮点型排序
sliFloat := []float64{12.56, 12.12}
sort.Float64s(sliFloat)
fmt.Println(sliFloat)

// output
0 1
1 3
2 4
3 5
---------------
[bruce lisi zhangsan]
---------------
[12.12 12.56]

迭代器

Go语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的每一个元素,如下所示:

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}
// output
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40

到此这篇关于一文详解Golang中的切片数据类型的文章就介绍到这了,更多相关Golang切片内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go设计模式之模板方法模式讲解和代码示例

    Go设计模式之模板方法模式讲解和代码示例

    模版方法是一种行为设计模式, 它在基类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤,本文将通过代码示例给大家详细的介绍一下Go模板方法模式,需要的朋友可以参考下
    2023-08-08
  • golang gorm开发架构及写插件示例

    golang gorm开发架构及写插件示例

    这篇文章主要为大家介绍了golang gorm开发架构及写插件的详细示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 详解Go使用Viper和YAML管理配置文件

    详解Go使用Viper和YAML管理配置文件

    在软件开发中,配置管理是一项基本但至关重要的任务,它涉及到如何有效地管理应用程序的配置变量,本文将探讨如何使用Viper库配合YAML配置文件来实现高效的配置管理,感兴趣的可以了解下
    2024-04-04
  • 浅析Golang中Gin框架存在的必要性

    浅析Golang中Gin框架存在的必要性

    在Go语言中,net/http 包提供了一个强大且灵活的标准HTTP库,那为什么还出现了像 Gin 这样的,方便我们构建Web应用程序的第三方库,下面就来和大家简单分析一下
    2023-08-08
  • 详解Go并发编程时如何避免发生竞态条件和数据竞争

    详解Go并发编程时如何避免发生竞态条件和数据竞争

    大家都知道,Go是一种支持并发编程的编程语言,但并发编程也是比较复杂和容易出错的。比如本篇分享的问题:竞态条件和数据竞争的问题
    2023-04-04
  • Go语言 go程释放操作(退出/销毁)

    Go语言 go程释放操作(退出/销毁)

    这篇文章主要介绍了Go语言 go程释放操作(退出/销毁),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 一文详细介绍golang中.()类型断言的使用方法

    一文详细介绍golang中.()类型断言的使用方法

    Golang是一门非常流行的编程语言,在很多领域都有着广泛的应用,在开发过程中,很多时候我们需要将函数作为参数传递给其他函数,这时候就需要用到golang中的.()用法,本文将详细介绍golang中.()的使用方法,需要的朋友可以参考下
    2023-08-08
  • Golang Time包与日期函数的用法详解

    Golang Time包与日期函数的用法详解

    在golang中,time包提供了时间的显示和测量用的函数,下面小编就来和大家详细聊聊Golang中Time包与日期函数的具体用法,快跟随小编一起学习一下吧
    2023-07-07
  • golang如何让string转int64

    golang如何让string转int64

    这篇文章主要介绍了golang如何让string转int64问题,
    2024-02-02
  • Go语言实现Fibonacci数列的方法

    Go语言实现Fibonacci数列的方法

    这篇文章主要介绍了Go语言实现Fibonacci数列的方法,实例分析了使用递归和不使用递归两种技巧,并对算法的效率进行了对比,需要的朋友可以参考下
    2015-02-02

最新评论