Golang-常见数据结构Slice

news2024/11/24 8:53:53

Slice

slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。

了解 slice 的本质, 最简单的方法就是看它的源码:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
}

slice 共有三个属性:

  1. 指针 指向底层数组
  2. 长度 表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度
  3. 容量 底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

注意: 底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

slice 创建

方式代码示例
直接声明var slice []int
newslice := *new([]int)
字面量slice := []int{1,2,3,4}
makeslice := make(int[], 5, 10)
从切片或者数组"截取"slice := array[1:5] 或 slice := sourceSlice[1:5]

直接声明

第一种创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true

这里比较混淆的是empty slice,它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false
16818915555725

创建方式nil切片空切片
方式一var s1 []intvar s2 = []int{}
方式二var s4 = *new([]int)var s3 = make([]int, 0)
len00
cap00
和 nil 比较truefalse

nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。

关于nil slice和empty slice的探索可以参考 - 深度解析 Go 语言中「切片」的三种特殊状态

字面量

比较简单,直接用初始化表达式创建。

package main

import "fmt"

func main() {
    s1 := []int{0, 1, 2, 3, 8: 100}
    fmt.Println(s1, len(s1), cap(s1))
}

运行结果:
[0 1 2 3 0 0 0 0 100] 9 9

唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值

make

make 函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。

使用 make 关键字创建 slice:

packge main
import "fmt"

func main(){
    // 长度为 5, 容量为 10
    slice := make(int[], 5, 10)
    // 索引为 2 的元素赋值为 2
    slice[2] = 2 
    fmt.Println(slice)
}

截取

截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。

基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。

基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。

值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组

截取操作采用如下方式:

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// data[low, high, max]
slice := data[2:4:6] 

其他易错点

slice 和数组的区别在哪

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

append 到底做了什么

先来看看 append 函数的原型:
func append(slice []Type, elems ...Type) []Type

append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 … 传入 slice,直接追加一个切片。

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

注: append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。

slice 扩容

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

大多数的文章都是这样描述的:

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。

其实结论不太准确的

为了说明上面的规律是错误的,如下代码:

package main

import "fmt"

func main() {
    s := make([]int, 0)

    oldCap := cap(s)

    for i := 0; i < 2048; i++ {
        s = append(s, i)

        newCap := cap(s)

        if newCap != oldCap {
            fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)
            oldCap = newCap
        }
    }
}

运行结果:

[0 ->   -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256 
[0 ->  255] cap = 256   |  after append 256   cap = 512 
[0 ->  511] cap = 512   |  after append 512   cap = 1024
[0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
[0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

在老 slice 容量小于1024的时候,新 slice 的容量的确是老 slice 的2倍。目前还算正确。

但是,当老 slice 容量大于等于 1024 的时候,情况就有变化了。当向 slice 中添加元素 1280 的时候,老 slice 的容量为 1280,之后变成了 1696,两者并不是 1.25 倍的关系(1696/1280=1.325)。添加完 1696 后,新的容量 2304 当然也不是 1696 的 1.25 倍。

可见,现在网上各种文章中的扩容策略并不正确。我们直接搬出源码:源码面前,了无秘密。

从前面汇编代码我们也看到了,向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,所以我们直接看它的代码。

// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
    // ……

    capmem = roundupsize(uintptr(newcap) * ptrSize)
    newcap = int(capmem / ptrSize)
}

看到了吗?如果只看前半部分,现在网上各种文章里说的 newcap 的规律是对的。现实是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。

之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。

为什么 nil slice 可以直接 append

其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice 或 empty slice,然后摇身一变,成为“真正”的 slice 了。

总结

  • 切片是对底层数组的一个抽象,描述了它的一个片段。
  • 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
  • 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
  • append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
  • 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
  • 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。

参考

深度解密Go语言之Slice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/477172.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 一条SQL语句是如何执行的?

总览 ​ 所以今天我们把MySQL拆解一下&#xff0c;看看里边有哪些零件。下边是MySQL的基本架构示意图。 大体来说&#xff0c;MySQL分为Server层和存储引擎两部分。 Server 层包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&am…

小白也能懂的可转债配债价格计算

可转债配债如何计算 先给理论公式&#xff1a; 配债10张/1手所需的钱数 配债所需股数 * 当前的股价 这个公式应该很好理解&#xff0c;不需要做过多的解释。 那&#xff0c; 为什么如此简单的公式&#xff0c;还是很多人不会算&#xff0c;是因为&#xff1a; 配债所需的股数跟…

类与对象之构造函数

文章目录 导读类的6个默认构造函数构造函数概念特性 析构函数概念特性 拷贝构造函数概念特性 赋值运算符重载运算符重载赋值运算符重载前置和后置重载 导读 本文是md导入的可能格式有点乱&#xff0c;希望各位理解一下 类的6个默认构造函数 默认成员函数&#xff1a;用户没有…

五一堵车 | AI“高速”车辆检测轻而易举监测大家安全

点击蓝字关注我们 关注并星标 从此不迷路 计算机视觉研究院 学习群&#xff5c;扫码在主页获取加入方式 计算机视觉研究院专栏 作者&#xff1a;Edison_G 五一节不管是离开小城镇还是进入大城市&#xff0c;每个高速路口都是堵车&#xff0c;现在人工智能愈来愈发达&#xff0c…

git fetch时,FETCH_HEAD和.git\refs\remotes\origin会有哪些变化

目录 github远程仓库状态clone 到本地对新clone的仓库直接 fetchgit fetchgit fetch origingit fetch origin test1git fetch origin test2:test22 结论 github远程仓库状态 clone 到本地 git fetchgit fetch origingit fetch origin test3git fetch origin test2:test22 git f…

Photon AI Translator 和做产品的一些思考

近 4 个月内我一直在做 Apple 平台的产品&#xff0c;虽然从使用量来说「简体中文」用户是占多数&#xff0c;但我一直有做多语言的支持&#xff1a;英语、简体中文和繁体中文。习惯上 Google 翻译的我&#xff0c;基本上在使用 Xcode 过程中也会一直在浏览器开着 Google Trans…

复古决战快速施法穿墙秒怪分析流程及安全防护

《决战》是一款非常古老的RPG游戏&#xff0c;作为热血传奇同期的热门游戏也深受7080后的喜爱。 在十几年前玩这个游戏时&#xff0c;我也使用过瞬移穿墙&#xff0c;快速施法秒怪等功能的辅助。 下面我们就用一个自己架设的单机版来回顾一下当年辅助开发的流程&#xff0c;并…

【三十天精通Vue 3】 第二十二天 Vue 3的UI框架详解

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、常用的Vue 3 UI框架概览1. 常用的Vue 3 UI框架有哪些&…

不同主题增删改查系统【控制台+MySQL】(Java课设)

有很多顾客都是只要实现各种各样的增删改查系统即可&#xff0c;只是主题和数据库表不一样&#xff0c;功能都是增删改查这四个功能&#xff0c;做出来的效果和下面的截图是一样的&#xff0c;后续这样的增删改查系统的运行效果请参考下面的截图&#xff0c;我就不一一演示了&a…

OSPF-MGRE综合实验

拓扑结构&#xff1a; 要求&#xff1a; 1、R6为ISP只能配置IP地址&#xff0c;R1~R5的环回为私有网段 2、R1/4/5为全连的MGRE结构&#xff0c;R1/2/3为星型的拓扑结构&#xff0c;R1为中心站点 3、所有私有网段可以互相通讯&#xff0c;私有网段使用OSPF协议完成 使用的设备…

【Java笔试强训 13】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;参数解析…

忆暖行动|“ 还可以留一点做成柿饼,做法也很简单,就是挑硬柿子把皮削掉,用开水烫个几秒”

追忆过往 感恩现在 我们知道&#xff0c;现在的生活与之前相比发生了翻天覆地的变化&#xff0c;您觉得有什么变化呢&#xff1f; 现在的生活好啊&#xff0c;家家房子都盖起来了&#xff0c;你瞅我这房子&#xff0c;是我子女们大前年给我盖的&#xff0c;我原来都是住的土房…

【Unity-UGUI控件全面解析】| Image 图片组件详解

🎬【Unity-UGUI控件全面解析】| Image 图片组件详解一、组件介绍二、组件属性面板2.1 Image Type三、代码操作组件四、组件常用方法示例4.1 简易血条制作4.2 简易技能冷却条制作五、组件相关扩展使用5.1 Mask 遮罩💯总结🎬 博客主页:https://xiaoy.blog.csdn.net 🎥 本…

Java每日一练(20230430)

目录 1. 文本左右对齐 &#x1f31f;&#x1f31f;&#x1f31f; 2. 求素数和 &#x1f31f; 3. 整数转换英文表示 &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专…

mycat的基本介绍及安装

海量数据存储解决方案之分库分表原理解析及mycat安装及使用_已经存在大量数据 可以使用mycat分表吗_踩踩踩从踩的博客-CSDN博客 Mycat核心概念工作原理及高级特性分析_mycat-mini-monitor_踩踩踩从踩的博客-CSDN博客 前言 在之前的文章中&#xff0c;介绍了一部分的mycat&am…

Web App Manager - 将任何网站转换为应用程序

Web App Manager - 将任何网站转换为应用程序 WebApp Manager 是一款实用程序&#xff0c;由 Linux Mint 和 Peppermint 基于 Peppermint 的 ICE 合作创建——用户可以使用该应用程序将他们喜欢的应用程序转换为独立的 Web 应用程序&#xff0c;它最早于 2010 年首次发布&…

CKA/CKS/CKAD认证考试攻略

什么是CKA考试&#xff1f; CKA认证考试是由Linux基金会和云原生计算基金会(CNCF)创建的&#xff0c;以促进Kubernetes生态系统的持续发展。该考试是一种远程在线、有监考、基于实操的认证考试&#xff0c;需要在运行Kubernetes的命令行中解决多个任务。CKA认证考试是专为Kube…

vue---组件基本知识

目录 一、组件基础 二、Props组件交互 三、自定义组件交互 一、组件基础 对于组件&#xff0c;我个人的理解是每个网页其实都是由一个个组件组成的&#xff0c;它可以理解成网页元素的组成单位&#xff0c;下面我们来看下如何将组件加载到页面中。 &#xff08;1&#xff09…

120 · 单词接龙

链接&#xff1a;LintCode 炼码 class Solution { public:/*** param start: a string* param end: a string* param dict: a set of string* return: An integer*/int ladderLength(string &start, string &end, unordered_set<string> &dict) {// write y…

基于springcloud微服务的java课程资源在线学习考试系统

在我国&#xff0c;由于计算机与网络技术的不断发展&#xff0c;信息化建设的不断深入&#xff0c;不管是企业、学校或个人都在结合计算机网络技术队现有的管理或生活中的一些环节进行开发研究&#xff0c;运用计算机进行一些必要的数据信息管理&#xff0c;分析及发布&#xf…