前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言进阶:数组与切片

Go语言进阶:数组与切片

原创
作者头像
windealli
发布2024-03-18 20:35:34
2450
发布2024-03-18 20:35:34
举报
文章被收录于专栏:windealli

一、引言

在Go语言的世界里,数组和切片是构建高效、可靠程序的基石。它们提供了一种强大的方式来组织和管理数据集合,使得数据操作既直观又灵活。本文《Go语言进阶,数组与切片》将带领你深入探索这两种数据结构的内部机制,理解它们的本质区别,以及如何有效地使用它们来提升你的Go编程技能。

二、Array (数组)

1. 数组Array简介

数组Array是编程语言中的常见数据类型,几乎所有的主流编程语言都支持数组Array,Go语言也不例外。

数组 Array 是一片连续的内存区域,存储相同类型的元素,元素的个数固定。 在Go语言中,数组Array不能进行扩容、在复制和传递时为值复制

  • 数组Array声明

Go语言中,数组声明主要有三种方式(其他方式一般为以下三种方式的变种)

代码语言:go
复制
var arr1 [5]int // 默认方式,定义一个长度为5的int类型数组,元素自动初始化为0
var arr2 = [5]int{1, 2, 3, 4, 5} // 声明并初始化一个长度为5的int类型数组
arr3 := [...]int{1, 2, 3, 4, 5} // 使用...让Go自动计算数组长度
  • 数组Array 访问
    • 访问数组元素:通过索引访问数组中的元素,索引从0开始。例如,arr[0]将访问数组的第一个元素。
    • 修改数组元素:通过索引可以直接修改数组中的元素值。例如,arr[1] = 10将把数组的第二个元素设置为10。
    • 遍历数组:可以使用for循环来遍历数组的所有元素。
  • 数组Array 的容量和长度

在Go语言中,数组的长度是固定的,且数组的长度决定了其容量。数组的长度可以通过内置函数len()来获取:

代码语言:go
复制
length := len(arr)

2. 数组Array底层结构和实现原理

数组在编译时构建抽象语法树阶段的数据类型为TARRAY,通过NewArray函数进行创建,AST节点的Op操作为OARRAYLIT

代码语言:go
复制
// NewArray 返回一个固定长度的数组类型对象
func NewArray(elem *Type, bound int64) *Type {

  // 元素的数量不能小于0
	if bound < 0 {
		base.Fatalf("NewArray: invalid bound %v", bound)
	}
	
	t := newType(TARRAY)
	t.extra = &Array{Elem: elem, Bound: bound}
	t.SetNotInHeap(elem.NotInHeap())
	if elem.HasTParam() {
		t.SetHasTParam(true)
	}
	if elem.HasShape() {
		t.SetHasShape(true)
	}
	return t
}

// 数组的底层结构
type Array struct {
	Elem  *Type // 元素类型
	Bound int64 // 元素的数量; <0 if unknown yet, 根据NewArray的实现可以看出不会小于0
}

3. 数组Array的优缺点分析

优点

  1. 类型安全:数组中的所有元素都是同一类型,这有助于确保类型的一致性和安全性。
  2. 内存连续:数组在内存中占用连续的空间,这使得访问数组元素非常高效。
  3. 性能可预测:由于数组的大小是固定的,因此其性能表现是可预测的,没有切片可能带来的扩容开销。

缺点

  1. 固定长度:数组的长度在创建后不能改变,这限制了其灵活性。
  2. 使用不便:在实际编程中,经常需要动态调整集合的大小,而数组无法满足这一需求,因此切片通常更受欢迎。
  3. 传递开销:当数组作为参数传递给函数时,如果数组很大,将发生值的完整复制,可能导致不必要的性能开销。虽然可以通过指针传递数组来避免这个问题,但这增加了代码的复杂性。

三、Slice(切片)

1. Slice(切片)简介

在Go语言中,Slice(切片)是一种非常灵活且强大的数据结构,它是对数组的抽象和扩展。

Slice本身并不存储数据,而是对底层数组的引用,并包含指向数组起始元素的指针、切片长度以及切片的容量等信息。

由于Slice的这种特性,它可以在不改变底层数组的情况下进行动态地增长和缩小,使得在处理可变大小的集合时更加高效和灵活。

  • Slice(切片)声明与初始化

下面是slice的常见声明方式

代码语言:go
复制
slice1 := []int{1, 2, 3} // 声明并初始化Slice
var slice2 []int         // 声明Slice但不分配空间
slice3 := make([]int, 3) // 使用make函数声明Slice
slice4 := make([]int, 3, 5) // 使用make函数声明Slice, 长度为3, 容量为5

2. Slice(切片)的底层数据结构

Slice(切片)的底层数据结构包含三个字段

代码语言:go
复制
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

3. Slice(切片)的截取

和数组一样,切片中的数据仍然是内存中的一片连续区域。

要获取切片某一区域的连续数据,可以通过下标的方式对切片进行截断。要获取切片某一区域的连续数据,可以通过下标的方式对切片进行截断。

代码语言:go
复制
// 切片截取的语法
subSlice := oldSlice[start:end]
  1. 左闭右开:切片的截取是左闭右开的,即包含起始索引的元素,但不包含结束索引的元素。
  2. 容量跟随原数组:如果没有指定新的容量(max),则新切片的容量将跟随原切片的容量。
  3. 底层数组不变:截取操作不会改变底层数组的内容,只是改变了新切片的起始位置、长度和容量。
  4. 索引有效性:必须确保 start 和 end 的值是有效的,即 start 必须小于等于 end,且两者都必须在原切片的索引范围内。

示例:

代码语言:go
复制
    oldSlice := []int{101, 102, 103, 104, 105}  
    // 截取从索引1(包含)到索引3(不包含)的元素  
    newSlice := oldSlice[1:3]  

4. Slice(切片)值复制与数据引用

在Go语言中,slice(切片)本身是一个值类型,但slice的值复制实际上是对底层数组的引用和长度、容量的拷贝,而不是对底层数组元素的完全复制。这意味着当你复制一个slice时,新的slice将引用相同的底层数组,但可以有不同的长度和容量。

代码语言:go
复制
    oldSlice := []int{1, 2, 3, 4, 5}  
  
    // 将原始slice赋值给一个新的slice  
    newSlice := oldSlice  
  
    // 修改新slice的元素  
    newSlice[0] = 100  
  
    // 打印原始slice和新slice  
    fmt.Println(oldSlice) // 输出: [100 2 3 4 5]  
    fmt.Println(newSlice)     // 输出: [100 2 3 4 5]  

5. Slice(切片)收缩与扩容

在Go语言中,Slice(切片)收缩可以通过Slice(切片)的截取来实现。

Go语言内置的append函数可以添加新的元素到切片的末尾,它可以接受可变长度的元素,并且可以自动扩容。如果原有数组的长度和容量已经相同,那么在扩容后,长度和容量都会相应增加。

代码语言:go
复制
package main

import "fmt"

func main() {
	slice := []int{1, 2, 3}
	fmt.Println("原始Slice:", slice, "长度:", len(slice), "容量:", cap(slice))

	// 向Slice添加元素,触发扩容
	slice = append(slice, 4, 5, 6, 7, 8, 9, 10)
	fmt.Println("扩容后的Slice:", slice, "长度:", len(slice), "容量:", cap(slice))
}

输出:

代码语言:go
复制
➜  test  go run main.go
原始Slice: [1 2 3] 长度: 3 容量: 3
扩容后的Slice: [1 2 3 4 5 6 7 8 9 10] 长度: 10 容量: 10
➜  test 

7. Slice(切片)扩容的实现原理

切片使用append函数添加元素,但不是使用了append函数就需要进行扩容,如下代码向长度为3,容量为4的切片a中添加元素后不需要扩容。

代码语言:go
复制
slice := make([]int, 3, 4) // 使用make函数声明Slice, 长度为3, 容量为4

当你向切片中添加元素,而切片的容量不足以容纳更多元素时,Go 会创建一个新的、容量更大的底层数组,并将原有元素复制到新数组中,这个过程即为扩容。

切片扩容的实现原理涉及以下几个步骤:

  1. 计算新容量:当你向切片追加元素时,如果容量不足,Go 会根据当前容量计算一个新的容量。新容量的计算方式通常是将当前容量翻倍,但这不是绝对的。如果当前容量小于1024个元素,那么通常会翻倍;如果大于1024个元素,增长因子会逐渐减小,增长到原来的1.25倍左右。
  2. 分配新数组:根据计算出的新容量,Go 会分配一个新的底层数组。
  3. 复制元素:将原切片中的元素复制到新的底层数组中。
  4. 更新切片数组指针:切片在Go中是由一个结构体表示的,包含指向底层数组的指针array、切片的长度len和容量cap。在扩容后,需要更新这个结构体的信息,指向新的底层数组,并更新长度和容量的值。

8. Slice(切片)的优缺点

优点:

  1. 动态大小:与数组不同,切片的长度是动态的,可以根据需要增长或缩小。这使得切片非常灵活,适用于不确定大小的数据集合。
  2. 内存效率:切片背后是数组,它们可以共享同一个底层数组,这意味着在多个切片之间传递数据时,可以避免数据的复制,提高内存使用效率。
  3. 便捷操作:Go语言为切片提供了许多内置函数,如appendcopy等,使得对切片的操作非常方便。
  4. 引用类型:切片是引用类型,这意味着当你将切片传递给函数或从函数返回切片时,传递的是引用而不是整个数据的副本。
  5. 内置函数支持:Go语言的内置函数可以直接作用于切片,例如len可以获取切片的长度,cap可以获取切片的容量。

缺点:

  1. 非线程安全:切片不是线程安全的,如果在多个goroutine中同时操作同一个切片(特别是进行写操作),可能会导致竞态条件。需要外部同步机制来保证并发安全。
  2. 内存泄漏风险:由于切片是对底层数组的引用,如果切片的某个元素指向了一个大的内存块,即使只有一个小的切片在使用它,整个内存块也不会被垃圾回收,可能导致内存泄漏。
  3. 性能开销:切片的动态扩容可能会导致性能开销,因为每次扩容都需要分配新的数组并复制数据。如果不合理地使用切片,可能会导致频繁的内存分配和复制。
  4. 容量管理:虽然切片的动态扩容提供了便利,但是对于性能敏感的应用,开发者需要仔细管理切片的容量,以避免不必要的扩容操作。
  5. 隐式行为:切片的一些行为可能不是很直观,比如切片的扩容规则、切片之间共享底层数组的行为等,这可能会导致一些难以发现的bug。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言
  • 二、Array (数组)
    • 1. 数组Array简介
      • 2. 数组Array的底层结构和实现原理
        • 3. 数组Array的优缺点分析
        • 三、Slice(切片)
          • 1. Slice(切片)简介
            • 2. Slice(切片)的底层数据结构
              • 3. Slice(切片)的截取
                • 4. Slice(切片)值复制与数据引用
                • 5. Slice(切片)收缩与扩容
                  • 7. Slice(切片)扩容的实现原理
                    • 8. Slice(切片)的优缺点
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档