Go切片与技巧(附图解)

Posted 太白技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go切片与技巧(附图解)相关的知识,希望对你有一定的参考价值。

你好,我是太白,今天和你聊聊Go语言的切片(Slice)以及它的一些技巧。

如果觉得文章对你有帮助,记得点赞+收藏+关注+转发。谢谢!

  • 1 前言

  • 2 数组

  • 3 切片的创建

  • 4 切片操作

  • 5 切片表达式

  • 1、简单的表达式

  • 2、扩展表达式

  • 6 切片技巧

  • Cut

  • Cut(GC)

  • Delete

  • Delete(GC)

  • Delete without preserving order

  • Delete without preserving order(GC)

  • Expand

  • Extend

  • Filter (in place)

  • Insert

  • InsertVector

  • Push

  • Pop

  • Push Front/Unshift

  • Pop Front/Shift

  • 7 切片额外技巧

  • Filtering without allocating

  • Reversing

  • Shuffling

  • Batching with minimal allocation

  • In-place deduplicate (comparable)

  • Move to front, or prepend if not present, in place if possible.

  • Sliding Window

  • 总结

  • 切片是Go语言中最多被使用的数据结构之一。本文将介绍切片的创建、操作、表达式以及使用的技巧。

    slice类型是建立在 Go 的数组类型之上的抽象,因此要理解 slice,我们必须首先理解数组。

    先看一段代码:

    完成了数组的初始化,表示一个由四个整数组成的数组。
  • 2、默认值为0。当我们打印a[1]的时候输出0。
  • 3、设置数组索引的值,可以直接指定下标的索引进行赋值,例子中是a[0] = 1
  • 3、Go数组的索引和其他语言一样,从0开始,数组的大小是固定的,上面例子中最大是4的的大小,所以下标索引最大是3,所以当我们访问a[4]会报数组越界的错误。
  • Go的数组是值类型。不像C语言数组变量是指向第一个元素的指针,所以当我们把数组变量传递或者赋值的时候,其实是做copy的操作。比如下面的例子a赋值给b,修改a中的元素,并没有影响b中的元素:

    由于数组需要固定长度,很多时候不是很灵活,而切片更灵活、更强大。切片包含对底层数组的引用,如果将一个切片分配给另一个切片,则两者都引用同一个数组。

    切片底层的数据结构(src/runtime/slice.go):

    内置函数append()用于向切片中追加元素。

    Go语言提供了两种切片的表达式:

  • 1、简单表达式
  • 2、扩展表达式
  • Go在Github的官方Wiki上介绍了切片的技巧

  • 1、Go切片本质上是一个结构体,保存了其长度、底层数组的容量、底层数组的指针。
  • 2、Go切片创建方式比较多样:变量声明切片字面量make创建new创建从切片/数组截取
  • 3、Go切片使用len()计算长度、cap()计算容量、append()来添加元素。
  • 4、Go切片相比数组更灵活,有很多技巧,也正因为灵活,容易发生类似内存泄露的问题,需要注意。
  • 参考资料
    [1]

    Go Slices: usage and internals: https://go.dev/blog/slices-intro

    [2]

    Effective Go: https://go.dev/doc/effective_go#arrays

    [3]

    go1.2#three_index: https://tip.golang.org/doc/go1.2#three_index

    [4]

    SliceTricks: https://github.com/golang/go/wiki/SliceTricks

    [5]

    Go Slice Tricks Cheat Sheet: https://ueokande.github.io/go-slice-tricks/

    [6]

    Shuffle: https://pkg.go.dev/math/rand#Shuffle

    你知道的Go切片扩容机制可能是错的

    你好,我是太白,今天和你探索下Go语言的切片扩容机制。

    如果觉得文章对你有帮助,记得点赞+关注。目前本公众号没有评论功能,有问题请联系我。

  • 1 前言

  • 2 代码片段1

  • 3 代码片段2

  • 4 growslice源代码

  • 5 代码片段3

  • 6 go1.18beta1的变化

  • 7 总结

  • 上一篇《Go切片与技巧(附图解)》,我们讲到了Go内置函数append操作,当append操作的时候,切片容量如果不够,会触发扩容。

    关于Go切片的扩容机制,网上文章很多,很多结论是这样的:

    结论1:

  • 1、当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
  • 2、当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
  • 结论2:

  • 在结论1的基础上(切片的预估容量阶段),提到了内存对齐,容量计算完了后还要考虑到内存的高效利用,进行内存对齐。
  • 其中结论1,可能最让人熟知。

    本文将和你一起来探索下这个切片的扩容机制,看看到底是不是这样的。

    我们来看下下面的这端代码,执行结果是6,为什么不是8呢?这个和前言提到的结论1貌似有冲突。

    为了解释上面的代码片段2,我们开看下go的runtime帮我们干了什么。

    我们可以通过delve来调式我们的Go代码。这边演示的Go版本:1.16.5

    通过打断点b main.main、然后用si指令定位到以下代码,runtime.growslice()正式Go扩容的源代码。

    带入变量变成capmem = roundupsize(5 * 8),所以主要是roundupsize影响了最终的结果。

    通过debug,我们发现执行到了return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])

    特别提下,下面两端切片类型不同,扩容的结果也不同,大家可以自行debug下,所以前言提到的结论1中翻倍的结论也是有问题的。

    上面分析是基于go1.16.5的,太白注意到go1.18之后,growslice改了

    1024变成了256,公式也改了,newcap += newcap / 4变成了newcap += (newcap + 3*threshold) / 4,这边我就不展开了。给大家贴下代码:

  • 1、前言中的结论1讲法不严谨,讲对了一部分(代码片段1可知),不同的切片类型,扩容值可能是不同的(代码片段3可知),Go的runtime分配内存的时候,会调用roundupsize,取整内存值(代码片段2可知)。
  • 2、前言中的结论2比结论1好一点,但它是基于结论1的,随着Go版本的迭代,这个结论也会过时。
  • 3、Go切片的扩容机制还是比较复杂的,受到Go版本、操作系统、数据类型等因数,其机制都会有不同,要具体情况具体分析,不要单一的记下一个结论就觉得ok。
  • 4、网上的文章参差不齐,大家在搜资料的时候,需要我们去甄别,最好自己去验证下。
  • 读完文章如果对你有帮助,记得关注下方

    以上是关于Go切片与技巧(附图解)的主要内容,如果未能解决你的问题,请参考以下文章

    21个超实用的 CSS 技巧分享(附图示)

    《Go语言精进之路》读书笔记 | 了解切片实现原理并高效使用

    Go语言中其他数据与字符串类型的转换

    Go语言中其他数据与字符串类型的转换

    请问股市行情的涨幅%数值前面的 "R"字符,"·" 红点 代表啥意思? 见附图

    一文掌握使用 Go 标准库 sort 对切片进行排序