美文网首页首页推荐程序员
Go 中 slice 的那些事

Go 中 slice 的那些事

作者: HenryCheng | 来源:发表于2017-11-08 16:40 被阅读1096次
    Go

    一、定义

    我们都知道在 Go 语言中,数组的长度是不可变的,那么为了更加灵活的处理数据,Go 提供了一种功能强悍的类型切片(slice),slice 可以理解为 “动态数组”。但是 slice 并不是真正意义上的动态数组,而是一个引用类型。slice 总是指向一个底层 array,slice 的声明也可以像 array 一样,只是不需要长度。slice 的声明和数组类似,如下

    var iSlice []int
    

    这里的声明和数组一样,只是少了长度,注意两者的比较

    //声明一个保存 int 的 slice
    var iSlice []int
    
    //声明一个长度为 10 的 int 数组
    var iArray [10]int
    
    

    还有一种声明的方法是使用 make() 函数,如下

    slice1 := make([]int, 5, 10)
    

    用 make() 函数创建的时候有三个参数,make(type, len[, cap]) ,依次是类型、长度、容量。

    slice1
    如图所示,上图表示创建了 slice1 ,长度是 5,默认的值都是 0,容量是 10,这样声明就开辟了一块容量是 10
    的连续的一块内存。当然如果我们不指定容量也是可以的,如下
    slice2 := make([]int, 5)
    

    这样就会根据实际情况动态分配内存,而不是最开始指定一块固定大小的内存。需要注意的是我们一般使用 make() 函数来创建 slice,因为我们可以指定 slice 的容量,这样在最开始创建的时候就分配好空间,避免数据多次改变导致多次重新改变 cap 分配空间带来不必要的开销。

    二、slice 的特性

    关于 slice 的一些基本特性,《Go Web 编程》 这本书里已经讲的很详细,有对基本知识不清楚的童鞋可以去补习一下,这里就不一一叙述了。我么来看一个例子,

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        aSlice := []int{1, 2, 3, 4, 5}
        fmt.Printf("aSlice length = %d, cap = %d, self = %v\n", len(aSlice), cap(aSlice), aSlice)
        aSlice = append(aSlice, 6)
        fmt.Printf("aSlice length= %d, cap = %d, self = %v", len(aSlice), cap(aSlice), aSlice)
    }
    

    这个时候我们运行,控制台打印


    我们会看到 aSlice 进行 append 操作以后,它的容量增加了一倍,cap 并没有变成我们想象中的 6 ,而是变成了 10
    aSlice
    如果我们最开始 slice 的容量是 10,长度是 5 ,那么再加一个元素是不会改变切片的容量的。也就是说,当我们往 slice中增加元素超过原来的容量时,slice 会自增容量,当现有长度 < 1024 时 cap 增长是翻倍的,当超过 1024,cap 的增长是 1.25 倍增长。我们来看一下 slice.go 的源码会发现有这样一个函数,里面说明了 cap 的增长规则
    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 {
                // Check 0 < newcap to detect overflow
                // and prevent an infinite loop.
                for 0 < newcap && newcap < cap {
                    newcap += newcap / 4
                }
                // Set newcap to the requested cap when
                // the newcap calculation overflowed.
                if newcap <= 0 {
                    newcap = cap
                }
            }
        }
    /**
        ....省略....
    **/
    }
    

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

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

    来看一个例子,

    package main
    
    import "fmt"
    
    func main() {
    
        aSlice := make([]int, 3, 5)
        bSlice := append(aSlice, 1, 2)
        fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
        fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
        aSlice[0] = 6
        fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
        fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))
    
    }
    
    

    我们会看到控制台输出

    print
    变化过程如下图所示
    slice变化
    上面说明,在 slice 的 cap 范围内增加元素, slice 只会发生 len 的变化不会发生 cap 的变化,同样也说明 slice 实际上是指向一个底层的数组,当多个 slice 指向同一个底层数组的时候,其中一个改变,其余的也会跟着改变,这里需要注意一下。我们同样从 slice.go 的源码中 slice 的定义可以看出,
    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }
    

    这里关于底层的东西就不多叙述,有兴趣的可以看看 一缕殇流化隐半边冰霜 冰霜的 深入解析 Go 中 Slice 底层实现 这篇文章,对 slice 的底层实现的讲解。接下来我们把上面的代码改变一下

    package main
    
    import "fmt"
    
    func main() {
    
        aSlice := make([]int, 3, 5)
        bSlice := append(aSlice, 1, 2, 3, 4)
        fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
        fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
        aSlice[0] = 6
        fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
        fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))
    
    }
    

    我们可以看到下面的输出


    改变后的 print

    上面代码可以用下图说明


    slice append
    也就是说,当 append 的数据超过原来的容量以后,就会重新分配一块新的内存,并把原来的数据 copy 过来,并且保留原来的空间,供原来的 slice(aSlice) 使用这样 aSlice 和 bSlice 就各自指向不同的地址,当 aSlice 改变时,bSlice 不会改变。
    关于 cap 还有一点需要注意,我们来用一个例子说明
    package main
    
    import "fmt"
    
    func main() {
    
        Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
        Slice_a := Array_a[2:5]
        Slice_b := Slice_a[6:7]
        fmt.Printf("Slice_a %v , cap = %d, len = %d\n", string(Slice_a), cap(Slice_a), len(Slice_a))
        fmt.Printf("Slice_b %v , cap = %d, len = %d\n", string(Slice_b), cap(Slice_b), len(Slice_b))
    }
    

    控制台打印


    reslice print

    这里我们会发现 Slice_b 对 Slice_a 进行重新切片后,并没有报错,而是还有输出,这是因为 Slice_a 的 cap 是 8 ,并不是我们想象的 3,slice 指向的是一块连续的内存,所以 Slice_a 的容量其实是一直到 Array_a 的最后的。所以这里 Array_b 对 Array_a 进行切片后会得到值,《Go Web 编程》 上这张图形象的解释了对数组的切片结果,这里是需要注意的一个点。

    slice

    三、关于 copy

    我们来看下面代码

    package main
    
    import "fmt"
    
    func main() {
    
        aSlice := []int{1, 2, 3}
        bSlice := []int{4, 5, 6, 7, 8, 9}
        copy(bSlice, aSlice)
        fmt.Println(aSlice, bSlice)//[1 2 3] [1 2 3 7 8 9]
           //如果是 copy( aSlice, bSlice) 则结果是 [4 5 6] 
    }
    

    也就是说 copy() 函数有两个参数,一个是 to 一个是 from,就是将第二个 copy 到第一个上面,如果第一个长度小于第二个,那么就会 copy 与第一个等长度的值,如 copy( aSlice, bSlice) 的结果是 [4 5 6] ,反之则是短的覆盖长的前几位。当然我们也可以指定复制长度

    package main
    
    import "fmt"
    
    func main() {
    
        aSlice := []int{1, 2, 3}
        bSlice := []int{4, 5, 6, 7, 8, 9}
        copy(bSlice[2:5], aSlice)
        fmt.Println(aSlice, bSlice)//[1 2 3] [4 5 1 2 3 9]
    }
    

    关于 slice 的 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
        }
    
        if raceenabled {
            callerpc := getcallerpc()
            pc := funcPC(slicecopy)
            racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
            racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
        }
        if msanenabled {
            msanwrite(to.array, uintptr(n*int(width)))
            msanread(fm.array, uintptr(n*int(width)))
        }
    
        size := uintptr(n) * width
        if size == 1 { // common case worth about 2x to do here
            // TODO: is this still worth it with new memmove impl?
            *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
        } else {
            memmove(to.array, fm.array, size)
        }
        return n
    }
    

    我们看源码接着往下看会发现这样一个方法

    func slicestringcopy(to []byte, fm string) int {
        if len(fm) == 0 || len(to) == 0 {
            return 0
        }
    
        n := len(fm)
        if len(to) < n {
            n = len(to)
        }
    
        if raceenabled {
            callerpc := getcallerpc()
            pc := funcPC(slicestringcopy)
            racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
        }
        if msanenabled {
            msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
        }
    
        memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
        return n
    }
    

    我们会发现这个函数的两个参数分别是 []byte 和 string ,这里其实是 Go 实现了一个将 string 复制到 []byte 上的方法,这个方法有什么用,我们来看个例子

    package main
    
    import "fmt"
    
    func main() {
    
        s := "hello"
        c := []byte(s) // 将字符串 s 转换为 []byte 类型
        c[0] = 'c'
        s2 := string(c) // 再转换回 string 类型
        fmt.Printf("%s\n", s2)
        fmt.Printf("s-%x, c-%x, s2-%x", &s, &c, &s2)
    }
    

    控制台输出

    字符串改变
    在 Go 中字符串是不可以改变的,我们可以用上面的方法来改变字符串,这里可以看到是实现了 string 和 []byte 的互相转换,达到了修改 string 的目的。我们去看看 string.go 的源码会发现,有下面的方法
    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
    }
    
    

    可以看到上面有个 copy(b, s) ,这里就是将 string 复制到 []byte 上,在 slice.go 已经实现过了的。从源码中我们也可以看出每次 b 都是重新分配的,然后将 s 复制 给 b,从我们上面程控制台输出也可以看到每次地址都有变化,所以说 string 和 []byte 的相互转换是有内存开销的,不过对于现在的机器来说,这点开销也不算什么。

    最后,这是我学习 Go 的 slice 的一些理解与总结,由于能力有限,如果有理解不到位的地方,可以随时留言与我交流。

    参考:
    1、build-web-application-with-golang
    2、go

    相关文章

      网友评论

        本文标题:Go 中 slice 的那些事

        本文链接:https://www.haomeiwen.com/subject/tpbdmxtx.html