英文:
Why are lists used infrequently in Go?
问题
在Go语言中,可以使用切片(slice)来创建一个动态大小的数组,而不需要硬编码数组大小。切片是Go语言中非常重要的数据结构,可以根据需要动态调整大小。
关于list.List
结构体,的确在Go语言中存在,但是关于它的文档资料相对较少。在我所了解的Go语言教程和书籍中(例如《Go By Example》、Summerfield、Chisnal和Balbaert的书),它们都花了很多时间介绍数组和切片,然后直接跳到了映射(maps)的内容。在源代码示例中,我也很少或几乎没有看到使用list.List
的情况。
另外,与Python不同,Go语言中的切片不支持使用Range
语法。这可能是一个不足之处。我是否遗漏了什么重要的信息?
总的来说,切片是非常方便的数据结构,但它们仍然需要基于一个具有硬编码大小的数组。这就是list.List
的用途所在。
英文:
Is there a way to create an array/slice in Go without a hard-coded array size? Why is List ignored?
In all the languages I've worked with extensively: Delphi, C#, C++, Python - Lists are very important because they can be dynamically resized, as opposed to arrays.
In Golang, there is indeed a list.List
struct, but I see very little documentation about it - whether in Go By Example or the three Go books that I have - Summerfield, Chisnal and Balbaert - they all spend a lot of time on arrays and slices and then skip to maps. In souce code examples I also find little or no use of list.List
.
It also appears that, unlike Python, Range
is not supported for List - big drawback IMO. Am I missing something?
Slices are lovely, but they still need to be based on an array with a hard-coded size. That's where List comes in.
答案1
得分: 112
在Go语言中,当你考虑使用列表时,几乎总是应该使用切片(slice)。切片具有动态调整大小的能力。它们底层是一块连续的内存片段,可以改变大小。
如果你阅读一下SliceTricks wiki页面,你会发现它们非常灵活。
以下是其中的一部分内容:
复制
b = make([]T, len(a)) copy(b, a) // 或者 b = append([]T(nil), a...)
切割
a = append(a[:i], a[j:]...)
删除
a = append(a[:i], a[i+1:]...) // 或者 a = a[:i+copy(a[i:], a[i+1:])]
无序删除
a[i], a = a[len(a)-1], a[:len(a)-1]
弹出
x, a = a[len(a)-1], a[:len(a)-1]
压入
a = append(a, x)
更新:这里有一个来自Go团队的关于切片的博客文章,它很好地解释了切片与数组以及切片内部的关系。
英文:
Just about always when you are thinking of a list - use a slice instead in Go. Slices are dynamically re-sized. Underlying them is a contiguous slice of memory which can change size.
They are very flexible as you'll see if you read the SliceTricks wiki page.
Here is an excerpt :-
> Copy
>
> b = make([]T, len(a))
> copy(b, a) // or b = append([]T(nil), a...)
>
> Cut
>
> a = append(a[:i], a[j:]...)
>
> Delete
>
> a = append(a[:i], a[i+1:]...) // or a = a[:i+copy(a[i:], a[i+1:])]
>
> Delete without preserving order
>
> a[i], a = a[len(a)-1], a[:len(a)-1]
>
> Pop
>
> x, a = a[len(a)-1], a[:len(a)-1]
>
> Push
>
> a = append(a, x)
Update: Here is a link to a blog post all about slices from the go team itself, which does a good job of explaining the relationship between slices and arrays and slice internals.
答案2
得分: 68
我几个月前问过这个问题,当时我刚开始研究Go语言。从那时起,我每天都在阅读关于Go的文章,并用Go进行编码。
因为我没有得到一个明确的答案(尽管我接受了一个答案),现在我打算根据我所学到的知识自己回答这个问题:
> 有没有一种方法可以在Go中创建一个没有硬编码数组大小的数组/切片?
有。切片不需要从硬编码的数组中进行切片:
var sl []int = make([]int, len, cap)
这段代码分配了一个大小为len
、容量为cap
的切片sl
,其中len
和cap
是可以在运行时赋值的变量。
> 为什么list.List
被忽视了?
Go中list.List
似乎受到忽视的主要原因有:
-
正如@Nick Craig-Wood的回答中所解释的那样,几乎没有什么可以用列表做而不能用切片做的事情,而且通常用切片更高效、语法更清晰、更优雅。例如,使用range结构:
for i := range sl { sl[i] = i }
不能用于列表-需要使用C风格的for循环。而且在许多情况下,必须使用C++的集合风格语法来操作列表,比如
push_back
等。 -
更重要的是,
list.List
没有强类型约束-它非常类似于Python的列表和字典,允许在集合中混合各种类型。这似乎与Go的设计理念相悖。Go是一种非常强类型的语言-例如,Go中不允许隐式类型转换,即使是从int
到int64
的向上转换也必须是显式的。但是,list.List
的所有方法都接受空接口-什么都可以传进去。
我放弃Python转而使用Go的原因之一就是因为Python的类型系统中存在这种弱点(在我看来是这样)。Go的list.List
似乎是一种“杂交”,它既有C++的vector<T>
的特点,又有Python的List()
的特点,在Go本身可能有些不合适。
如果在不久的将来,我们发现list.List
在Go中被弃用,我不会感到惊讶,尽管也许它会保留下来,以适应那些罕见的情况,即使使用良好的设计实践,问题最好的解决方法仍然是使用一个可以容纳各种类型的集合。或者也许它存在的目的是为了为C系列的开发人员提供一个“桥梁”,让他们在学习Go的细微差别之前先熟悉一下切片,切片在我所知道的范围内是Go独有的(在某些方面,切片似乎类似于C++或Delphi中的流类,但并不完全相同)。
虽然我有Delphi/C++/Python的背景,但在我最初接触Go时,list.List
比Go的切片更加熟悉,但随着我对Go的熟悉程度越来越高,我已经回过头来将所有的列表都改为切片。到目前为止,我还没有发现任何slice
和/或map
不能做到的事情,以至于我需要使用list.List
。
英文:
I asked this question a few months ago, when I first started investigating Go. Since then, every day I have been reading about Go, and coding in Go.
Because I did not receive a clear-cut answer to this question (although I had accepted one answer) I'm now going to answer it myself, based on what I have learned, since I asked it:
> Is there a way to create an array /slice in Go without a hard coded
> array size?
Yes. Slices do not require a hard coded array to slice
from:
var sl []int = make([]int, len, cap)
This code allocates slice sl
, of size len
with a capacity of cap
- len
and cap
are variables that can be assigned at runtime.
> Why is list.List
ignored?
It appears the main reasons list.List
seem to get little attention in Go are:
-
As has been explained in @Nick Craig-Wood's answer, there is
virtually nothing that can be done with lists that cannot be done
with slices, often more efficiently and with a cleaner, more
elegant syntax. For example the range construct:for i := range sl { sl[i] = i }
cannot be used with list - a C style for loop is required. And in
many cases, C++ collection style syntax must be used with lists:
push_back
etc. -
Perhaps more importantly,
list.List
is not strongly typed - it is very similar to Python's lists and dictionaries, which allow for mixing various types together in the collection. This seems to run contrary
to the Go approach to things. Go is a very strongly typed language - for example, implicit type conversions never allowed in Go, even an upCast fromint
toint64
must be
explicit. But all the methods for list.List take empty interfaces -
anything goes.One of the reasons that I abandoned Python and moved to Go is because
of this sort of weakness in Python's type system, although Python
claims to be "strongly typed" (IMO it isn't). Go'slist.List
seems to
be a sort of "mongrel", born of C++'svector<T>
and Python's
List()
, and is perhaps a bit out of place in Go itself.
It would not surprise me if at some point in the not too distant future, we find list.List deprecated in Go, although perhaps it will remain, to accommodate those rare situations where, even using good design practices, a problem can best be solved with a collection that holds various types. Or perhaps it's there to provide a "bridge" for C family developers to get comfortable with Go before they learn the nuances of slices, which are unique to Go, AFAIK. (In some respects slices seem similar to stream classes in C++ or Delphi, but not entirely.)
Although coming from a Delphi/C++/Python background, in my initial exposure to Go I found list.List
to be more familiar than Go's slices, as I have become more comfortable with Go, I have gone back and changed all my lists to slices. I haven't found anything yet that slice
and/or map
do not allow me to do, such that I need to use list.List
.
答案3
得分: 11
我认为这是因为关于container/list
包的内容没有太多可以说的,因为一旦你理解了Go语言中处理通用数据的主要惯用法,它就变得非常容易理解。
在没有泛型的Delphi或C中,你会在列表中存储指针或TObject
,然后在从列表中获取时将它们强制转换回其真实类型。在C++的STL中,列表是模板,因此可以通过类型参数化,而在C#中(现在)列表是泛型的。
在Go中,container/list
存储类型为interface{}
的值,这是一种特殊类型,能够表示任何其他(真实)类型的值,通过存储一对指针:一个指向所包含值的类型信息的指针,以及一个指向该值的指针(如果其大小不大于指针的大小,则直接存储值)。因此,当你想要向列表中添加元素时,你只需要将其作为类型为interface{}
的函数参数传递,接受任何类型的值。但是,当你从列表中提取值并希望使用它们的真实类型时,你必须对它们进行类型断言或进行类型切换,这两种方法本质上是做同样的事情的不同方式。
这里有一个示例,摘自这里:
package main
import ("fmt" ; "container/list")
func main() {
var x list.List
x.PushBack(1)
x.PushBack(2)
x.PushBack(3)
for e := x.Front(); e != nil; e=e.Next() {
fmt.Println(e.Value.(int))
}
}
在这个示例中,我们使用e.Value()
获取元素的值,然后将其类型断言为插入的原始值的类型int
。
你可以在《Effective Go》或其他入门书籍中了解有关类型断言和类型切换的内容。container/list
包的文档总结了列表支持的所有方法。
英文:
I think that's because there's not much to say about them as the container/list
package is rather self-explanatory once you absorbed what is the chief Go idiom for working with generic data.
In Delphi (without generics) or in C you would store pointers or TObject
s in the list, and then cast them back to their real types when obtaining from the list. In C++ STL lists are templates and hence parameterized by type, and in C# (these days) lists are generic.
In Go, container/list
stores values of type interface{}
which is a special type capable to represent values of any other (real) type—by storing a pair of pointers: one to the type info of the contained value, and a pointer to the value (or the value directly, if it's size is no greater than the size of a pointer). So when you want to add an element to the list, you just do that as function parameters of type interface{}
accept values coo any type. But when you extract values from the list, and what to work with their real types you have to either type-asert them or do a type switch on them—both approaches are just different ways to do essentially the same thing.
Here is an example taken from here:
package main
import ("fmt" ; "container/list")
func main() {
var x list.List
x.PushBack(1)
x.PushBack(2)
x.PushBack(3)
for e := x.Front(); e != nil; e=e.Next() {
fmt.Println(e.Value.(int))
}
}
Here we obtain an element's value using e.Value()
and then type-assert it as int
a type of the original inserted value.
You can read up on type assertions and type switches in "Effective Go" or any other introduction book. The container/list
package's documentation summaries all the methods lists support.
答案4
得分: 7
请注意,Go语言的切片可以通过append()
内置函数进行扩展。尽管这有时需要复制支持数组,但并不是每次都需要复制,因为Go语言会为新数组分配比报告的长度更大的容量。这意味着可以在不进行数据复制的情况下完成后续的追加操作。
虽然与使用链表实现的等效代码相比,你最终会有更多的数据复制,但你无需单独分配列表中的元素,也无需更新Next
指针。对于许多用途来说,基于数组的实现提供了更好或足够好的性能,因此在语言中强调了这一点。有趣的是,Python的标准list
类型也是基于数组的,并且在追加值时具有类似的性能特征。
尽管如此,有些情况下链表是更好的选择(例如,当你需要在长列表的开头/中间插入或删除元素时),这就是为什么提供了标准库实现的原因。我猜他们没有添加任何特殊的语言功能来处理链表,因为这些情况比使用切片的情况更少见。
英文:
Note that Go slices can be expanded via the append()
builtin function. While this will sometimes require making a copy of the backing array, it won't happen every time, since Go will over-size the new array giving it a larger capacity than the reported length. This means that a subsequent append operation can be completed without another data copy.
While you do end up with more data copies than with equivalent code implemented with linked lists, you remove the need to allocate elements in the list individually and the need to update the Next
pointers. For many uses the array based implementation provides better or good enough performance, so that is what is emphasised in the language. Interestingly, Python's standard list
type is also array backed and has similar performance characteristics when appending values.
That said, there are cases where linked lists are a better choice (e.g. when you need to insert or remove elements from the start/middle of a long list), and that is why a standard library implementation is provided. I guess they didn't add any special language features to work with them because these cases are less common than those where slices are used.
答案5
得分: 5
来源:https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ
<pre>
这在很大程度上取决于列表中元素的数量,当你需要在列表的“中间”进行多次删除时,使用真正的列表还是切片更高效。
#1
元素越多,切片就越不具吸引力。
#2
当元素的顺序不重要时,使用切片是最高效的方法,可以通过将要删除的元素与切片中的最后一个元素交换,并重新调整切片的长度减1来删除元素
(如SliceTricks wiki中所解释的)。
</pre>
所以
使用切片
- 如果列表中元素的顺序不重要,并且需要进行删除操作,只需使用切片将要删除的元素与最后一个元素交换,并重新切片为(长度-1)。
- 当元素较多时(无论“更多”意味着什么)。
有一些方法可以缓解删除问题 - 例如你提到的交换技巧或仅将元素标记为逻辑删除。但是无法缓解遍历链表的速度慢的问题。
所以
使用切片
- 如果需要快速遍历。
英文:
From: https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ
<pre>
It depends a lot on the number of elements in your lists,
whether a true list or a slice will be more efficient
when you need to do many deletions in the 'middle' of the list.
#1
The more elements, the less attractive a slice becomes.
#2
When the ordering of the elements isn't important,
it is most efficient to use a slice and
deleting an element by replacing it by the last element in the slice and
reslicing the slice to shrink the len by 1
(as explained in the SliceTricks wiki)
</pre>
So
use slice
- If order of elements in list is Not important, and you need to delete, just
use List swap the element to delete with last element, & re-slice to (length-1) - when elements are more (whatever more means)
There are ways to mitigate the deletion problem --
e.g. the swap trick you mentioned or
just marking the elements as logically deleted.
But it's impossible to mitigate the problem of slowness of walking linked lists.
So
use slice
- If you need speed in traversal
答案6
得分: 5
除非切片更新得太频繁(在随机位置删除、添加元素),否则与链表相比,切片的内存连续性将提供更好的缓存命中率。
Scott Meyer在他的演讲中强调了缓存的重要性。
https://www.youtube.com/watch?v=WDIkqP4JbkE
英文:
Unless the slice is updated way too often (delete, add elements at random locations) the memory contiguity of slices will offer excellent cache hit ratio compared to linked lists.
Scott Meyer's talk on the importance of cache..
https://www.youtube.com/watch?v=WDIkqP4JbkE
答案7
得分: 4
list.List
是一个双向链表实现。在大多数情况下,基于数组的列表(如C++中的向量或golang中的切片)比链表更好,除非你经常在列表中间插入元素。尽管数组列表需要扩展容量并复制现有值,但附加操作的摊销时间复杂度对于数组列表和链表都是O(1)。数组列表具有更快的随机访问、更小的内存占用,并且更重要的是,由于数据结构内部没有指针,对垃圾收集器更友好。
英文:
list.List
is implemented as a doubly linked list. Array-based lists (vectors in C++, or slices in golang) are better choice than linked lists in most conditions if you don't frequently insert into the middle of the list. The amortized time complexity for append is O(1) for both array list and linked list even though array list has to extend the capacity and copy over existing values. Array lists have faster random access, smaller memory footprint, and more importantly friendly to garbage collector because of no pointers inside the data structure.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论