123

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.Liststruct, 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.

Amin Shojaei
  • 5,451
  • 2
  • 38
  • 46
Vector
  • 10,879
  • 12
  • 61
  • 101
  • 14
    Note that Python's `list` type is not implemented using a linked list: it behaves similar to a Go slice, occasionally requiring data copies to expand. – James Henstridge Jan 24 '14 at 07:02
  • @JamesHenstridge - duly noted and corrected. – Vector Jan 24 '14 at 07:13
  • 2
    C++ does not use lists extensively. `std::list` is almost always a bad idea. `std::vector` is what you want to manage a sequence of items. For the same reasons `std::vector` is preferred, Go slice is preferred as well. – deft_code May 07 '14 at 19:10
  • @deft_code - understood. In my question `std::vector` was included in the `list` category because it requires no constant value for initialization and can be resized dynamically. When I asked the question, it was not clear to me that Go's `slice` could be used similarly - everything I read at the time explained that a slice was a "view of an array", and like in most other languages, plain vanilla arrays in Go need to be declared with a constant size. (But thanks for the heads up.) – Vector May 08 '14 at 17:24

7 Answers7

112

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.

Nick Craig-Wood
  • 52,955
  • 12
  • 126
  • 132
  • 4
    OK - this is what I was looking for. I had a misunderstanding about slices. You don't have to declare an array to use a slice. You can allocate a slice and that allocates the backing store. Sounds similar to streams in Delphi or C++. Now I understand why all the hoopla about slices. – Vector Jan 24 '14 at 08:59
  • 2
    @ComeAndGo, note that somethimes creating a slice which points into a "static" array is a useful idiom. – kostix Jan 24 '14 at 09:15
  • 2
    @FelikZ, slices create "a view" into their backing array. Often you know in advance that the data a function will operate on will have fixed size (or will have size no longer than a known amount of bytes; this is quite common for network protocols). So you might just declare an array to hold this data in your function and then slice it as will -- passing these slices to called functions etc. – kostix May 25 '15 at 10:19
  • What if I want to do something like - keep removing the first element from a list and return the element, and also keep appending elements to the list, should I use list or slice? If I were to work with a slice I would keep a "pointer" as the index of the element to return next and append to the slice whenever needed, but I worry that after some append operations the slice may grow very large with lots of useless elements at the front – instant501 Jan 25 '23 at 14:34
69

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 from int to int64 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.Listseems to be a sort of "mongrel", born of C++'s vector<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.

bh90210
  • 9
  • 3
Vector
  • 10,879
  • 12
  • 61
  • 101
  • @Alok [Go is a general-purpose language designed with systems programming in mind. **It is strongly typed...**](https://golang.org/ref/spec#Introduction) - Do they also have no idea what they're talking about? The use of type inference does not mean GoLang is **not** strongly typed. I also gave a clear illustration of this point : **Implicit type conversions are not permitted in GoLang, even when up-casting.** (Exclamation points don't make you more correct. Save them for kiddie blogs.) – Vector Jul 16 '15 at 08:27
  • @Alok - the mods deleted your comment, not me. Simply saying someone "doesn't know what they're talking about!" is useless unless you provide an explanation and proof. In addition this is supposed to be a professional venue, so we can leave out the exclamation points and hyperbole - save them for kiddie blogs. If you have a problem just say "I don't see how you can say GoLang is so strongly typed when we have A,B and C which seem to contradict that." Perhaps the OP will agree, or explain why they think you're wrong. That would be a useful and professional sounding comment, – Vector Jul 16 '15 at 19:14
  • 6
    statically checked language, which enforce some rules before the code runs. Languages, like C give you a primitive type system: your code might type check correctly but blow up at runtime. You keep going on this spectrum, you get Go, which gives you better guarantees than C. It's however nowhere close to type systems in languages like OCaml (which isn't the end of spectrum either). Saying "Go is perhaps the most strongly typed language out there" is plain wrong. It's important for developers to understand the safety properties of different languages so they can make an informed choice. – Alok Jul 16 '15 at 19:57
  • 5
    Specific examples of things missing from Go: lack of generics forces you to use dynamic casts. Lack of enumerations/ability to check switch completeness further implies dynamic checks where other languages can provide static guarantees. – Alok Jul 16 '15 at 20:00
  • @Alok-1 I) said _perhaps_ 2) We're talking about languages in fairly common use. Go isn't very strong these days,but Go has 10545 questions tagged,here OCaml has 3,230. 3) The deficiencies in Go you cite IMO don't have much to with "strongly typed", (a nebulous term which doesn't **necessarily** correlate to compile time checks). 4) "It's important .." - sorry but that doesn't make sense-if someone is reading this, they are probably using Go already. I doubt anyone is using this answer to decide if Go is for them. IMO you should find something more important to be "deeply bothered" about... – Vector Jul 16 '15 at 22:14
  • @Alok - I changed the language that you found so "deeply disturbing". :) It is not necessary for the answer. – Vector Jul 17 '15 at 18:46
11

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 TObjects 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.

kostix
  • 51,517
  • 14
  • 93
  • 176
  • Well, since Go lists don't act like other lists or vectors: they can't be indexed (List[i]) AFAIK (maybe I'm missing something...) and they also don't support Range, some explanations would be in order. But thanks on type assertions/switches - that was something I was missing until now. – Vector Jan 24 '14 at 07:58
  • @ComeAndGo, yes, they doesn't support ranges because `range` is a language builtin which is only applicable to builtin types (arrays, slices, strings and maps) because each "invocation" or `range` will in fact produce different machine code for traversing the container it's applied to. – kostix Jan 24 '14 at 08:40
  • 2
    @ComeAndGo, as to indexing... From the package's documentation it's clear that `container/list` provides a double-linked list. This means indexing is an `O(N)` operation (you have to start at head and traverse over each element towards the tail, counting), and one of the Go's cornerstone design paradigms is having no *hidden performance costs;* with another one being that putting some small extra burden on the programmer (implementing an indexing function for a double-linked list is a 10-line no-brainer) is OK. So the container only implements "canonical" operations sensible for its kind. – kostix Jan 24 '14 at 08:45
  • @ComeAndGo, note that in Delphi `TList` and others of its ilk use a dynamic array underneath, so extending such a list is not cheap while indexing it *is* cheap. So while Delphi's "lists" look like abstract lists in fact they are arrays--what you would use slices for in Go. What I want to highlight is that Go strives to lay things out clear without piling up "beautiful abstractions" "hiding" the details from the programmer. Go's approach is more like C's where you explicitly know how your data is laid out and how you access it. – kostix Jan 24 '14 at 08:49
  • The Delphi list classes (and most other languages as well ) provide a capacity property that pre-allocates a given amount of memory so things don't have to be re-shuffled as the list grows. – Vector Jan 24 '14 at 10:46
  • 3
    @ComeAndGo, precisely what can be done with Go's slices which have both length and capacity. – kostix Jan 24 '14 at 13:32
  • Yes, I saw the capacity parameter. Have to spend some time messing around with slices. The way the books describe slices don't seem to get to the heart of what you guys are telling me. That SliceTricks page looks a lot more interesting than what I've been seeing. I wish there was a really good detailed practical book on Go - "Go in a Nutshell" style. Chisnall's book is excellent but he's very brief. Summerfield's writing I don't care for - he always seems to be all over the map - I have a few of his books. Balbaert's book is good but it doesn't seem to be very well executed/translated, etc. – Vector Jan 24 '14 at 22:15
7

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.

James Henstridge
  • 42,244
  • 6
  • 132
  • 114
  • Still, slices must be back by an array with a hard coded size, correct? That's what I don't like. – Vector Jan 24 '14 at 07:54
  • 3
    The size of a slice isn't hard coded in the program source code, if that's what you mean. It can be dynamically expanded through the `append()` operation, as I explained (which will sometimes involve a data copy). – James Henstridge Jan 24 '14 at 07:59
  • @Vector in modern processors it's highly beneficial to have memory stored in contiguous blocks(arrays) instead of linked lists, as the CPU branch prediction will more likely load the appropriate array components into cache since CPUs better understand sequential memory. You're right a dynamically sized array has inefficiencies, but often these are less at actual runtime than you'd lose in cache misses from using a linked list. Of course all this is moot if you're never doing sequential access, but then a linked list is an even worse given the linear time to access a given element. – tsturzl Sep 20 '21 at 19:33
5

From: https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ


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)

So
use slice
1. 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)
2. 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
1. If you need speed in traversal

Manohar Reddy Poreddy
  • 25,399
  • 9
  • 157
  • 140
5

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

Manohar
  • 3,865
  • 11
  • 41
  • 56
4

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.

woodings
  • 7,503
  • 5
  • 34
  • 52