深入理解 Go 数组、切片、字符串

news2024/9/9 6:37:54

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!

alt

原文链接:深入理解 Go 数组、切片、字符串 欢迎点赞关注

前言

为什么在一篇文章里同时介绍数组、切片、字符串,了解这三个数据类型底层数据结构的同学一定知道,在go中这三个数据类型,底层有一定联系,切片和字符串底层都是基于数组实现的,字符切片和字符串之间还可以相互转换。

alt

数组

类型

数组是一种复合类型,具体类型是由:数组的长度 + 数组元素的类型决定的,下面的是两种数组类型:

[10]int // 长度为10的int数组
[11]int // 长度为11的int数组

只有长度和元素类型都相同才是同一类型。

访问

我们通过指向数组开头的指针、元素的数量以及元素类型占的空间大小表示数组。如果我们不知道数组中元素的数量,访问时可能发生越界;而如果不知道数组中元素类型的大小,就没有办法知道应该一次取出多少字节的数据,无论丢失了哪个信息,我们都无法知道这片连续的内存空间到底存储了什么数据:

alt

访问索引为1的元素只需要数组的起始地址偏移1个元素大小即可:P + 1 * size。

越界检查

Go 语言中可以在编译期间的静态类型检查判断数组越界,cmd/compile/internal/gc.typecheck1 会验证访问数组的索引:

func typecheck1(n *Node, top int) (res *Node) {
        switch n.Op {
        case OINDEX:
                ok |= ctxExpr
                l := n.Left  // array
                r := n.Right // index
                switch n.Left.Type.Etype {
                case TSTRING, TARRAY, TSLICE:
                        ...
                        if n.Right.Type != nil && !n.Right.Type.IsInteger() {
                                yyerror("non-integer array index %v", n.Right)
                                break
                        }
                        if !n.Bounded() && Isconst(n.Right, CTINT) {
                                x := n.Right.Int64()
                                if x < 0 {
                                        yyerror("invalid array index %v (index must be non-negative)", n.Right)
                                } else if n.Left.Type.IsArray() && x >= n.Left.Type.NumElem() {
                                        yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, n.Left.Type.NumElem())
                                }
                        }
                }
        ...
        }
}
  1. 访问数组的索引是非整数时,报错 “non-integer array index %v”;

  2. 访问数组的索引是负数时,报错 “invalid array index %v (index must be non-negative)";

  3. 访问数组的索引越界时,报错 “invalid array index %v (out of bounds for %d-element array)";

除了上面的检查外,在生成SSA期间还会插入越界检查指令,我们编写如下代码,然后生成SSA中间代码:

package check

func outOfRange() int {
        arr := [3]int{123}
        i := 4
        elem := arr[i]return elem
}

$ GOSSAFUNC=outOfRange go build array.go
dumped SSA to ./ssa.html

ssa.html 中间代码如下:

b1:...
    v22 (6) = LocalAddr <*[3]int> {arr} v2 v20
    v23 (6) = IsInBounds <bool> v21 v11 // 判断idx是否小于len
If v23 → b2 b3 (likely) (6// 如果小于执行 b2,否则(越界)执行 b3

b2: ← b1-
    v26 (6) = PtrIndex <*int> v22 v21
    v27 (6) = Copy <mem> v20
    v28 (6) = Load <int> v26 v27 (elem[int])...
Ret v30 (+7)

b3: ← b1-
    v24 (6) = Copy <mem> v20
    v25 (6) = PanicBounds <mem> [0] v21 v11 v24 // panic 退出
Exit v25 (6)

切片

Go 语言切片可以看做对数组的封账,Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

数据结构

编译期间的切片是 cmd/compile/internal/types.Slice 类型的,但是在运行时切片可以由如下的 reflect.SliceHeader 结构体表示,其中:

  • Data 是指向数组的指针;

  • Len 是当前切片的长度;

  • Cap 是当前切片的容量,即 Data 数组的大小:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

Data 是一片连续的内存空间,这片内存空间可以用于存储切片中的全部元素,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量的标识。

alt

创建切片

使用make创建Slice

使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

例如,语句slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:

alt

该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可。

使用数组创建Slice

使用数组来创建Slice时,Slice将与原数组共用一部分内存。

例如,语句slice := array[5:7]所创建的Slice,结构如下图所示:

alt

切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5。

数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。

切片扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

例如,当向一个capacity为5,且length也为5的Slice再次追加1个元素时,就会发生扩容,如下图所示:

alt

当切片的容量不足时,我们会调用 runtime.growslice 函数为切片扩容,扩容是为切片分配新的内存空间并拷贝原切片中元素的过程,我们先来看新切片的容量是如何确定的:

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 0 < newcap && newcap < cap {
                                newcap += newcap / 4
                        }
                        if newcap <= 0 {
                                newcap = cap
                        }
                }
        }

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

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;

  2. 如果当前切片的长度小于 1024 就会将容量翻倍;

  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

上述代码片段仅会确定切片的大致容量,下面还需要根据切片中的元素大小对齐内存,当数组中元素所占的字节大小为 1、8 或者 2 的倍数时,运行时会使用如下所示的代码对齐内存:

var overflow bool
    var lenmem, newlenmem, capmem uintptr
    switch {
    case et.size == 1:
            lenmem = uintptr(old.len)
            newlenmem = uintptr(cap)
            capmem = roundupsize(uintptr(newcap))
            overflow = uintptr(newcap) > maxAlloc
            newcap = int(capmem)
    case et.size == sys.PtrSize:
            lenmem = uintptr(old.len) * sys.PtrSize
            newlenmem = uintptr(cap) * sys.PtrSize
            capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
            overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
            newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
            ...
    default:
            ...
    }

runtime.roundupsize 函数会将待申请的内存向上取整,取整时会使用 runtime.class_to_size 数组,使用该数组中的整数可以提高内存的分配效率并减少碎片,我们会在内存分配一节详细介绍该数组的作用:

var class_to_size = [_NumSizeClasses]uint16{
    0,
    8,
    16,
    32,
    48,
    64,
    80,
    ...,
}

下面举个例子,说明一下扩容和内存对齐的效果:

var arr []int64
arr = append(arr, 12345)

fmt.Println(cap(arr)) // 6

上面代码里会触发 runtime.growslice 函数扩容 arr 切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于 sys.PtrSize,所以运行时会调用 runtime.roundupsize 向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6。

切片copy

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值,例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素,也就是说,copy过程中不会发生扩容。

func slicecopy(to, fm slice, width uintptr) int {
        if fm.len == 0 || to.len == 0 {
                return 0
        }
        n := fm.len
        if to.len < n {
                n = to.len
        }
        if width == 0 {
                return n
        }
        ...

        size := uintptr(n) * width
        if size == 1 {
                *(*byte)(to.array) = *(*byte)(fm.array)
        } else {
                memmove(to.array, fm.array, size)
        }
        return n
}

其他知识点

nil 切片和空切片

// nil切片
var s1 []int
    
// nil切片指针
s2 := new([]int)

// 空切片
s3 := make([]int0)
s4 := []int{}

sh1 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
sh3 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s3)))           

用debug看一下s1 s2 s3 s4的内存情况:

alt

通过debug可以发现,nil切片的data指针指向的是nil,空切片的data指针指向的是空数组:

alt

切片作为函数参数

在Go函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了slice结构体对象,两个slice结构体的字段值均相等。正常情况下,由于函数内slice结构体的array和函数外slice结构体的array指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。

func main() {
    arr := []int{1,2,3}
    update(arr)
    fmt.Println(arr[1]) // 100
}

func update(arr []int) {
    arr[1] = 100
}

但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。

func main() {
    arr := []int{1,2,3}
    update(arr)
    fmt.Println(len(arr)) // 3
}

func update(arr []int) {
    arr = append(arr, 4)
    fmt.Println(len(arr)) // 4
}

字符串

Go标准库builtin给出了所有内置类型的定义。 源代码位于src/builtin/builtin.go,其中关于string的描述如下:

// string is the set of all strings of 8-bit bytes, conventionally but not// necessarily representing UTF-8-encoded text. A string may be empty, but// not nil. Values of string type are immutable.type string string
解释

所以string是8比特字节的集合,通常但并不一定是UTF-8编码的文本。

另外,还提到了两点,非常重要:

  • string可以为空(长度为0),但不会是nil;

  • string对象不可以修改。

数据结构

字符串在 Go 语言中的接口其实非常简单,每一个字符串在运行时都会使用如下的 reflect.StringHeader 表示,其中包含指向字节数组的指针和数组的大小:

type StringHeader struct {
    Data uintptr
    Len  int
}

因为字符串作为只读的类型,我们并不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。

字符串拼接

Go 语言拼接字符串会使用 + 符号,底层实现是 runtime.concatstrings,它会先对遍历传入的切片参数,再过滤空字符串并计算拼接后字符串的长度。

func concatstrings(buf *tmpBuf, a []string) string {
        idx := 0
        l := 0
        count := 0
        for i, x := range a {
                n := len(x)
                if n == 0 {
                        continue
                }
                l += n
                count++
                idx = i
        }
        if count == 0 {
                return ""
        }
        if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
                return a[idx]
        }
        s, b := rawstringtmp(buf, l)
        for _, x := range a {
                copy(b, x)
                b = b[len(x):]
        }
        return s
}

拼接的过程:

  1. 通过遍历计算新字符串的长度

  2. 生成新字符串

  3. 在通过遍历,将原字符串拷贝到新字符串中

字节串与字符数组互转

从字节数组到字符串的转换需要使用 runtime.slicebytetostring 函数,例如:string(bytes),该函数在函数体中会先处理两种比较常见的情况,也就是长度为 0 或者 1 的字节数组,这两种情况处理起来都非常简单:

func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
        l := len(b)
        if l == 0 {
                return ""
        }
        if l == 1 {
                stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
                stringStructOf(&str).len = 1
                return
        }
        var p unsafe.Pointer
        if buf != nil && len(b) <= len(buf) {
                p = unsafe.Pointer(buf)
        } else {
                p = mallocgc(uintptr(len(b)), nilfalse)
        }
        stringStructOf(&str).str = p
        stringStructOf(&str).len = len(b)
        memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
        return
}

处理过后会根据传入的缓冲区大小决定是否需要为新字符串分配一片内存空间,runtime.stringStructOf 会将传入的字符串指针转换成 runtime.stringStruct 结构体指针,然后设置结构体持有的字符串指针 str 和长度 len,最后通过 runtime.memmove 将原 []byte 中的字节全部复制到新的内存空间中。

当我们想要将字符串转换成 []byte 类型时,需要使用 runtime.stringtoslicebyte 函数,该函数的实现非常容易理解:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
        var b []byte
        if buf != nil && len(s) <= len(buf) {
                *buf = tmpBuf{}
                b = buf[:len(s)]
        } else {
                b = rawbyteslice(len(s))
        }
        copy(b, s)
        return b
}

上述函数会根据是否传入缓冲区做出不同的处理:

  • 当传入缓冲区时,它会使用传入的缓冲区存储 []byte;

  • 当没有传入缓冲区时,运行时会调用 runtime.rawbyteslice 创建新的字节切片并将字符串中的内容拷贝过去;

上面的两种转换方式都进行了内存复制,会存在一些性能问题,还有一种通过unsafe包进行类型转换的方式,不涉及到内存拷贝,但是unsafe包并不推荐在生产环境使用,所以大家使用时要谨慎:

// string转ytes
func Str2sbyte(s string) (b []byte) {
    *(*string)(unsafe.Pointer(&b)) = s        // 把s的地址付给b
    *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*unsafe.Sizeof(&b))) = len(s)        // 修改容量为长度
    return
}

// []byte转string
func Sbyte2str(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

参考

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array

https://books.studygolang.com/GoExpertProgramming/chapter01/1.2-slice.html

本文由 mdnice 多平台发布

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

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

相关文章

【人工智能专栏】Beam Search 束搜索

Beam Search 束搜索 这里是一个 beam_size=2 的Beam Search示意图,每个节点都会扩展5个下级节点,在 Beam Search 每次都会从所有扩展节点里面挑选出2个累计启发值最大的节点,直到达到结束标准。 理念 Beam Search 是对 Greedy Search(贪心搜索)的一个改进算法,能够扩展…

windows常用的dos命令

1.打开dos命令窗口: winr -> 输入cmd -> 回车 进入之后可以看到如下界面 其中 c: 代表盘符users: 代表的是磁盘符目录下的文件夹qayrup lin 是users文件夹下的子文件夹 以上的所有构成了我们当前操作的所在位置 常用的dos命令 作用命令切换盘符盘符名: -> 回车盘…

昇思25天学习打卡营第26天|Diffusion扩散模型

看了这个diffusion扩散模型&#xff0c;不得不感慨现在AI还是很厉害的。从一张包浆的图片&#xff0c;可以还原出来图片本来的面目&#xff0c;甚至可能一张打了马赛克的图片&#xff0c;用AI处理可能也可以还原出来原始图片。攻防战在AI加入战斗后又增加了很多变数。 受限于算…

【Bug收割机】已解决使用maven插件打包成功,在控制台使用mvn命令打包失败问题详解,亲测有效!

文章目录 前言问题分析报错原因解决方法私域 前言 在maven项目中&#xff0c;大家经常会使用maven插件来打包项目文件 但是有的人也习惯使用mvn命令在控制台直接进行打包&#xff0c;因为这样可以自定义组装一些命令&#xff0c;使用起来也更加灵活方便&#xff0c;比如mvn pa…

前端开发实用的网站合集

文章目录 一、技能提升篇vueuseJavaScript中文网JavaScript.infoRxJsWeb安全学习书栈网码农之家 二、UI篇iconfont&#xff1a;阿里巴巴矢量图标库IconPark3dicons美叶UndrawError 404摹克 三、CSS篇You-need-to-know-cssCSS TricksAnimate.cssCSS ScanCSS Filter 四、颜色篇中…

Java真人版猫爪老鼠活动报名平台系统

&#x1f43e;“真人版猫爪老鼠活动报名平台系统”——趣味追逐&#xff0c;等你来战&#xff01;&#x1f42d; &#x1f431;【萌宠变主角&#xff0c;现实版趣味游戏】 厌倦了电子屏幕的虚拟游戏&#xff1f;来试试“真人版猫爪老鼠活动”吧&#xff01;在这个平台上&…

android java socket server端 可以不断的连接断开,不断的收发 TCP转发

adb.exe forward tcp:5902 tcp:5902 前面本地5901 转发到 后面设备为5902查看转发 adb forward --list删除所有转发 adb forward --remove-allpublic static final String TAG "Communicate";private static boolean isEnable;private final WebConfig webConfig;//…

jenkins流水线语法--withCredentials篇

jenkins流水线语法--withCredentials篇 &#xff08;在流水线代码中不显示明文密码&#xff09; 在jenkinsfile中进行harbor登录上传镜像时直接用的密码&#xff0c;在代码中不怎么严谨&#xff0c;也缺失安全性&#xff1b;在网上查找资料和大佬们的博客&#xff0c;得出一篇完…

一起来做几道有趣的概率题

看到一篇叫做《和上帝一起掷骰子》的文章&#xff0c;里面提到了很多概率有关的问题&#xff0c;不少经过计算得出的概率都与人第一看上去产生的直觉大相径庭。所以&#xff0c;人类的直觉往往是靠不住的。 举两个例子&#xff1a; 若1千人中有1人携带hiv病毒&#xff0c;有一种…

电脑卡了怎么办?

在日常使用电脑的过程中&#xff0c;我们可能会遇到各种各样的问题&#xff0c;其中电脑卡顿是很让人心烦的问题之一。电脑卡顿不仅会影响我们的工作效率&#xff0c;还会让人感到非常烦恼。本文将详细介绍电脑卡顿的常见原因及其解决方法&#xff0c;帮助大家轻松应对这一问题…

深入浅出消息队列----【延迟消息的实现原理】

深入浅出消息队列----【延迟消息的实现原理】 粗说 RocketMQ 的设计细说 RocketMQ 的设计这样实现是否有什么问题&#xff1f; 本文仅是文章笔记&#xff0c;整理了原文章中重要的知识点、记录了个人的看法 文章来源&#xff1a;编程导航-鱼皮【yes哥深入浅出消息队列专栏】 粗…

四步教你快速解决UE5文件迁移失败❗️

本期作者&#xff1a;尼克 易知微3D引擎技术负责人 不知道大家在用UE5迁移文件时&#xff0c;有没有发现这个问题&#xff1a;如果文件输出的路径选择了非项目路径&#xff0c;那么UE会提示无法迁移。在UE4中&#xff0c;这样做是不存在问题的&#xff0c;只要选择「忽略」就可…

OS—文件系统

目录 一. 文件系统结构I/O 控制层基本文件系统文件组织模块逻辑文件系统 二. 文件系统布局文件系统在磁盘中的结构主引导记录(MasterBoot Record,MBR)引导块(boot block)超级块(super block)文件系统中空闲块的信息 文件系统在内存中的结构 三. 外存空间管理空闲表法空闲链表法…

关于CDN

CDN 代表内容分发网络&#xff08;Content Delivery Network&#xff09;它是一种通过将内容复制到多个地理位置分散的服务器上&#xff0c;从而加速网络内容传输的技术。CDN 的主要目的是提高用户访问速度、减少延迟和提升网站的可靠性。 具体来说&#xff0c;CDN 通过以下方…

飞创直线模组桁架机械手优势及应用领域

随着工业自动化和智能制造的发展&#xff0c;直线模组桁架机械手极大地减轻了人类的体力劳动负担&#xff0c;在危险性、重复性高的作业环境中展现出了非凡的替代能力&#xff0c;引领着工业生产向自动化、智能化方向迈进。 一、飞创直线模组桁架机械手优势 飞创直线模组桁架…

爬虫问题---ChromeDriver的安装和使用

一、安装 1.查看chrome的版本 在浏览器里面输入 chrome://version/ 回车查看浏览器版本 Chrome的版本要和ChromeDriver的版本对应&#xff0c;否则会出现版本问题。 2.ChromeDriver的版本选择 114之前的版本&#xff1a;https://chromedriver.storage.googleapis.com/index.ht…

mmdetection:用于目标检测、实例分割、全景分割和半监督目标检测的工具包

MMDetection 是一个基于 PyTorch 的开源目标检测工具箱&#xff0c;是 OpenMMLab 项目的一部分。该工具箱采用模块化设计&#xff0c;使用户能够通过组合不同组件轻松构建自定义的目标检测框架。 MMDetection 支持多种检测任务&#xff0c;包括目标检测、实例分割、全景分割和…

【ROS 最简单教程 002/300】ROS 集成开发环境安装: Noetic

&#x1f497; 有遇到安装问题可以留言呀 ~ 当时踩了挺多坑&#xff0c;能帮忙解决的我会尽力 &#xff01; 1. 安装操作系统环境 Linux ❄️ VM / VirtualBox Ubuntu20.04 如果已有 linux 环境 (如双系统等)&#xff0c;可跳过步骤 1 ~ &#x1f449; 保姆级图文安装教程指路…

Python_Flask学习笔记

1.配置 查询字符串的形式传参 app.route(/book/list) def book_list():page request.args.get(page,default1,typeint)return f"您获取的是{page}的图书列表&#xff01;"if __name__ __main__:app.run()3.HTML模版渲染 from flask import Flask,render_templa…