文档章节

深入理解go的slice和到底什么时候该用slice?

sheepbao
 sheepbao
发布于 2016/06/27 19:06
字数 1573
阅读 172
收藏 0

深入理解go的slice和到底什么时候该用slice

前言

用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特性可以很容易的切割slice,它们是怎么实现这些特性的呢?现在我们来探究一下这些特性的本质是什么。

先了解一下slice的特性

  • 定义一个slice
    s := []int{1,2,3,4,5}
    fmt.Println(s)  // [1 2 3 4 5]

一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

  • slice的扩容
    s := []int{1,2,3,4,5}
    s = append(s, 6)
    fmt.Println(s)  // [1 2 3 4 5 6]

内置append函数在现有数组的长度 < 1024 时 cap 增长是翻倍的,再往上的增长率则是 1.25,至于为何后面会说。

  • slice的切割
    s := []int{1,2,3,4,5,6}
    s1 := s[0:2]
    fmt.Println(s1)  // [1 2]
    s2 := s[4:]
    fmt.Println(s2)  // [5 6]
    s3 := s[:4]
    fmt.Println(s3)  // [1 2 3 4]
  • slice作为函数参数
    package main

    import "fmt"

    func main() {

        slice_1 := []int{1, 2, 3, 4, 5}
        fmt.Printf("main-->data:\t%#v\n", slice_1)
        fmt.Printf("main-->len:\t%#v\n", len(slice_1))
        fmt.Printf("main-->cap:\t%#v\n", cap(slice_1))
        test1(slice_1)
        fmt.Printf("main-->data:\t%#v\n", slice_1)

        test2(&slice_1)
        fmt.Printf("main-->data:\t%#v\n", slice_1)

    }

    func test1(slice_2 []int) {
        slice_2[1] = 6666               // 函数外的slice确实有被修改
        slice_2 = append(slice_2, 8888) // 函数外的不变
        fmt.Printf("test1-->data:\t%#v\n", slice_2)
        fmt.Printf("test1-->len:\t%#v\n", len(slice_2))
        fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2))
    }

    func test2(slice_2 *[]int) { // 这样才能修改函数外的slice
        *slice_2 = append(*slice_2, 6666)
    }

结果:

main-->data:    []int{1, 2, 3, 4, 5}
main-->len: 5
main-->cap: 5
test1-->data:   []int{1, 6666, 3, 4, 5, 8888}
test1-->len:    6
test1-->cap:    12
main-->data:    []int{1, 6666, 3, 4, 5}
main-->data:    []int{1, 6666, 3, 4, 5, 6666}

这里要注意注释的地方,为何slice作为值传递参数,函数外的slice也被更改了?为何在函数内append不能改变函数外的slice?要回da这些问题就得了解slice内部结构,详细请看下面.

slice的内部结构

其实slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定义:

struct    Slice
{    // must not move anything
    byte*    array;        // actual data
    uintgo    len;        // number of elements
    uintgo    cap;        // allocated number of elements
};

这个结构有3个字段,第一个字段表示array的指针,就是真实数据的指针(这个一定要注意),所以才经常说slice是数组的引用,第二个是表示slice的长度,第三个是表示slice的容量,注意:len和cap都不是指针

现在就可以解释前面的例子slice作为函数参数提出的问题: 函数外的slice叫slice_1,函数的参数叫slice_2,当函数传递slice_1的时候,其实传入的确实是slice_1参数的复制,所以slice_2复制了slise_1,但要注意的是slice_2里存储的数组的指针,所以当在函数内更改数组内容时,函数外的slice_1的内容也改变了。在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。

append的运作机制

在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:

  • 如果新的slice大小是当前大小2倍以上,则大小增长为新大小
  • 否则循环以下操作:如果当前slice大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。
  • append的实现只是简单的在内存中将旧slice复制给新slice

至于为何会这样,你要看一下golang的源码就知道了: https://github.com/golang/go/blob/master/src/runtime/slice.go

    newcap := old.cap
    if newcap+newcap < cap {
        newcap = cap
    } else {
        for {
            if old.len < 1024 {
                newcap += newcap
            } else {
                newcap += newcap / 4
            }
            if newcap >= cap {
                break
            }
        }
    }

为何不用动态链表实现slice?

  • 首先拷贝一断连续的内存是很快的,假如不想发生拷贝,也就是用动态链表,那你就没有连续内存。此时随机访问开销会是:链表 O(N), 2倍增长块链 O(LogN),二级表一个常数很大的O(1)。问题不仅是算法上开销,还有内存位置分散而对缓存高度不友好,这些问题i在连续内存方案里都是不存在的。除非你的应用是狂append然后只顺序读一次,否则优化写而牺牲读都完全不 make sense. 而就算你的应用是严格顺序读,缓存命中率也通常会让你的综合效率比拷贝换连续内存低。
  • 对小 slice 来说,连续 append 的开销更多的不是在 memmove, 而是在分配一块新空间的 memory allocator 和之后的 gc 压力(这方面对链表更是不利)。所以,当你能大致知道所需的最大空间(在大部分时候都是的)时,在make的时候预留相应的 cap 就好。如果所需的最大空间很大而每次使用的空间量分布不确定,那你就要在浪费内存和耗 CPU 在 allocator + gc 上做权衡。
  • Go 在 append 和 copy 方面的开销是可预知+可控的,应用上简单的调优有很好的效果。这个世界上没有免费的动态增长内存,各种实现方案都有设计权衡。

什么时候该用slice?

在go语言中slice是很灵活的,大部分情况都能表现的很好,但也有特殊情况。 当程序要求slice的容量超大并且需要频繁的更改slice的内容时,就不应该用slice,改用list更合适。

© 著作权归作者所有

sheepbao

sheepbao

粉丝 7
博文 18
码字总数 16873
作品 1
深圳
后端工程师
私信 提问
深入理解go的slice和到底什么时候该用slice

前言 用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特...

sheepbao
09/29
0
0
《Golang 疑问解答一》Go语言参数传递是传值还是传引用

对于了解一门语言来说,会关心我们在函数调用的时候,参数到底是传的值,还是引用? 其实对于传值和传引用,是一个比较古老的话题,做研发的都有这个概念,但是可能不是非常清楚。对于我们做...

IT--小哥
2018/06/06
422
3
golang cannot assign to XXX 问题分析

今天在编译golang项目时,遇到了一个错误。编译器提示 原项目太大了,不贴了代码大体是这样的 编译器提示,不能取到m[1][1]的地址。 但是使用 fmt 能打印出数值 打印结果 想了一下,go中的数组...

紫葡萄0
08/19
0
0
Go 中 slice 的那些事

一、定义 我们都知道在 Go 语言中,数组的长度是不可变的,那么为了更加灵活的处理数据,Go 提供了一种功能强悍的类型切片(slice),slice 可以理解为 “动态数组”。但是 slice 并不是真正...

HenryCheng
2017/11/08
0
0
java mongo3.0 $slice怎么用

在管道aggregate的$project里使用老是提示无效的操作$slice,$slice到底怎么使用,在哪可以用

kurumi
2015/08/28
121
0

没有更多内容

加载失败,请刷新页面

加载更多

redis 不同数据结构的使用场景?

1. string string 类型也就是 key-value 类型 常用命令:get、set、incr 应用场景:string 是最常用的一种数据类型 2. list 常用命令:lpush,rpush,brpop,blpop 应用场景:作为消息队列,因为...

happywe
21分钟前
3
0
PG jdbc

import java.sql.DriverManager;import java.sql.Connection;import java.sql.SQLException;import java.sql.ResultSet;import java.sql.Statement; public class PG{ public stat......

MtrS
25分钟前
2
0
Java工程师学习指南(中级篇)

Java工程师学习指南 中级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好。原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我写...

Java技术江湖
36分钟前
2
0
java 三元表达式

例子:C=A>B ? 100 :200; 这条语句的意思是,如果A>B的话,就将100赋给C,否则就将200赋给C;

无名氏的程序员
48分钟前
6
0
针对回流和重绘的渲染优化--公司分享

如果是你,你会如何实现浏览器内核,你认为的浏览器渲染的流程是怎么样的 工作开发中,你有做过哪些关于性能优化的工作(代码),或者目前的业务中有哪些是可以做优化的 浏览器渲染机制 什么...

莫西摩西
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部