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:
| Field | Meaning |
|---|---|
| Pointer | The first element in the underlying array |
len | Number of elements |
cap | Allocated 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
rangeorder is random each time. If you need order, extract the keys and sort them - Writing to a nil map panics: reads,
delete, andrangeare 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.Mutexor usesync.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:
vis a copy of the element. To modify a struct, uses[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{} |
| LINQ | for loops / slices and maps packages |
list.Add(x) | s = append(s, x) |
list.Count | len(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) |
ConcurrentDictionary | sync.Map / map + sync.RWMutex |
Span<T> / ArraySegment<T> | Slice expression s[a:b] |
SortedDictionary | Not 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). Useslices.Equal - A map's
lenis only the entry count and there is nocap. If you will add many entries, preallocate withmake(map[K]V, n) - When deleting during
for range, iterate backwards or useslices.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 useappend's return value - Use
slices.Clone/copyfor independent copies. Preallocate withmake([]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 aremap[T]struct{} - Instead of LINQ, use
forloops and theslices/mapspackages