Notessh2a

Data Structures

Arrays

In Go, an array is a fixed-size, ordered collection of elements of the same 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, 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, int is int64 (on a 64-bit system), so each element occupies 8 bytes. Element addresses 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 (slice expression):

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

    arr[low:high]

    • low: Starting index (inclusive).
    • high: 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]
    slice2 := arr[:4]  // [0 10 20 30]
    slice3 := arr[4:]  // [40]
    slice4 := arr[:]   // [0 10 20 30 40]

    Slicing does not copy elements. The new slice references 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 dynamic-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 make:

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

    Slices have a length (number of elements) and a capacity (size of the underlying array). The initial capacity defaults to the initial length.

    Appending beyond the current capacity allocates a larger (2x) array and copies elements to a new memory location. This is a performance-heavy operation.

    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
    
    /*
      len: 0, cap: 0    [] (nil slice)
      len: 1, cap: 1    [1] (allocation)
      len: 2, cap: 2    [1 2] (reallocation)
      len: 3, cap: 4    [1 2 3] (reallocation)
      len: 4, cap: 4    [1 2 3 4]
      len: 5, cap: 8    [1 2 3 4 5] (reallocation)
      len: 6, cap: 8    [1 2 3 4 5 6]
      len: 7, cap: 8    [1 2 3 4 5 6 7]
      len: 8, cap: 8    [1 2 3 4 5 6 7 8]
      len: 9, cap: 16   [1 2 3 4 5 6 7 8 9] (reallocation)
    */

    Predefining capacity improves performance by avoiding unnecessary reallocations.

    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 capacity only makes sense when you have 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, a has remaining capacity, b and c share the same backing array. Appending through one affects the other.

    This unexpected behavior would not occur if there was 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 ... (ellipsis/expansion) operator 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:

    Keep only the first two:

    slice = slice[:2]

    Remove the last element:

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

    Remove a specific index:

    slice = append(slice[:2], slice[3:]...) // removes 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

    Full slice expression (x[low:high:max]) allows explicit capacity control (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,
    }

    Maps must be initialized first (empty or with values) to make them ready to use. Declaring a map with var m map[string]int and then assigning values causes a panic: assignment to entry in nil map.

  • Access:

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

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

    Can safely be used even on a non-existent key.

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

Extra:

  • Clear all elements:

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

    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. Accessing a missing key returns the zero value of the map 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

    Inner maps must also 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 fields, each with a name and a type.

  • Define:

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

    user := person{name: "John", age: 35}
    Any field that is not explicitly specified receives the zero value of its type.

    Or without field names:

    user := person{"John", 35}

    Values must follow declaration order and include every field.

    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:

    • The embedded struct's fields are promoted to the outer struct and can be accessed directly as if they belong to it.
    • The nested struct remains a separate field and must be accessed through its field name.
    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: "[email protected]",
        },
      }
    
      fmt.Println(user)               // {John 35 {Los Angeles California 00000} {(000) 000-0000 [email protected]}}
      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 encoding and decoding behavior.

By default, Go encodes fields using their names. When fields must be capitalized but require different names in output formats, struct tags define the external representation.

type User struct {
	Id       int    `json:"id"`                   // same but lowercase
	Name     string `json:"full_name"`            // renamed field
	Email    string `json:"email,omitempty"`      // omitted if empty ("")
	Password string `json:"-"`                    // ignored completely
	Age      int    `json:"age,string,omitempty"` // encoded as string
	Salary   int    `json:"salary,omitempty"`     // omitted if 0
	Score    *int   `json:"score,omitempty"`      // nil omitted, 0 still included
}

func main() {
	score := 0

	myUser := User{
		Id: 1, Name: "John", Email: "[email protected]", Password: "12345", Age: 26, Salary: 0, Score: &score,
	}

	fmt.Printf("myUser: %+v\n", myUser) // myUser: {Id:1 Name:John Email:[email protected] Password:12345 Age:26 Salary:0 Score:0x255956d3e048}

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

	fmt.Printf("jUser: %+v\n", string(jUser)) // jUser: {"id":1,"full_name":"John","email":"[email protected]","age":"26","score":0}
}

The json package only accesses the exported fields of struct types (those that begin with an uppercase letter).

omitempty can hide valid values like 0 or false. Using a pointer with omitempty omits only nil while keeping other values.

On this page