关于后端:整明白-Golang-slice-声明方式浅复制现象深复制append操作

5次阅读

共计 9709 个字符,预计需要花费 25 分钟才能阅读完成。

什么是切片

切片(slice)是对数组一个间断片段的援用。切片是一个援用类型,它理论并不存储元素,它只是标识了数组上的某一个间断片段。

数组在内存中是一连串的内存空间,每个元素占据一块内存。

切片的数据结构是一个构造体,构造体内由三个参数。

  • Pointer 指向数组中它要示意的片段的起始元素;
  • len 长度
  • cap 最大容量
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice 示意图:

申明形式

slice 有 []T{}newmake 三种申明形式。具体有哪些区别将会依据上面实例进行剖析。

sl := []string{"a", "b", "c", "d"}

sl := make([]string, 4)

sl := new([]string)
*sl = make([]string, 4)

浅复制景象

赋值过程中产生的浅复制

来看实例代码

func example1a()  {sl := []string{"a", "b", "c", "d"}
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    // 浅复制 1:赋值过程中产生的浅复制
    sl1 := sl
    fmt.Printf("sl1:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
    sl1[0] = "a 被批改"
    fmt.Println("================ sl1 被批改后 ================")
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    fmt.Printf("sl1:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
}
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl1:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
================ sl1 被批改后 ================
sl:[a 被批改 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl1:[a 被批改 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0

sl 申明失去了一个切片,并在创立了一个数组,sl 切片的外部指针指向这个数组。

sl1 由 sl 赋值而来,sl1 失去了一个和 sl 一样的切片,同样它的外部指针也指向最后创立的数组。

当对 sl1 的索引 0 进行批改后,打印 sl 对应的元素值也将发生变化。

通常,在没有理解切片构造的开发者,会误以为 sl1 与 sl 是齐全独立,相互的批改并不影响对方。实际上,它们的确是两个齐全独立的内存,然而它们的内部结构都指向了同一个数组。

切片并不存储数组元素,它只是 搬运工,标识了数组上的片段区间。

所以,sl1[0] 的批改实际上是批改的 sl1 索引 0 对应的在数组上的元素值。当拜访 sl 时,它读取本人在数组上的片段时,也将受到影响。

这一景象也被称之为 浅复制

函数形参中产生的浅复制

浅复制 不只产生在变量赋值过程中,在调用函数实参传给形参的时候也在悄悄产生。

func example1b()  {sl := []string{"a", "b", "c", "d"}
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    // 浅复制 2:函数形参中产生的浅复制
    func (slParam []string) {fmt.Printf("slParam:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", slParam, slParam, &slParam)
        slParam[0] = "a 被批改"
        fmt.Println("================ slParam 被批改后 ================")
        fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
        fmt.Printf("slParam:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", slParam, slParam, &slParam)
    }(sl)
    // 内部的 sl 也将受到变动
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
slParam:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
================ slParam 被批改后 ================
sl:[a 被批改 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
slParam:[a 被批改 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc0000040c0
sl:[a 被批改 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078

在函数外部对形参切片的批改,将影响函数外实参。看过实例 1a 后,置信你对于这个后果并不会太震惊。

切片实参和形参是两个不同变量,但它们领有同样的内部结构,内部结构中的指针仍然是别离指向数组。

深复制操作

实例 1a 和 1b 中展现了切片的浅复制景象,对于如何解决浅复制问题在本例中将会解答。

func example3()  {sl := []string{"a", "b", "c", "d"}
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    // 深复制:通过 copy 解决赋值过程中产生的浅复制
    sl2 := make([]string, 4)
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    copy(sl2, sl)
    fmt.Println("================ copy 复制后 ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    sl2[0] = "a 被批改了"
    fmt.Println("================ sl2 被批改后 ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:[ ] 变量(或变量构造某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
================ copy 复制后 ================
sl2:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ sl2 被批改后 ================
sl2:[a 被批改了 b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc0000200c0 变量地址:0xc0000040c0
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078

本例中通过 copy 办法 深复制 操作解决了赋值过程中的 浅复制 景象。sl2 和 sl 将是两个齐全不同的切片,并且其外部指针也将指向两个不同的数组。这样,一方的批改就不会影响另一方了。

append 操作

本例中展现了 append 操作。

func example4()  {sl := []string{"a", "b", "c", "d"}
    sl2 := sl
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))

    fmt.Println("================ 数组每个元素对应的地址 ================")
    fmt.Printf("a:%p b:%p c:%p d:%p \n", &sl[0], &sl[1], &sl[2], &sl[3])
    sl2 = sl2[1:2]
    fmt.Println("================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)

    fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
    sl2 = append(sl2[:1], "e")
    fmt.Println("================ 切片还有闲暇容量进行 append e ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    sl2 = append(sl2, "f")
    fmt.Println("================ 切片还有闲暇容量进行 append f  ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
    sl2 = append(sl2, "g")
    fmt.Println("================ 切片没有闲暇容量进行 append g  ================")
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    fmt.Println("================ 产生扩容后 ================")
    fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    sl2 = sl2[:6]
    fmt.Printf("sl2:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl2, sl2, &sl2)
    fmt.Println("================ 新数组每个元素对应的地址 ================")
    fmt.Printf("b:%p c:%p e:%p f:%p \n", &sl2[0], &sl2[1], &sl2[2], &sl2[3])
}
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004090
sl2:cap:4,len:4
================ 数组每个元素对应的地址 ================
a:0xc000020080 b:0xc000020090 c:0xc0000200a0 d:0xc0000200b0 
================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================
sl2:[b] 变量(或变量构造某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl2:cap:3,len:1
================ 切片还有闲暇容量进行 append e ================
sl2:[b e] 变量(或变量构造某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl:[a b e d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ 切片还有闲暇容量进行 append f  ================
sl2:[b e f] 变量(或变量构造某个指针)指向地址(变量值):0xc000020090 变量地址:0xc000004090
sl:[a b e f] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl2:cap:3,len:3
================ 切片没有闲暇容量进行 append g  ================
sl2:[b e f g] 变量(或变量构造某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
sl:[a b e f] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
================ 产生扩容后 ================
sl2:cap:6,len:4
sl2:[b e f g] 变量(或变量构造某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
sl2:[b e f g] 变量(或变量构造某个指针)指向地址(变量值):0xc00004e060 变量地址:0xc000004090
================ 新数组每个元素对应的地址 ================
b:0xc00004e060 c:0xc00004e070 e:0xc00004e080 f:0xc00004e090 

1. 最后切片刚创立的时候,sl、sl2 切片外部指针指向数组第一个元素 a。

2. 通过 sl2 = sl2[1:2] 后,sl2 指向了数组中的第二个 b 元素。

3. 往 sl2 切片 append e 时,此时 sl2 还有闲暇空间(cap-len>0),append 操作间接批改了数组元素 c => e。

4. 往 sl2 切片 append f 时,此时 sl2 仍然还有闲暇空间(cap-len>0),append 操作间接批改了数组元素 d => f。

5. 往 sl2 切片 append g 时,此时 sl2 曾经没有闲暇空间了(cap-len=0),append 操作会导致扩容。因为数组空间是固定不变的,扩容将使 sl2 指向新的数组。sl2 第一个元素依然是 b,但它指向地址曾经不再是最后数组中元素 b 的地址了,这一点能够证实产生了扩容,并产生了新数组。

实际上 sl2 仅须要 4 个空间,对应的新数组却提供了 6 个空间,至于这点应该和切片的扩容机制无关,后续文章可能会持续深入探讨。

其余几种 slice 申明和操作形式

&[]T

sl := &[]string{"a", "b", "c", "d"}
// 等价于
s := []string{"a", "b", "c", "d"}
sl := &s

sl 将失去的是指向切片的地址,它是一个指针,指向切片,而切片外部指针指向数组。

func example2()  {sl := &[]string{"a", "b", "c", "d"}
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    sl1 := sl
    fmt.Printf("sl1:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
    *sl1 = append(*sl1, "e")
    fmt.Println("================ append 后 ================")
    fmt.Printf("sl1:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    (*sl1)[0] = "a 被批改"
    fmt.Println("================ sl1 被批改后 ================")
    fmt.Printf("sl1:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl1, sl1, &sl1)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
sl:&[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
sl1:&[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
================ append 后 ================
sl1:&[a b c d e] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
sl:&[a b c d e] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ sl1 被批改后 ================
sl1:&[a 被批改 b c d e] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006038
sl:&[a 被批改 b c d e] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028

make

make 形式创立切片,make 初始化了数组空间大小,元素初始值默认为零值。

func example5()  {sl := make([]string, 4)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    sl[0] = "a"
    sl[1] = "b"
    sl[2] = "c"
    sl[3] = "d"
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    sl = append(sl, "e", "f", "g", "h")
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
sl:[ ] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl:[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000020080 变量地址:0xc000004078
sl:[a b c d e f g h] 变量(或变量构造某个指针)指向地址(变量值):0xc00010a000 变量地址:0xc000004078

new

new 创立切片将返回地址,sl 此时拿到的仅是地址,切片对应的数组甚至都没有初始化,此时无奈应用这个切片。

直到通过*sl = make([]string, 4),之后能力失常通过指针操作切片。

func example6()  {sl := new([]string)
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
    // new 只拿到了一个指针,并没法应用这个 slice,必须通过 make 初始化后,能力应用
    *sl = make([]string, 4)
    fmt.Println("================ make 后 ================")
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    (*sl)[0] = "a"
    (*sl)[1] = "b"
    (*sl)[2] = "c"
    (*sl)[3] = "d"
    fmt.Println("================ 赋值后 ================")
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)

    *sl = append(*sl, "b")
    fmt.Println("================ append 后 ================")
    fmt.Printf("sl:%+v 变量(或变量构造某个指针)指向地址(变量值):%p 变量地址:%p\n", sl, sl, &sl)
}
sl:&[] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ make 后 ================
sl:&[ ] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ 赋值后 ================
sl:&[a b c d] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028
================ append 后 ================
sl:&[a b c d b] 变量(或变量构造某个指针)指向地址(变量值):0xc000004078 变量地址:0xc000006028

总结

  1. slice 有 []T{}newmake 三种申明形式。
  2. slice 会在变量赋值时产生浅复制。
  3. copy() 能够让 slice 进行深复制。
  4. append 再操作切片时,切片闲暇容量有余时会产生扩容。

end!

文章来自 整明确 Golang slice 申明形式、浅复制景象、深复制、append 操作 | 猴子星球 |Mr-houzi

正文完
 0