Skip to main content

Slices, Maps, and Arrays

For developers with .NET (C#) experience, this guide covers arrays, slices, maps, range loops, and how they differ from List<T> / Dictionary<K,V>.

1. Arrays​

A Go array has its length as part of its type and is a value type. [3]int and [4]int are different types, and assignment or passing to a function copies all elements.

var a [3]int // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]string{"x", "y"} // length inferred from the elements

d := b // copies all elements
d[0] = 99
fmt.Println(b[0]) // 1

Except for special cases that need a fixed length (such as hashes like [32]byte), use slices day to day.

2. Slices​

A slice is a view onto part of an array. Internally it is a header of three fields:

FieldMeaning
PointerThe first element in the underlying array
lenNumber of elements
capAllocated capacity of the underlying array, counted from the start of the slice
s := []int{1, 2, 3} // literal
t := make([]int, 0, 10) // len=0, cap=10 (preallocated)
var u []int // nil slice (len=0, cap=0)

fmt.Println(len(s), cap(s)) // 3 3

How append Works​

append writes into the underlying array if there is spare capacity; otherwise it allocates a larger array and copies. You must always use its return value.

s := make([]int, 0, 2)
s = append(s, 1, 2) // cap stays 2
s = append(s, 3) // reallocated: cap grows (typically about 2x)

If you know the number of elements in advance, reserve capacity with make([]T, 0, n) to avoid reallocations.

Slice Expressions and Sharing​

s[low:high] does not copy the data; it shares the same underlying array.

a := []int{0, 1, 2, 3, 4}
b := a[1:3] // [1 2] len=2, cap=4
b[0] = 99
fmt.Println(a) // [0 99 2 3 4] the original changes too

b = append(b, 100)
fmt.Println(a) // [0 99 2 100 4] spare cap, so it overwrites the original element

When you need an independent copy, use slices.Clone or copy.

c := slices.Clone(a[1:3]) // Go 1.21+
d := make([]int, 2)
copy(d, a[1:3])

A three-index slice a[1:3:3] limits cap so that append no longer overwrites the original.

nil Slices vs. Empty Slices​

var n []int // nil
e := []int{} // empty (not nil)

fmt.Println(len(n), n == nil) // 0 true
n = append(n, 1) // append works on nil too

A nil slice works as is with len, cap, range, and append. Test for "empty" with len(s) == 0, not s == nil. Note that in JSON, a nil slice serializes to null and an empty slice to [].

Deleting and Inserting Elements​

There are no dedicated methods; use the standard slices package.

s := []int{0, 1, 2, 3, 4}
s = slices.Delete(s, 1, 3) // [0 3 4] (removes indexes 1 up to, not including, 3)
s = slices.Insert(s, 1, 10, 11) // [0 10 11 3 4]

3. Maps​

map[K]V is a hash table, equivalent to Dictionary<K,V>. Keys must be of a type comparable with == (strings, numbers, structs, arrays, etc.). Slices, maps, and functions cannot be keys.

m := map[string]int{"a": 1, "b": 2}
m2 := make(map[string]int)

m["c"] = 3
v := m["a"] // 1
v, ok := m["x"] // ok=false: a missing key yields the zero value and false
delete(m, "a") // no error even if the key does not exist
fmt.Println(len(m))

Important Caveats​

  • Order is not guaranteed: the range order is random each time. If you need order, extract the keys and sort them
  • Writing to a nil map panics: reads, delete, and range are safe on nil
var m map[string]int
fmt.Println(m["a"]) // 0 (OK)
m["a"] = 1 // panic: assignment to entry in nil map
  • Concurrent access is not safe: reading and writing from multiple goroutines causes a fatal error. Protect it with sync.Mutex or use sync.Map (Goroutines and Channels)
  • You cannot modify a struct value in place: m["a"].Count++ is a compile error
m := map[string]Item{}
it := m["a"]
it.Count++
m["a"] = it // take it out, modify, and put it back

mp := map[string]*Item{} // with pointer values you can modify directly
mp["a"].Count++

Iterating in Order​

import (
"maps"
"slices"
)

for _, k := range slices.Sorted(maps.Keys(m)) { // Go 1.23+
fmt.Println(k, m[k])
}

Replacing HashSet​

Represent a set as map[T]struct{} (or map[T]bool). struct{} uses no memory.

set := map[string]struct{}{}
set["a"] = struct{}{}
if _, ok := set["a"]; ok {
fmt.Println("contains")
}

4. range Loops​

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

for i, v := range s { fmt.Println(i, v) } // index and value
for i := range s { fmt.Println(i) } // index only
for _, v := range s { fmt.Println(v) } // value only

for k, v := range map[string]int{"x": 1} { fmt.Println(k, v) }
for i, r := range "日本語" { fmt.Println(i, r) } // strings iterate by rune (i is the byte offset)
for i := range 3 { fmt.Println(i) } // integer (Go 1.22+): 0 1 2

Notes:

  • v is a copy of the element. To modify a struct, use s[i].Field = ...
  • From Go 1.22, the loop variable is a new variable per iteration
  • Adding or deleting map entries during a loop has defined behavior, but whether added entries are visited is unspecified

5. The slices / maps Standard Packages​

There is no general-purpose equivalent of LINQ, but since Go 1.21 the slices and maps packages provide the basic operations.

import (
"maps"
"slices"
)

nums := []int{3, 1, 2}

slices.Sort(nums) // [1 2 3]
slices.Contains(nums, 2) // true
idx := slices.Index(nums, 3) // 2
slices.Reverse(nums) // [3 2 1]
slices.Max(nums) // 3
i, found := slices.BinarySearch(nums, 2) // assumes sorted input

people := []Person{{"a", 30}, {"b", 20}}
slices.SortFunc(people, func(x, y Person) int {
return cmp.Compare(x.Age, y.Age) // import "cmp"
})

m2 := maps.Clone(m)
keys := slices.Collect(maps.Keys(m)) // Go 1.23+

For Where / Select equivalents, writing a for loop is the normal Go way.

var adults []Person
for _, p := range people {
if p.Age >= 20 {
adults = append(adults, p)
}
}

6. Mapping to .NET​

.NET (C#)Go
T[] (array, reference type)[N]T (array, value type) / usually []T
List<T>[]T (slice)
Dictionary<K,V>map[K]V
HashSet<T>map[T]struct{}
LINQfor loops / slices and maps packages
list.Add(x)s = append(s, x)
list.Countlen(s)
list.RemoveAt(i)s = slices.Delete(s, i, i+1)
list.Contains(x)slices.Contains(s, x)
list.Sort()slices.Sort(s)
dict.TryGetValue(k, out v)v, ok := m[k]
dict.Remove(k)delete(m, k)
ReadOnlyCollection<T>None (protect via convention and copies)
ConcurrentDictionarysync.Map / map + sync.RWMutex
Span<T> / ArraySegment<T>Slice expression s[a:b]
SortedDictionaryNot in the standard library (sort the keys and iterate)

7. Common Pitfalls for C# Developers​

Slices Are Not References: They Pass a Header by Value​

Passing a slice to a function copies only the header. Element changes are visible to the caller, but the result of append is not reflected unless you return it.

func add(s []int) { s = append(s, 1) } // the caller's len does not change
func add2(s []int) []int { return append(s, 1) }

s = add2(s)

Assign append's Result to the Same Variable​

With s2 := append(s1, x), depending on capacity s1 and s2 may or may not share the underlying array. The rule is to use the form s = append(s, x).

Holding Part of a Slice Keeps Memory Alive​

Holding a piece big[:10] of a huge slice prevents the whole underlying array from being garbage collected. Copy with slices.Clone when needed.

Map Iteration Order​

C# Dictionary typically enumerates in an order close to insertion (as an implementation detail), but Go is random. Do not let test expectations or output order depend on map order.

Other Points​

  • Arrays are copied when passed by value (C# arrays are reference types)
  • An out-of-range index panics (index out of range)
  • Slices cannot be compared with == (only against nil). Use slices.Equal
  • A map's len is only the entry count and there is no cap. If you will add many entries, preallocate with make(map[K]V, n)
  • When deleting during for range, iterate backwards or use slices.DeleteFunc
  • Strings are immutable byte sequences. Converting to []byte(s) or []rune(s) involves a copy

8. Code Example​

C#​

var users = new List<User> { new("a", 30), new("b", 17) };

var adultNames = users
.Where(u => u.Age >= 18)
.Select(u => u.Name)
.OrderBy(n => n)
.ToList();

var counts = new Dictionary<string, int>();
foreach (var u in users)
{
counts.TryGetValue(u.Name, out var c);
counts[u.Name] = c + 1;
}

Go​

package main

import (
"fmt"
"maps"
"slices"
)

type User struct {
Name string
Age int
}

func main() {
users := []User{{"a", 30}, {"b", 17}}

// Equivalent of Where + Select + OrderBy (capacity preallocated)
adultNames := make([]string, 0, len(users))
for _, u := range users {
if u.Age >= 18 {
adultNames = append(adultNames, u.Name)
}
}
slices.Sort(adultNames)

// Counting: a missing key yields the zero value, so you can add directly
counts := map[string]int{}
for _, u := range users {
counts[u.Name]++
}

// Print in order
for _, k := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(k, counts[k])
}

fmt.Println(adultNames)
}

9. Summary​

  • Arrays are value types with the length in the type. Normally use slices ([]T)
  • A slice is a (pointer, len, cap) header. s[a:b] shares the underlying array, and you must use append's return value
  • Use slices.Clone / copy for independent copies. Preallocate with make([]T, 0, n) when the size is known
  • Maps are unordered, writing to a nil map panics, and concurrent access needs protection
  • Check existence with v, ok := m[k]. Sets are map[T]struct{}
  • Instead of LINQ, use for loops and the slices / maps packages

References​