Notessh2a

Data Structures

Arrays

In Go, array is a fixed-size sequence of elements of a single type.

  • Declare:

    var arr [5]int // [0 0 0 0 0] (zero valued)

    Or initialize at declaration:

    arr := [5]int{1, 2, 3, 4, 5}
  • Access:

    fmt.Println(arr[0]) // 1
  • Modify:

    arr[0] = 100
    fmt.Println(arr[0]) // 100

Array elements are stored in contiguous memory locations, meaning they are placed directly next to each other in memory.

func main() {
	arr := [3]int{1, 2, 3}

	fmt.Printf("&arr[0]: %v\n", &arr[0]) // &arr[0]: 0xc00011e000
	fmt.Printf("&arr[1]: %v\n", &arr[1]) // &arr[1]: 0xc00011e008
	fmt.Printf("&arr[2]: %v\n", &arr[2]) // &arr[2]: 0xc00011e010
}

Here, the type int is int64 (because I'm on a 64-bit system), so each element takes up 8 bytes of memory. The addresses of the elements are spaced 8 bytes apart (0 -> 8 -> 16).

Extra:

  • Let the compiler determine the size:

    arr := [...]int{3, 5, 6, 2, 1} // [5]int
  • Initialize specific indices:

    The index: value syntax assigns values to selected indices. Unspecified indices remain zero-valued.

    arr := [...]int{3, 5, 4: 6, 10: 1, 2, 1}
    fmt.Println(arr) // [3 5 0 0 6 0 0 0 0 0 1 2 1]
  • Multi-dimensional arrays:

    arr2d := [3][2]int{
        {1, 2},
        {3, 4},
        {5, 6},
    }
    
    arr2d[1][1] = 9
    fmt.Println(arr2d) // [[1 2] [3 9] [5 6]]
  • Slicing an array:

    Slicing creates a slice from a selected range of elements in an array.

    arr[low:high]

    • low: the starting index, inclusive.
    • high: the ending index, exclusive.
    arr := [5]int{0, 10, 20, 30, 40}
    slice := arr[1:3]
    fmt.Printf("%T, %v", slice, slice) // []int, [10 20]

    Variants:

    slice1 := arr[2:4] // [20 30] (elements at index 2 and 3)
    slice2 := arr[:4]  // [0 10 20 30] (from the start to index 3)
    slice3 := arr[4:]  // [40] (from index 4 to the end)
    slice4 := arr[:]   // [0 10 20 30 40] (the entire array)

    Slicing does not copy the elements. It creates a slice that refers to the same underlying array.

    slice[0] = 99
    fmt.Printf("%T, %v", slice, slice) // []int, [99 20]
    fmt.Printf("%T, %v", arr, arr)     // [5]int, [0 99 20 30 40]
    
    slice = append(slice, 100)
    fmt.Printf("%T, %v", slice, slice) // []int, [99 20 100]
    fmt.Printf("%T, %v", arr, arr)     // [5]int, [0 99 20 100 40]

Slices

In Go, a slice is a dynamically-sized, flexible view into the elements of an array.

  • Declare:

    var slice []int // []int(nil) // (nil slice, zero length)

    Or initialize at declaration:

    slice := []int{} // []int{} (non-nil, zero length)
    slice := []int{1, 2, 3, 4, 5} // []int{1, 2, 3, 4, 5}

    Or using the make function:

    slice := make([]int, 5) // []int{0, 0, 0, 0, 0} (non-zero length but zero valued)

    Slices have a length, which is the number of elements they contain, and a capacity, which is the size of the underlying array they reference. The initial capacity is automatically set to match the initial length.

    If you append elements to a slice beyond its current capacity, Go automatically handles this by allocating a larger (2x) array and copying the existing elements into it at a new memory address. This can be an expensive operation in terms of performance.

    var slice []int // slice is initially nil (has no underlying array).
    fmt.Println(len(slice)) // 0
    fmt.Println(cap(slice)) // 0
    
    slice = append(slice, 1, 2, 3, 4)
    
    fmt.Println(len(slice)) // len: 4
    fmt.Println(cap(slice)) // cap: 4
    
    slice = append(slice, 5, 6)
    
    fmt.Println(len(slice)) // len: 6
    fmt.Println(cap(slice)) // cap: 8
    
    slice = append(slice, 7, 8, 9)
    
    fmt.Println(len(slice)) // len: 9
    fmt.Println(cap(slice)) // cap: 16

    To improve performance, we can predefine the capacity of a slice. Predefining the capacity helps avoid unnecessary reallocations when appending elements.

    slice := make([]int, 0, 20) // the third argument specifies capacity.
    
    fmt.Println(slice)      // []
    fmt.Println(len(slice)) // len: 0
    fmt.Println(cap(slice)) // cap: 20

    Predefining the capacity manually only makes sense when you have a reasonable knowledge of how the data will grow or change over time.

  • Append:

    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5, 6)
    fmt.Println(slice) // [1 2 3 4 5 6]

    Never use append on anything other than itself.

    func main() {
      a := make([]int, 5, 7)
      fmt.Println("a:", a) // a: [0 0 0 0 0]
    
      b := append(a, 1)
      fmt.Println("b:", b) // b: [0 0 0 0 0 1]
    
      c := append(a, 2)
    
      fmt.Println("a:", a) // a: [0 0 0 0 0]
      fmt.Println("b:", b) // b: [0 0 0 0 0 2] (b got updated because of c)
      fmt.Println("c:", c) // c: [0 0 0 0 0 2]
    }

    Here, when creating the b slice, the a slice has a capacity of 7 and a length of 5, which means it can add a new element without allocating a new array. So, b now references the same underlying array as a. The same thing happens when creating c. It also references the same array as a. At this point, because both b and c share the same underlying array, appending 2 through c updates the 1 that was appended through b.

    This unexpected behavior would not occur if there were not enough capacity for the new element. In that case, Go would allocate a new array and copy the existing elements to it, resulting in new addresses. But still, it is prone to go unexpected.

  • Prepend:

    slice := []int{1, 2, 3, 4, 5}
    slice = append([]int{99, 98}, slice...)
    fmt.Println(slice) // [99 98 1 2 3 4 5]

    The ... is called the variadic expansion (or ellipsis) operator, and it expands a slice into individual elements.

    slice := []int{2, 4, 6}
    otherSlice := []int{1, 3, 5}
    
    slice = append(slice, otherSlice...)
    
    fmt.Println(slice) // [2 4 6 1 3 5]
  • Remove:

    Remove all elements except the first two:

    slice = slice[:2]

    Remove the last element:

    slice = slice[:len(slice)-1]

    Remove a specific indexed element:

    slice = append(slice[:2], slice[3:]...) // removes the element at index 2

Extra:

  • Multi-dimensional slices:

    slice2d := [][]string{
      {"a", "b", "c"},
      {"d", "e"},
      {"f", "g"},
    }
    
    slice2d[1][1] = "x"
    fmt.Println(slice2d) // [[a b c] [d x] [f g]]
  • Full slice expression:

    With a normal slice expression (x[low:high]), the resulting slice inherits the remaining cap(x)-low capacity of the underlying array.

    a := []int{1, 2, 3, 4, 5}
    
    b := a[1:2]
    
    fmt.Printf("a:%v, len:%v, cap:%v", a, len(a), cap(a)) // a:[1 2 3 4 5], len:5, cap:5
    fmt.Printf("b:%v, len:%v, cap:%v", b, len(b), cap(b)) // b:[2], len:1, cap:4

    With a full slice expression (x[low:high:max]), you can control that capacity explicitly (max-low).

    c := a[1:2:3]
    
    fmt.Printf("c:%v, len:%v, cap:%v", c, len(c), cap(c)) // c:[2], len:1, cap:2

Maps

Maps in Go are unordered collections of key-value pairs.

Syntax:

map[keyType]valueType
  • Initialize:

    m := make(map[string]int)

    or

    m := map[string]int{}

    or with values:

    m := map[string]int{
    	"one":   1,
    	"two":   2,
    	"three": 3,
    }

    You cannot just declare a map with var m map[string]int and then assign values to it. If you try, you will get a panic: assignment to entry in nil map. That is why you need to initialize the map first, either empty or with values as shown above before using it.

  • Access:

    fmt.Println(m["one"]) // 1
  • Insert/Modify:

    m["four"] = 4
    fmt.Println(m) // map[four:4 one:1 three:3 two:2]
  • Remove:

    delete(m, "two")
    fmt.Println(m) // map[one:1 three:3]

Extra:

  • Clear all elements from a map:

    clear(m)
    fmt.Println(m) // map[]
  • Check if a key exists in a map:

    The optional second return value is a boolean indicating whether the key was found.

    val, ok := m["two"]
    fmt.Println(ok) // true

    Maps always return a value for a key, even if the key does not exist. If you access a missing key, Go returns the zero value for the map's value type.

    m := map[string]int{
      "zero": 0,
      "one":  1,
      "two":  2,
    }
    
    fmt.Println(m["three"]) // 0
    
    m["x"]++
    
    fmt.Println(m) // map[one:1 two:2 x:1 zero:0]
  • Nested maps:

    m2d := make(map[string]map[string]int)
    
    m2d["a"] = map[string]int{"first": 1}
    
    fmt.Println(m2d) // map[a:map[first:1]]
    
    fmt.Println(m2d["a"]["first"]) // 1

    Maps must be initialized before use.

    func main() {
      m2d := make(map[string]map[string]int)
    
      m2d["a"]["b"] = 1 // <-- panic: assignment to entry in nil map
    
      m2d["a"] = make(map[string]int)
    
      m2d["a"]["b"] = 1 // <- ok
    
      fmt.Println(m2d["a"]["b"]) // 1
    }

Structs

A struct is a collection of uniquely named elements called fields, each of which has a name and a type.

  • Define:

    type person struct {
      name string
      age  int
    }
  • Create an instance:

    user := person{name: "John", age: 35}

    Or zero valued:

    var user person // { 0}
  • Access:

    fmt.Println(user.name) // John
  • Modify:

    user.age = 30
    
    fmt.Println(user.age) // 30

Extra:

  • Embedded and nested structs:

    type address struct {
      city    string
      state   string
      zipCode string
    }
    
    type contact struct {
      phone string
      email string
    }
    
    type person struct {
      name    string
      age     int
      address         // Embedded struct
      contact contact // Nested struct
    
    }
    
    func main() {
      user := person{
        name: "John",
        age:  35,
        address: address{
          city:    "Los Angeles",
          state:   "California",
          zipCode: "00000",
        },
        contact: contact{
          phone: "(000) 000-0000",
          email: "john@example.com",
        },
      }
    
      fmt.Println(user)               // {John 35 {Los Angeles California 00000} {(000) 000-0000 john@example.com}}
      fmt.Println(user.city)          // Los Angeles
      fmt.Println(user.contact.phone) // (000) 000-0000
    }

Struct Tags

Struct tags are metadata attached to struct fields. They are typically used by packages like encoding/json to control how fields are encoded or decoded.

By default, Go uses struct field names as they are when encoding to JSON or other formats. Suppose you need to export the fields, which requires the field names to be capitalized. This often results in capitalized field names in the JSON output, which may not match your desired JSON structure or naming convention. Struct tags allow you to specify how the fields should be named in other formats.

type User struct {
	Id    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func main() {
	myUser := User{
		Id: 1, Name: "John", Email: "hello@example.com",
	}

	fmt.Printf("myUser: %+v\n", myUser) // myUser: {Id:1 Name:John Email:hello@example.com}

	jUser, _ := json.Marshal(myUser) // Converting struct to JSON

	fmt.Printf("jUser: %+v\n", string(jUser)) // jUser: {"id":1,"name":"John","email":"hello@example.com"}
}

Tags use backticks and the format: key:"value", and are not limited to json.

On this page