Golang新手疑问:为什么需要切片的存在
Golang新手疑问:为什么需要切片的存在 作为一名Go语言新手,我对切片为何需要存在感到困惑。在我的编程生涯中,我一直使用数组和列表,从未觉得有必要创建类似切片的东西来与我的数组交互。
我在互联网上找不到任何关于Go切片为何需要存在的解释。
谁能给我指出一个决定性的、最简单的基案例子,清晰地展示切片在某些有用的方面比直接使用数组/列表更好。
谢谢
大家好,
原帖作者说得对,切片确实比数组开销稍大,他的直觉是正确的。但是,你不需要手动为切片分配内存(以调整大小),这是一个巨大的优势,因为你需要编写的代码更少。
虽然成本稍高(但几乎相同),但使用它们进行编码要容易得多(与使用纯数组相比,使用切片时需要编写的代码更少)。
更多关于Golang新手疑问:为什么需要切片的存在的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我认为在 Go 语言中处理切片或在 Rust 中处理 Vec,比直接在 C 语言中处理数组要更加符合人体工程学。在 C 语言中,当我需要动态数组时,总是需要进行大量手动管理,这基本上相当于重新实现 Go 语言中已知的切片或 Rust 中的 Vec。
我知道 Rust 的 Vec 或 Go 的切片是以一种对最常见用例足够高效的方式实现的。
我不会信任自己的实现,因此可能会选择使用库来处理,这样会更容易,但那样我就必须信任第三方。
我更倾向于能够直接信任标准库/运行时来处理这类事情。
Can anyone point me at the definitive and simplest base case example that cleanly shows slices being better in some useful way over straight array / list use.
我不会说哪个更好。由于它们具有不同的特性(数组 = 固定长度 & 按值传递,切片 = 可变长度,基于数组映射,几乎按引用传递),它们只是有不同的使用场景。
切片可以看作是数组之上的一个便利层,而数组对于控制内存布局和堆分配可能很有用。
这里有一个相当有用的区别:在二维切片中,每个内部切片可以有不同的长度。
我将切片视为链表的替代品。我几乎从不使用链表或其他方法,更倾向于使用切片。我的背景主要是“C”语言。
创建切片非常简单,对它们进行排序也是如此,尽管如果能为已排序的切片提供一个库搜索机制就更好了。
我使用切片编写了一个SPF算法,其底层内存管理出奇地简单:
var s [] typeof(el)
// 添加下一个更高成本的节点
s = append(s, el)
处理和移除成本最低的节点:
el = s[0]
s = [1:]
在“C”语言中实现类似功能是相当具有挑战性的,包括必须跟踪和释放“el”。
由于不了解你正在比较的语言,也许可以展示执行此功能的软件。对我来说,很难想象代码能比这简单多少。
虽然我希望Go能提供更快速(不安全作用域内)的代码编写方式,但我从未将切片视为性能问题。我认为它只是类似于Python为确保内存安全而采用的概念。在安全代码块中,切片只会缩小,起始点向右移动,结束点向左移动,这些都在运行时进行检查,不允许我进行其他操作,因此在我看来,这就像是指针运算的一个安全子集。虽然不是绝对完美的方案,但尚可接受。
如果C/C++程序能够在出现初始错误时就尽早且强制性地停止(因为这类语言要求你声明不安全代码块,而不是让你记住大量需要留意的规则),那么我使用Go的理由就会少一个。所以,Go并没有绝对的理由,但也没有真正的替代方案,不是吗?(我不喜欢Go过度“保姆式”的照顾,有时它确实如此。不幸的是,这看起来像是一个有助于剔除未使用代码的概念,但它的实际帮助可能不如Go的创造者所设想的那样大。)
如果我理解这个问题,它问的是“既然我们有了指针,为什么还要使用切片?”。这是一个合理的问题,因为它们可以服务于相同的目的,即指向可变数量的元素。切片和指针之间的唯一区别在于,切片还包含关于你所指向的元素数量的信息,而指针则没有。在C语言中,你可能会编写一个消耗指针的函数,例如:
int sum(int *array, int len) {
int sum = 0;
for(int i = 0; i < len; i++) {
sum += array[i];
}
return i;
}
在Go语言中,我们可以使用切片来实现相同的功能:
func sum(array []int) int {
sum := 0
for i := 0; i < len(array); i++ {
sum += array[i];
}
return sum
}
简而言之,可以将切片视为带有额外信息的指针,即你所指向的数据的长度。上述观点的证明:Go Playground - The Go Programming Language
是的,Go语言中的切片(slice)在其他语言中通常被称为数组(C语言除外,它使用指针)。因此,如果原帖作者在其他语言中使用过数组和列表,他会发现Go的切片同样有用。
我可能会问,为什么Go语言要有数组(按照它的定义),以及为什么需要它们?我想我从未使用过Go的数组。甚至不可能创建一个大小为N的Go数组,其中N是在运行时指定的。是这样吗?我不完全确定,因为我不使用它们。你必须写成 [2]int 或 [16]byte 等等。如果你写 make([]int, n),你得到的是一个切片,而不是数组。如果你正在编写使用固定大小块的专用算法,例如AES和其他与加密相关的算法,你可能会使用数组。如果你不希望将数组传递给函数时复制整个数组,就必须小心传递它们的方式。
我想对Go语言的新手说,忘掉数组,使用切片。
我希望Go语言规范能在数组类型之前提到切片类型。数组类型更接近于结构体类型,而结构体类型是在切片类型之后才提到的。
func main() {
fmt.Println("hello world")
}
切片的大小是可变的,数组的大小是固定的。
我已经知道了。但仍然不明白为什么切片比数组引用/指针更好。
切片具有O(1)的随机访问复杂度,而链表是O(n)。
我不相信。为什么世界上所有语言的开发者,特别是数据库语言的开发者,不给他们发明的每种语言都打上补丁,使其支持切片,从而让随机访问达到O(1)呢?
一个有n个元素的数组会占用n倍数据大小的内存。一个切片占用相同的内存,外加一个指向起始位置的指针和当前大小。一个链表:每个元素都有一个指针!
我知道Go语言将数组复制到函数中,而不是引用它们。但为什么不直接传递一个指向数组或数组元素的指针呢?
我重申,自2009年Go语言诞生以来,没有人能够证明切片相对于数组/链表/指针的优势。如果有人做到了,演示代码肯定会在互联网上被大肆宣扬多年:“看看我们通过使用切片而不是数组/链表/指针节省了多少内存/时间/代码行数!”
而在这个流行的Go语言论坛上对此事的沉默,证实了这一点。
唯一合理的结论是,肯·汤普森只是随口提出了另一种处理数组的方式——切片,并不知道它是否有用,但想知道某些地方的开发者是否能找到一种方法,使切片比数组/链表/指针更好。
你好,@meemoeuk,欢迎来到论坛!
Can anyone point me at the definitive and simplest base case example that cleanly shows slices being better in some useful way over straight array / list use.
不,我无法给你任何确定性的答案。在我看来,Go 中之所以需要切片,仅仅是因为他们决定让数组在编译时具有固定大小。从我的角度来看,Go 的切片就是我在 C 语言中称之为数组的东西。在 C 语言中,我可能会这样写:
err_code_t getsomedata(data_t *buffer, int capacity, int *outElements);
所以 getsomedata 函数接收一个缓冲区指针和容量参数,需要将其数据写入缓冲区,并将写入的元素数量存储到 outElements 中。这个 API 可能必须返回某个 err_code_t 值,以指示容量不足或其他情况。
在 Go 中,我会这样写:
func getsomedata(buffer []data) ([]data, error)
就像 C 语言一样,我可以预先分配一个缓冲区并传入,然后实现会向其追加数据并返回它。
就像我说的,我没有什么确定性的东西,但 Go 版本至少在一个方面比 C 版本更好,因为它能提供更多的编译时安全性。在 C 语言中,当你得到一个 data_t* 时,类型系统不会告诉你那是指向单个 data_t 的指针还是一个数组。程序员必须熟悉 API 和约定。在 Go 中,你使用 *data 表示单个,使用 []data 表示多个。
对我来说,Go 的 append 函数与 C 的 realloc 是同一回事。在 C 语言中,我会在自己的变量中(或者可能在一个结构体中)跟踪缓冲区的当前长度和总容量(当然,这取决于上下文),但在 Go 中,所有这些都被封装进了一个切片中(切片只是一个数组指针以及长度和容量字段)。
在我看来,他们希望内置类型具有接近 C 语言的简洁性,只添加了一些功能,以使连接字符串、向数据序列追加、关联数组等操作更容易。如果你想要类似 Python 或 C# 的 list/List<> 类型,你可以在切片(或数组)的基础上构建它(但如果使用数组,则必须使用 unsafe 包或进行大量的运行时类型检查)。
切片在Go语言中的存在主要是为了解决数组的几个核心限制,同时提供比列表更高效的内存布局和性能。以下是一个简单的例子,展示切片相比数组的优势:
package main
import "fmt"
func main() {
// 数组的局限性
arr := [3]int{1, 2, 3}
// 问题1:数组长度固定,无法动态添加元素
// arr = append(arr, 4) // 编译错误
// 切片的解决方案
slice := []int{1, 2, 3}
slice = append(slice, 4) // 可以动态扩展
fmt.Println("动态扩展后的切片:", slice) // [1 2 3 4]
// 问题2:数组作为函数参数时是值传递
modifyArray(arr)
fmt.Println("原数组未被修改:", arr) // [1 2 3]
// 切片作为函数参数传递的是引用
modifySlice(slice)
fmt.Println("原切片已被修改:", slice) // [100 2 3 4]
// 问题3:数组无法方便地获取子集
subSlice := slice[1:3] // 获取第2到第3个元素
fmt.Println("子切片:", subSlice) // [2 3]
// 问题4:切片可以共享底层数组,节省内存
original := []int{1, 2, 3, 4, 5}
view1 := original[1:4] // 共享底层数组
view2 := original[2:5] // 共享底层数组
view1[0] = 99
fmt.Println("修改view1影响original:", original) // [1 99 3 4 5]
fmt.Println("view2也受到影响:", view2) // [3 4 5]
}
func modifyArray(a [3]int) {
a[0] = 100
}
func modifySlice(s []int) {
s[0] = 100
}
切片的核心优势:
- 动态大小:切片可以动态增长(通过
append) - 引用语义:函数间传递时不会复制底层数据
- 内存效率:多个切片可以共享同一个底层数组
- 灵活性:可以方便地创建子切片
相比之下,Go标准库没有内置的链表实现,因为切片在大多数场景下性能更好(连续内存、CPU缓存友好)。


