Go Notes of Array, Slice, Struct and Map (2)
Go Notes of Array, Slice, Struct and Map

Go Notes of Array, Slice, Struct and Map (2)

2020, Nov 26    

Array

Defination

An array is a contiguous collection of elements of the same type. It is an ordered sequence of elements stored contiguously in memory

Array are value type in go. So an array variable name is not a pointer to the first element in fact it denotes the entire array. A copy of the array will be created when

  • An array variable is assigned to another array variable.

  • An array variable is passed as an argument to a function.

Declaration of an array

Both number of elements and actual elements are optional in the array declaration.

In below example, we see 4 ways of declaring of an array

  • Specifying both the length of the array and actual elements. Eg.
[2]int{1, 2}
  • Only length – In this case all the actual elements are filled up with default value zero of that type. Eg
[2]int{}
  • Only actual elements – In this case, the length of array will be equal to the number of actual elements. The symbol ‘…’ needs to be used within square brackets like this […] when not specifying the length. The symbol is an instruction to the compiler to calculate the length.
[...]int{2, 3}
  • Without length and actual elements – an empty array will be created in this case. Similar to above the symbol ‘…’ also needs to be used in this case as well.
[...]int{}

Let’s see a code example illustrating above points. Also please keep in mind that the builtin function len() can be used to calculate the length of an array. In below program we are using len() function to calculate the length of the array.

package main

import "fmt"

func main() {
    //Both number of elements and actual elements
    sample1 := [2]int{1, 2}
    fmt.Printf("Sample1: Len: %d, %v\n", len(sample1), sample1)

    //Only actual elements
    sample2 := [...]int{2, 3}
    fmt.Printf("Sample2: Len: %d, %v\n", len(sample2), sample2)

    //Only number of elements
    sample3 := [2]int{}
    fmt.Printf("Sample3: Len: %d, %v\n", len(sample3), sample3)

    //Without both number of elements and actual elements
    sample4 := [...]int{}
    fmt.Printf("Sample4: Len: %d, %v\n", len(sample4), sample4)

    sample5 := [4]int{5, 8}
    fmt.Printf("Sample5: Len: %d, %v\n", len(sample5), sample5)
}

Output

Sample1: Len: 2, [1 2]
Sample2: Len: 2, [2 3]
Sample3: Len: 2, [0 0]
Sample4: Len: 0, []
Sample5: Len: 4, [5 8 0 0]

Different ways of iterating an array

An array can be iterated using:

  • Using for loop
  • Using for-range loop
  • Let’s see a code example for both
package main

import "fmt"

func main() {
    letters := [3]string{"a", "b", "c"}
    //Using for loop
    fmt.Println("Using for loop")
    len := len(letters)
    for i := 0; i < len; i++ {
        fmt.Println(letters[i])
    }
    //Using for-range operator
    fmt.Println("\nUsing for-range loop")
    for i, letter := range letters {
        fmt.Printf("%d %s\n", i, letter)
    }
}

Output

Using for loop
a
b
c

Using for-range loop
0 a
1 b
2 c

Slice

Defination

A slice points to an underlying array and is internally represented by a slice header. Unlike array, the size of a slice is flexible and can be changed.

Internally a slice is represented by three things.

  • Pointer to the underlying array

  • Current length of the underlying array

  • Total Capacity which is the maximum capacity to which the underlying array can expand.

The Pointer field in the slice header is a pointer to the underlying array. Len is the current length of the slice and Cap is the capacity of the slice. Similar to array a slice index starts from zero till length_of_slice-1. So a slice of 3 lengths and 5 capacity will look like below

a slice of 3 lengths and 5 capacity

We also have two library functions provided by go which can be used to know the length and capacity of a slice.

  • len() function – for length of the slice

  • cap() function – for capacity of the slice

Using the make function

make is a builtin function provided by go that can also be used to create a slice. Below is the signature of make function

func make([], length, capacity int) []

Capacity is an optional parameter while creating slice using the make function. When capacity is omitted, the capacity of the slice is equal length specified for the slice. When using make function, behind the scenes go allocates an array equal to the capacity. All the elements of the allocated array are initialized with default zero value of the type. Let’s see a program illustrating this point.

Length vs Capacity

Before moving further, let’s emphasis on understanding the caveats of length and capacity. Let’s create a simple slice with capacity greater than length.

numbers := make([]int, 3, 5)

Accessing the slice behind its length will result in a run time error “Index out of range”. It doesn’t matter if the accessed index is within the capacity. So the below line will cause the run time error.

numbers[4] = 5

The length of the slice can be increased up to its capacity by re-slicing. So below re-slice will increase the length from 3 to 5.

numbers = numbers[0:5]

The length of the slice can also be decreased using re-slicing. So below re-slice will decrease the length from 3 to 2

numbers = numbers[0:2]

The advantage of having capacity is that array of size capacity can be pre-allocated during the initialization. This is a performance boost as if more elements are needed to include in this array then space is already allocated for them.

Accessing and Modifying Slice Elements

A slice element can be accessed by specifying the index. Slice element can also be allotted a new value using the index. Also, note that any changes in the underlying array will reflect back in the slice as we have also seen above. Let’s see a small example of accessing and modifying

Appending to a slice

go builtin package provides an append function that can be used to append to a slice at the end. Below is the signature of this function

func append(slice []Type, elems ...Type) []Type

numbers := []int{1,2}
// ATTENTION OF THE 'numbers' used in append
numbers := append(numbers, 4) //Slice will become [1, 2, 3, 4]

Copy a slice

go builtin package provides copy function that can be used to copy a slice. Below is the signature of this function. It takes in two slices dst and src, and copies data from src to dst. It returns the number of elements copied.

func copy(dst, src []Type) int

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 5)

numberOfElementsCopied := copy(dst, src)

Basically the number of elements copied is minimum of length of (src, dst).

Nil Slice

The default zero value of a slice is nil. The length and capacity both of a nil slice is zero. Though it is possible to append to a nil slice as well.

Map

Defination

Maps are golang builtin datatype similar to the hash table which maps a key to a value. Map is an unordered collection where each key is unique while values can be the same for two or more different keys. The advantages of using a map are that it provides fast retrieval, search, insert, and delete operations.

Maps are referenced data types. When you assign one map to another both refer to the same underlying map.

map[key_type]value_type
map[string]int

Both key_type and value_type can be of different type or same type. For below example the key type is string and value type is int.

Allowed Key types in a Map

The map key can be any type that is comparable. Some of the comparable types as defined by go specification are

  • boolean

  • numeric

  • string

  • pointer

  • channel

  • interface types

  • structs – if all it’s field type is comparable

  • array – if the type of value of array element is comparable

Some of the types which are not comparable as per go specification and which cannot be used as a key in a map are.

  • Slice

  • Map

  • Function

Map Operations

The below operations are applicable for map

  • Add a key-value pair

  • Update a key

  • Get the value corresponding to a key

  • Delete a key-value pair

  • Check if a key exists

Check if a key exists

Below is the format to check if a key exist in the map

val, ok := mapName[key]

Zero Value

zero value of a map is nil. This is also proved when we declare a map using the var keyword. See below program.

Maps are referenced data types

Map are reference data types. So on assigning one map to a new variable, then both variable refers to the same map. Any change in one of the map would reflect in other and vice versa.

Maps are not safe for concurrent use

go maps are not safe for concurrent use.

Buggy code: Below is a buggy code. It might result in crash if concurrent read and write of map happens.

package main

var (
   allData = make(map[string]string)
)

func get(key string) string {
    return allData[key]
}

func set(key string, value string) {
    allData[key] = value
}

func main() {
    go set("a", "Some Data 1")
    go set("b", "Some Data 2")
    go get("a")
    go get("b")
    go get("a")
}

Possible Output:

fatal error: concurrent map read and map write

Correct Code:

package main

import (
    "fmt"
    "sync"
)

var (
    allData = make(map[string]string)
    rwm     sync.RWMutex
)

func get(key string) string {
    rwm.RLock()
    defer rwm.RUnlock()
    return allData[key]

}

func set(key string, value string) {
    rwm.Lock()
    defer rwm.Unlock()
    allData[key] = value

}

func main() {
    set("a", "Some Data")
    result := get("a")
    fmt.Println(result)
}

Struct

Defination

GO struct is named collection of data fields which can be of different types. Struct acts as a container that has different heterogeneous data types which together represents an entity.

A struct in golang can be compared to a class in Object Oriented Languages

Declaring a struct type

type struct_name struct {
    field_name1 field_type1
    field_name2 field_type2
    ...
}

Pointer to a struct

There are two ways of creating a pointer to the struct

  • Using the & operator

  • Using the new keyword

The & operator can be used to get the pointer to a struct variable.

emp := employee{name: "Sam", age: 31, salary: 2000}
empP := &emp

OR

empP := &employee{name: "Sam", age: 31, salary: 2000}

Using the new() keyword will:

  • Create the struct

  • Initialize all the field to the zero default value of their type

  • Return the pointer to the newly created struct

empP := new(employee)

// Pointer address can be print using the %p format modifier
fmt.Printf("Emp Pointer: %p\n", empP)

// Deference operator ‘*’ can be used to print the value at the pointer.
fmt.Printf("Emp Value: %+v\n", *empP)

// Emp Value: {name: age:0 salary:0}

// When not using the dereference pointer but using the format identifier  %+v, then ampersand will be appended before the struct indicating that is a pointer.
fmt.Printf("Emp Value: %+v\n", empP)

// Emp Value: &{name: age:0 salary:0}

There are two ways to print all struct variables including all its key and values.

  • Using the fmt package

  • Printing the struct in JSON form using the json/encoding package. This also allows pretty print of a struct as well.

fmt.Printf() function can be used to print a struct. Different format identifiers can be used to print a struct in different ways.

emp := employee{name: "Sam", age: 31, salary: 2000}

// %v – It will print only values. Field name will not be printed. This is the default way of printing a struct
fmt.Printf("%v", emp)  -  {Sam 31 2000}

// %+v – It will print both field and value.
fmt.Printf("%+v", emp) - {name:Sam age:31 salary:2000}

Second method is to print the struct in the JSON format. Marshal and MarshalIndent function of encoding/json package can be used to print a struct in JSON format.

  • Marshal – Below is the signature of the Marshal function. This function returns the JSON encoding of v by traversing the value recursively
Marshal(v interface{}) ([]byte, error)
  • MarshalIndent– Below is the signature of the MarshalIndent function. It is similar to Marshal function but applies Indent to format the output. So it can be used to pretty print a struct
MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

It is to be noted that both Marshal and MarshalIndent function can only access the exported fields of a struct, which means that only the capitalized fields can be accessed and encoded in JSON form.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type employee struct {
    Name   string
    Age    int
    salary int
}

func main() {
    emp := employee{Name: "Sam", Age: 31, salary: 2000}
    //Marshal
    empJSON, err := json.Marshal(emp)
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Marshal funnction output %s\n", string(empJSON))

    //MarshalIndent
    empJSON, err = json.MarshalIndent(emp, "", "  ")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("MarshalIndent funnction output %s\n", string(empJSON))
}

Struct Field Meta or Tags

A struct in go also allows adding metadata to its fields. These meta fields can be used to encode decode into different forms, doing some forms of validations on struct fields, etc. So basically any meta information can be stored with fields of a struct and can be used by any package or library for different purposes.

type strutName struct{
   fieldName type `key:value key2:value2`
}

type employee struct {
    Name   string `json:"n"`
    Age    int    `json:"a"`
    Salary int    `json:"s"`
}

Let’s see full program

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type employee struct {
    Name   string `json:"n"`
    Age    int    `json:"a"`
    Salary int    `json:"s"`
}

func main() {
    emp := employee{Name: "Sam", Age: 31, Salary: 2000}
    //Converting to jsonn
    empJSON, err := json.MarshalIndent(emp, "", "  ")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Println(string(empJSON))
}

Anonymous Fields in a Struct

A struct can have anonymous fields as well, meaning a field having no name. The type will become the field name.

package main

import "fmt"

type employee struct {
    string
    age    int
    salary int
}

func main() {
    emp := employee{string: "Sam", age: 31, salary: 2000}
    //Accessing a struct field
    n := emp.string
    fmt.Printf("Current name is: %s\n", n)
    //Assigning a new value
    emp.string = "John"
    fmt.Printf("New name is: %s\n", emp.string)
}

Struct Equality

The first thing to know before considering struct equality is weather if all struct fields types are comparable or not

Some of the comparable types as defined by go specification are

  • boolean

  • numeric

  • string,

  • pointer

  • channel

  • interface types

  • structs – if all it’s field type is comparable

  • array – if the type of value of array element is comparable

Some of the types which are not comparable as per go specification and which cannot be used as a key in a map are.

  • Slice

  • Map

  • Function

So two struct will be equal if first all their field types are comparable and all the corresponding field values are equal.

Struct are value types

A struct is value type in go. So a struct variable name is not a pointer to the struct in fact it denotes the entire struct. A new copy of the struct will be created when

  • A struct variable is assigned to another struct variable.

  • A struct variable is passed as an argument to a function.

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp1 := employee{name: "Sam", age: 31, salary: 2000}
    fmt.Printf("Emp1 Before: %v\n", emp1)

    emp2 := emp1

    emp2.name = "John"
    fmt.Printf("Emp1 After assignment: %v\n", emp1)
    fmt.Printf("Emp2: %v\n", emp2)

    test(emp1)
    fmt.Printf("Emp1 After Test Function Call: %v\n", emp1)
}

func test(emp employee) {
    emp.name = "Mike"
    fmt.Printf("Emp in Test function: %v\n", emp)
}