题目起源: 深服气、知乎、跟谁学
题目解析: GOALNG ROADMAP 社区
答案 1:(溪尾)
数组长度是固定的,而切片是可变长的。能够把切片看作是对底层数组的封装,每个切片的底层数据结构中,肯定会蕴含一个数组。数组能够被称为切片的底层数组,切片也能够被看作对数组某一间断片段的援用。因而,Go 中切片属于援用类型,而数组属于值类型,通过内建函数 len,能够获得数组和切片的长度。通过内建函数 cap,能够失去数组和切片的容量。然而数组的长度和容量是相等的,并且都不可变,而且切片容量是有变化规律的。
答案 2:(行飞子)
数组和切片的关系:
切片一旦初始化,切片始终与保留其元素的根底数组相关联。因而,切片会和与其领有同一根底数组的其余切片共享存储 ; 相比之下,不同的数组总是代表不同的存储。
数组和切片的区别
- 切片的长度可能在执行期间发生变化,而数组的长度不能变动,能够把切片看成一个长度可变的数组。
- 数组作为函数参数是进行值传递的,函数外部扭转传入的数组元素值不会影响函数内部数组的元素值;切片作为函数的参数是进行的指针传递,函数外部扭转切片的值会影响函数内部的切片元素值。
- 数组能够比拟,切片不能比拟(对底层数组的援用)。
答案 3:(栾龙生)
1. Go 切片和 Go 数组
Go 切片,又称动静数组,它理论是基于数组类型做的一层封装。
Go 数组
数组是内置 (build-in) 类型, 是一组同类型数据的汇合,它是值类型,通过从 0 开始的下标索引拜访元素值。在初始化后长度是固定的,无奈批改其长度。当作为办法的参数传入时将复制一份数组而不是援用同一指针。数组的长度也是其类型的一部分,通过内置函数 len(array)获取其长度。
Go 数组与像 C/C++ 等语言中数组略有不同,如下
- Go 中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份。因而,在 Go 中如果将数组作为函数的参数传递的话,那效率就必定没有传递指针高了。
- 数组的长度也是类型的一部分,这就阐明
[10]int
和[20]int
不是同一种数据类型。
Go 切片
Go 语言中数组的长度是固定的,且不同长度的数组是不同类型,这样的限度带来不少局限性。
而切片则不同,切片(slice)是一个领有雷同类型元素的可变长序列,能够不便地进行扩容和传递,理论应用时比数组更加灵便,这也正是切片存在的意义。
切片是援用类型,因而在当传递切片时将援用同一指针,批改值将会影响其余的对象。
2. 切片底层
当初就来看一下 Go 语言切片的底层是什么样子吧!
Go 切片 (slice) 的实现能够在源码包 src/runtime/slice.go
中找到。在源码中,slice 的数据结构定义如下。
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片长度
cap int // 切片容量
}
能够看到,组成 Go 切片的三元组别离为指向底层数组的指针,切片长度和切片容量。
-
指向底层数组的指针
后面曾经提到,切片理论是对数组的一层封装。这个指针便是记录其底层数组的地址,也正是切片开始的地位。
-
切片长度
len
示意切片的长度,即切片中现存无效元素的个数,它不能超过切片的容量。能够通过len()
函数获取切片长度。 -
切片容量
cap
示意切片的容量,即切片能存储元素的多少,通常是从切片的起始元素到底层数组的最初一个元素间的元素个数,当切片容量有余时,便会触发 slice 扩容。能够通过cap()
函数获取切片容量。
下图展现了一个 Go 切片的底层数据结构,这个切片的长度为 3,容量为 6。
3. 切片应用
- 切片定义形式
var a []int //nil 切片,和 nil 相等,个别用来示意一个不存在的切片
var b []int{} // 空切片,和 nil 不相等,个别用来示意一个空的汇合
var c []int{1, 2, 3} // 有 3 个元素的切片,len 和 cap 都为 3
var d = c[:2] // 有 2 个元素的切片,len 为 2,cap 为 3
var e = c[:2:cap(c)] // 有 2 个元素的切片,len 为 2,cap 为 3
var f = c[:0] // 有 0 个元素的切片,len 为 0,cap 为 3
var g = make([]int, 3) // 创立一个切片,len 和 cap 均为 3
var h = make([]int, 3, 6) // 创立一个切片,len 为 3,cap 为 5
var i = make([]int, 0, 3) // 创立一个切片,len 为 0,cap 为 3
-
从数组中切取切片
数组和切片是严密相连的。切片能够用来拜访数组的局部或全副元素,而这个数组称为切片的底层数组。切片的指针指向数组第一个能够从切片中拜访的元素,这个元素并不一定是数组的第一个元素。
一个底层数组能够对应多个切片,这些切片能够援用数组的任何地位,彼此之前的元素能够重叠。
slice 操作符
s[i:j]
创立了一个新的 slice,这个新的 slice 援用了 s 中从 i 到 j-1 索引地位的所有元素。如果表达式省略了 i,那么默认是
s[0:j]
;如果省略了 j,默认是s[i:len(s)]
;
// 示例起源:The Go Programming Language
// 创立一个数组
months := [...]string{1:"January", /*...*/, 12: "December"}
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) //["April" "May" "June"]
fmt.Println(summer) //["June" "July" "August"]
月份名称字符串数组与其对应的两个元素重叠的 slice 图示
留神:切片与原数组或切片共享底层空间,批改切片会影响原数组或切片
-
迭代切片
切片能够用 range 迭代,然而要留神:如果只用一个值接管 range,则失去的只是切片的下标,用两个值接管 range,则失去的才是下标和对应的值。
// 应用一个值接管 range, 则失去的是切片的下标
for i := range months {fmt.Println(i) // 返回下标 0 1 ... 12
}
// 应用两个值接管 range,则失去的是下标和对应的值
for i, v := range months {fmt.Println(i, v) // 返回下标 0 1 ... 12 和 值 """January" ... "December"
}
-
切片拷贝
应用
copy
内置函数拷贝两个切片时,会将源切片的数据一一拷贝到目标切片指向的数组中,拷贝数量取两个切片的最小值。例如长度为 10 的切片拷贝到长度为 5 的切片时,将拷贝 5 个元素。也就是说,拷贝过程中不会产生扩容。
copy 函数有返回值,它返回实际上复制的元素个数,这个值就是两个 slice 长度的较小值。
4. 切片扩容 -append 函数
追加元素
- 通过
append()
函数能够在切片的尾部追加 N 个元素
var a []int
a = append(a, 1) // 追加一个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1, 2, 3}...) // 追加一个切片,留神追加切片时前面要加...
- 应用 append()函数也能够在切片头部增加元素
a = append([]int{0}, a...) // 在结尾增加一个元素
a = append([]int{1, 2, 3}, a...) // 在结尾增加一个切片
注: 从头部增加元素会引起内存的重调配,导致已有元素全副复制一次。因而从头部增加元素的开销要比从尾部增加元素大很多
- 通过 append()函数链式操作从两头插入元素
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第 i 个地位上插入 x
a = append(a[:i], append([]int{1, 2, 3}, a[i:]...)...) // 在第 i 个地位上插入切片
应用链式操作在插入元素,在内层 append 函数中会创立一个临式切片,而后将 a[i:]
内容复制到新创建的临式切片中,再将临式切片追加至 a[:i]
中。
-
通过 append()和 copy()函数组合从两头插入元素
应用这种形式能够防止创立过程两头的临式切片,也能够做到从两头插入元素
// 两头插入一个元素
a = append(a, 0) // 切片扩大一个空间
copy(a[i+1:], a[i:]) //a[i:]向后挪动一个地位
a[i] = x // 设置新增加的元素
// 两头插入多个元素
a = append(a, x...) // 为 x 切片扩大足够的空间
copy(a[i+len(x):], a[i:]) //a[i:]向后挪动 len(x)个地位
copy(a[i:], x) // 复制新增加的切片
应用此形式尽管稍显简单,然而能够缩小创立两头长期切片的开销。
删除元素
很遗憾,Go 语言中并没有提供间接删除指定地位元素的形式。不过依据切片的性质,咱们能够通过奇妙的拼接切片来达到删除指定数据的目标。
a = []int{1, 2, 3}
// 删除尾部元素
a = a[:len(a) - 1] // 删除尾部一个元素
a = a[:len(a) - N] // 删除尾部 N 个元素
// 删除头部元素
a = [1:] // 删除结尾 1 个元素
a = [N:] // 删除结尾 N 个元素
// 删除两头元素
a = append(a[:i], a[i+1:]...) // 删除两头一个元素
a = append(a[:i], a[i+N:]...) // 删除两头 N 个元素
slice 扩容
很多人认为 slice 是能够主动裁减的, 预计都是 append
函数误导的。其实 slice 并不会本人主动裁减, 而是 append
数据时, 该函数如果发现超出了 cap 限度主动帮咱们扩的。
应用 append 向 slice 追加元素时,如果 slice 空间有余,则会触发 slice 扩容,扩容实际上是调配一块更大的内存,将原 slice 的数据拷贝进新 slice,而后返回新 slice,扩容后再将数据追加进去。
例如,当向一个容量为 5 且长度也为 5 的切片再次追加 1 个元素时,就会产生扩容,如下图所示。(示例起源:Go 专家编程)
扩容操作只关怀容量,会把原 slice 的数据拷贝至新 slice 中,追加数据由 append 在扩容后实现。由上图可见,扩容后新 slice 的长度依然是 5,但容量由 5 提到了 10,原 slice 的数据也都拷贝到了新的 slice 指向的数组中。
扩容容量的抉择遵循以下根本规定
- 如果原 slice 的容量小于 1024,则新 slice 的容量将扩充为原来的 2 倍;
- 如果原 slice 的容量大于 1024,则新的 slice 的容量将扩充为原来的 1.25 倍;
5. Go 切片,Python 切片,都是切片,有什么不同?
Go 有切片 slice 类型,Python 有列表和元组,这两种语言都有切片操作。然而它们的切片操作是齐全不同的。
- 最大的不同就是
- Python 的切片产生的是新的 对象,对新对象的成员的操作不影响旧对象;
- Go 的切片产生的是旧对象一部分的 援用,对其成员的操作会影响旧对象;
究其原因还是底层实现不同
- Go 的切片,底层是一个三元组。指针指向一块间断的内存,长度是已有成员数,容量是最大成员数。切片时,个别并不会申请新的内存,而是对原指针进行挪动,而后和新的长度、容量组成一个切片类型值返回。也就是说,Go 的切片操作通常会和生成该切片的切片或数组共享内存。
- Python 的切片,其实就是指针数组。对它进行切片,会创立新的数组。在 Python 的切片中,并没有容量的概念。
这其实也体现了脚本语言和编译语言的不同。尽管两个语言都有相似的切片操作;然而 Python 次要指标是不便;Go 次要指标却是疾速。
- 在应用中,Go 切片和 Python 切片也有很多不同
- 首先,Go 的切片,其成员是雷同类型的,Python 的列表则不限度类型。
- 两种语言都有 [a:b] 这种切片操作,意义也相似,然而 Go 的 a、b 两个参数不能是正数,Python 能够是正数,此时就相当于从开端往前数。
- 两种语言都有
[a:b:c]
这种切片操作,意义却是齐全不同的。Go 中的 c 示意的是 容量 ;而 Python 的 c 示意的是 步长。
6. 切片陷阱
-
无奈做比拟
和数组不同的是,slice 无奈做比拟,因而不能用 == 来测试两个 slice 是否领有雷同的元素。规范库外面提供了高度优化的函数
bytes.Equal
来比拟两个字节 slice。然而对于其它类型的 slice,就必须要本人写函数来比拟。slice 惟一容许的比拟操作是和 nil 进行比拟,例如
if slice == nil {/*...*/}
-
空切片和 nil 切片
空切片和 nil 切片是不同的。
- nil 切片中,切片的指针指向的是空地址,其长度和容量都为零。nil 切片和 nil 相等。
- 空切片,切片的指针指向了一个地址,但其长度和容量也为 0,和 nil 不相等,通常用来示意一个空的汇合。
var s []int // s == nil
var s = nil // s == nil
var s = []int{nil} // s == nil
var s = []int{} // s != nil
s := make([]int,0) // s != nil
-
应用 range 进行切片迭代
当应用 range 进行切片迭代时,range 创立了每个元素的正本,而不是间接返回对该元素的援用。如果应用该值变量的地址作为每个元素的指针,就会造成谬误。
func main() {a := []int{1, 2, 3, 4, 5}
for i, v := range a {
fmt.Printf("Value: %d, v-addr: %X, Elem-addr: %X\n",
v, &v, &a[i])
}
}
# output
Value: 1, v-addr: C0000AA058, Elem-addr: C0000CC030
Value: 2, v-addr: C0000AA058, Elem-addr: C0000CC038
Value: 3, v-addr: C0000AA058, Elem-addr: C0000CC040
Value: 4, v-addr: C0000AA058, Elem-addr: C0000CC048
Value: 5, v-addr: C0000AA058, Elem-addr: C0000CC050
从后果中能够看出,应用 range 进行迭代时,v 的地址是始终不变的,它并不是切片中每个变量的理论地址。而是在应用 range 进行遍历时,将切片中每个元素都复制到了同一个变量 v 中。如果谬误的将 v 的地址当作切边元素的地址,将会引发谬误。
-
切片扩容引发的问题
正因为有扩容机制。所以咱们无奈保障原始的 slice 和用 append 后的后果 slice 指向同一个底层数组,也无奈证实它们就指向不同的底层数组。同样,咱们也无奈假如旧 slice 上对元素的操作会或者不会影响新的 slice 元素。所以,通常咱们将 append 的调用后果再次赋给传入 append 的 slice。
内置 append 函数在向切片追加元素时,如果切片存储容量不足以存储新元素,则会把以后切片扩容并产生一个新的切片。
append 函数每次追加元素都有可能触发切片扩容,即有可能返回一个新的切片,这正是 append 函数申明中返回值为切片的起因,应用时应该总是接管该返回值。
倡议
应用 append 函数时,谨记 append 可能会产生新的切片,并审慎的解决返回值。
-
append 函数误用
应用 append 函数时,须要思考 append 返回的切片是否跟原切片共享底层的数组。上面这段程序片段,来看看函数返回的后果。
// 示例起源:Go 专家编程
func AppendDemo() {x := make([]int, 0, 10)
x = append(x, 1, 2, 3)
y := append(x, 4)
z := append(x, 5)
fmt.Println(x)
fmt.Println(y)
fmt.Println(z)
}
//output
[1 2 3]
[1 2 3 5]
[1 2 3 5]
题目首先创立了一个长度为 0,容量为 10 的切片 x,而后向切片 x 追加了 1,2,3 三个元素。其底层的数组构造如下图所示
创立切片 y 为切片 x 追加一个元素 4 后,底层数组构造如下图所示
须要留神的是切片 x 依然没有变动,切片 x 中记录的长度仍为 3。持续向 x 追加元素 5 后,底层数组构造如下图所示
至此,答案曾经十分明确了。当向 x 持续追加元素 5 后,切片 y 的最初一个元素被笼罩掉了。
此时切片 x 依然为[1 2 3],而切片 y 和 z 则为[1 2 3 5]。
倡议
个别状况下,应用 append 函数追加新的元素时,都会用原切片变量接管返回值来取得更新
a = append(a, elems...)
-
函数传参
Go 语言中将切片作为函数参数传递会有什么神奇的景象,一起来看看上面这个示例。
package main
import "fmt"
func main(){a := []int{1, 2, 3} // 长度为 3,容量为 3
b := make([]int, 1, 10) // 长度为 1,容量为 10
test(a,b)
fmt.Println("main a =", a)
fmt.Println("main b =", b)
}
func test(a,b []int){a = append(a, 4) // 引发扩容,此时返回的 a 是一个新的切片
b = append(b, 2) // 没有引发扩容,依然是原切片
a[0] = 3 // 扭转 a 切片元素
b[0] = 3 // 扭转 b 切片元素
fmt.Println("test a =", a) // 打印函数内的 a 切片
fmt.Println("test b =", b) // 打印函数内的 b 切片
}
//output
test a = [3 2 3 4]
test b = [3 2]
main a = [1 2 3]
main b = [3]
首先,咱们创立了两个切片,a 切片长度和容量均为 3,b 切片长度为 1,容量为 10。将 a 切片和 b 切片作为函数参数传入 test 函数中。
在 test 函数中,对 a 切片和 b 切片做了如下两点改变
- 别离应用 append 函数在 a 切片和 b 切片中追加一个元素
- 别离对 a 切片和 b 切片的第一个元素做了批改
别离在主函数中和 test 函数中输入两个切片,会发现在主函数中和 test 函数中两个切片如同改了,又如同没改,上面咱们就来剖析一下。
实践剖析
当咱们将一个切片作为函数参数传递给函数的时候,采纳的是值传递,因而咱们传递给函数的参数其实是下面这个切片三元组的值拷贝。当咱们对切片构造中的指针进行值拷贝的时候,失去的指针还是指向了同一个底层数组。因而咱们通过指针对底层数组的值进行批改,从而批改了切片的值。
然而,当咱们以值传递的形式传递下面的构造体的时候,同时也是传递了 len
和cap
的值拷贝,因为这两个成员并不是指针,因而,当咱们从函数返回的时候,外层切片构造体的 len
和cap
这两个成员并没有扭转。
所以当咱们传递切片给函数的时候,并且在被调函数中通过 append
操作向切片中减少了值,然而当函数返回的时候,咱们看到的切片的值还是没有发生变化,其实底层数组的值是曾经扭转了的(如果没有触发扩容的话),然而因为长度 len
没有产生扭转,所以咱们看到的切片的值也没有产生扭转。
题目再剖析
有了后面的实践根底,咱们再来剖析一下 a,b 切片的返回后果。
-
a 切片作为参数传至 test 函数中,在 test 中向 a 切片追加一个元素后,此时触发扩容机制,返回的切片曾经不再是原切片,而是一个新的切片。后续对 a 切片中的第一个元素进行批改也是对新切片进行批改,对老切片不会产生任何影响。
所以,最终在主函数中 a 切片依然为[1 2 3],而在 test 函数中 a 切片变成了[3 2 3 4]。
- b 切片作为参数传至 test 函数中,在 test 中向 b 切片追加一个元素后,不会触发扩容机制,返回的依然是原切片,所以在后续对 b 切片的批改都是在原切片中进行的批改。故在 test 函数中 b 切片为[3 2]。然而在主函数中确为[3],能够看出在 test 中对切片进行批改的确反馈到主函数中了,然而因为其 len 和 cap 没有扭转,len 仍为 1,所以最终就只输入切片中的第一个元素[3],但其底层数组的值其实曾经扭转了。