Chapter03 - pslpune/golang-jumpstart GitHub Wiki

Arrays, slices, maps & structs

Now things are getting a bit serious, slices and maps are used quite frequently in all real world solutions. While structs is as far as Go can venture into Object oriented programming. All the above programming constructs will be vital for your understanding of the language.

Arrays

Arrays is an aggregate of elements of the same type. Go does not allow mixing of types in an array. Plus the Go compiler will always need the size of the array before runtime. Hence [n]T denotes an array of size n having elements of type T.

[3,4,5,6,7,8] // valid array 
[bool, "niranjan", 3] // this aggregation is not regarded as array

package main

import (
	"fmt"
)

// this has array of int with size 4 elements.
func main() {
	var a [4]int 
	fmt.Println(a)
}
[0 0 0 0]

Next question - can you change the elements of an array given the index ?

Yes! - but please note that the indices for the array starts at 0. Below we notice how an array can changed of its element value using the indices as well as the short hand declaration is possible. When initilizing notice how the elements are inside the {} braces. If the initilization expression has lesser elements than size - the remaining elements would then be initilized to 0 values. On the other side if we initialize with more values it would result in compile error. With the ... ellipsis we are indicating that arrays like need the compiler to refer to the elements in the initilization expression to know the size. The size of the array is part of the type declaration and there has to be a way for the compiler to determine it. - explicitly / implicitly

package main

import (
	"fmt"
)
func main() {
	var a [4]int 
	a[0] = 10 
	a[1] = 20
	a[2] = 30
    // we do not modify the 4th element of thearray to demonstrate there isnt something called undefined in arrays.
    // All the elements if not exclusively assigned would still have the zero value.
	fmt.Println(a)

    a = [3]int{538, 0152, 6} // short hand declaration to create array
	

    a := [...]int{28, 3, 82}
    fmt.Println(a)
}
[10 20 30 0]
[538 0152 6]
[28 3 82]

Arrays are value types

When one variable of type array is assigned to another variable of the same type, you get a copy of the underlying array. This denotes that arrays are value types as against the common belief that they are refernce types.

package main

import "fmt"

func main() {
	a := [...]string{"Pune", "Bangalore", "Mysore", "Delhi", "Mumbai"}
	b := a // a copy of a is assigned to b
	b[0] = "Chandigarh"
	fmt.Println("a is ", a)
	fmt.Println("b is ", b)	
}
a is [Pune Bangalore Mysore Delhi Mumbai]
b is [Chandigarh Bangalore Mysore Delhi Mumbai]

As you can notice above, when b is assigned the value and then changed the first item in the array. Had this been a reference type assignment it woudl have changed the Pune value to Chandigarh already. Below this you can notice arrays are passed by value as function parameters.

package main

import "fmt"

func changeLocal(num [5]int) {
	num[0] = 55
	fmt.Println("inside function ", num)

}
func main() {
	num := [...]int{5, 6, 7, 8, 8}
	fmt.Println("before passing to function ", num)
	changeLocal(num) //num is passed by value
	fmt.Println("after passing to function ", num)
}
before passing to function  [5 6 7 8 8]
inside function  [55 6 7 8 8]
after passing to function  [5 6 7 8 8]

Length of an array

package main

import "fmt"

func main() {
	a := [...]float64{67.7, 89.8, 21, 78}
	for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
		fmt.Printf("%d th element of a is %.2f\n", i, a[i])
	}
}
0 th element of a is 67.70
1 th element of a is 89.80
2 th element of a is 21.00
3 th element of a is 78.00

Getting the length of an array is simple call to len() function.

Multi dimensional arrays

package main

import (
	"fmt"
)

func printarray(a [3][2]string) {
	for _, v1 := range a {
		for _, v2 := range v1 {
			fmt.Printf("%s ", v2)
		}
		fmt.Printf("\n")
	}
}

func main() {
	a := [3][2]string{
		{"lion", "tiger"},
		{"cat", "dog"},
		{"pigeon", "peacock"}, //this comma is necessary. The compiler will complain if you omit this comma
	}
	printarray(a)
	var b [3][2]string
	b[0][0] = "apple"
	b[0][1] = "samsung"
	b[1][0] = "microsoft"
	b[1][1] = "google"
	b[2][0] = "AT&T"
	b[2][1] = "T-Mobile"
	fmt.Printf("\n")
	printarray(b)
}
lion tiger 
cat dog 
pigeon peacock 

apple samsung 
microsoft google 
AT&T T-Mobile 

Slices

Slices are a convenient, flexible thin wrapper atop arrays. Slices do not own data, they just reference an array underneath. When using the slicing operator [:] left index is inclusive right index is exclusive.

Example: [1:3] would mean the slice is from index 1 to 3 in inclusive of 1, but not including 3

package main

import (
	"fmt"
)

func main() {
	undrlArray := [5]int{76, 77, 78, 79, 80}
	var slceExample []int = a[1:4] //creates a slice from a[1] to a[3]
	fmt.Println(b)

    alsoASlice := []int{77, 78, 79}
    fmt.Println(c)
}

Modifying slices

As mentioned before slices do not own any data, and any changes to elements of the slices would mean the underlying array has those changes too. Since slices are just references to an underlying array, there can be multiple slices referencing a single array. Changes to any one reference would mean all the refernces get the changes.

package main

import (  
    "fmt"
)

func main() {  
    anArray := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    aSlice := anArray[2:5]
    fmt.Println("array before",aSlice)
    for i := range aSlice {
        aSlice[i]++
    }
    fmt.Println("array after",anArray) 
}
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
package main

import (
	"fmt"
)

func main() {
	numArr := [3]int{78, 79 ,80}
	nums1 := numArr[:] //creates a slice which contains all elements of the array
	nums2 := numArr[:]
	fmt.Println("array before change 1",numArr)
	nums1[0] = 420
	fmt.Println("array after modification to slice nums1", numArr)
	nums2[1] = 200
	fmt.Println("array after modification to slice nums2", numArr)
}
array before change 1 [78 79 80]
array after modification to slice nums1 [420 79 80]
array after modification to slice nums2 [420 200 80]

Length & Capacity of a slice

Length of the slice is the number of elements in the slice while the capacity is the number of elements in the underlying array starting from the index of the slice

package main

import (
	"fmt"
)

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of fruitslice is 2 and capacity is 6
    // any reslicing to the capacity of slice is legal, beyond that it would mean we have an runtime error 
    fruistlice[:cap(fruitslice)]
}
 length of slice 2 capacity 6.

Appending to the slice

We already know the underlying array of the slices are of fixed length, but slices can grow and even shrink. When you append any more elements to a slice that has reached its capacity, the underlying array is copied into a new array of double size, reference of this array is then given to the slice.

package main

import (
	"fmt"
)

func main() {
	tzs := []string{"America/Tegucigalpa", "Africa/Cairo", "Europe/Prague", "Asia/Jakarta", "Asia/Manila"}
	fmt.Println("timezones:", tzs, "has old length", len(tzs), "and capacity", cap(tzs)) 
	tzs = append(tzs, "Africa/Johannesburg")
	fmt.Println("timezones:", tzs, "has new length", len(tzs), "and capacity", cap(tzs)) 
}
tzs: [America/Tegucigalpa, Africa/Cairo, Europe/Prague, Asia/Jakarta, Asia/Manila] has old length 3 and capacity 3
tzs: [America/Tegucigalpa, Africa/Cairo, Europe/Prague, Asia/Jakarta, Asia/Manila Africa/Johannesburg] has new length 4 and capacity 6

Here comes the interesting part : nil slices still have a capacity and length. len =0 cap=0. Hence when we append to a nil slice it tolerates this and adds new elements to the slice. This is counter intuitive since adding a new elements to nil SHOULD HAVE lead to an runtime error.

package main

import (
	"fmt"
)

func main() {
	var names []string //zero value of a slice is nil
	if names == nil {
		fmt.Println("slice is nil going to be appended")
		names = append(names, "[email protected]", "[email protected]", "[email protected]")
		fmt.Println("email contents:",names)
	}
}
slice is nil going to append
names contents: [[email protected], [email protected], [email protected]]

Its also possible to append one slice to another, since append() uses the variadic parameter approach.

package main

import (
	"fmt"
)

func main() {
	domains := []string{"bigcartel.com","trellian.com","github.io"}
	moreDomains := []string{"usa.gov","mlb.com"}
	domains := append(domains, moreDomains...)
	fmt.Println("all domains:",domains)
}

Memory optimization

A case when we have sliced a section of a large array, even though you might be working with only the slice, since the slice is referencing the array, the large array continues to be in memory. Is not garbage collected One way to get around the problem is to make a copy of the slice so that the original slice and the underlying array are garbage collected.

package main

import (
	"fmt"
)

func countries() []string { 
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
	neededCountries := countries[:len(countries)-2]
	countriesCpy := make([]string, len(neededCountries))
	copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
	return countriesCpy
}
func main() {
	countriesNeeded := countries()
	fmt.Println(countriesNeeded)
}

Maps

Maps is a built in type in Go, that uses key-value pairs to store data. Analogous to dictionaries in Python. While arrays identify the elements within it using the numerical index, maps can use custom data type keys to pick on elements. As you can imagine that can save you a for loop and instead can let you access the elements directly using the keys.

Here is how you can create a map.

package main

import (
	"fmt"
)

func main() {
	cityScore := make(map[string]int)
    cityScore["pune"] = 9
    cityScore["mumbai"] = 9
    cityScore["mysore"] = 5
    cityScore["bangalore"] = 2
    cityScore["delhi"] = 0
	fmt.Println(cityScore)

    // with the short hand declaration

    cityScore := map[string]int {
        "pune" :9,
        "mumbai" :9,
        "mysore" :9,
        "bangalore" :9,
        "delhi" :9,
    }
}

Adding items to the map

In the [] notation one can specify the key and the value then can be just an assignment.

package main

import (
	"fmt"
)

func main() {
	employeeYoe := make(map[string]int)
	employeeYoe["niranjan"] = 16
	employeeYoe["rupesh"] = 16
	employeeYoe["niharika"] = 16
	employeeYoe["prathamesh"] = 16
	fmt.Println("employeeSalary map contents:", employeeYoe)
}

Zero value of the map

Zero value of any map is nil & adding any element to the map results in a runtime panic. This is in contrast with array as append function counter-intuitively addeed a new element despite not being initialized.

package main

func main() {
	var employeeSalary map[string]int
	employeeSalary["steve"] = 12000
}

Retreiving values by keys

A simple map[key] notation can get you the value as desired. If the key does not exists, map would emit the "zero value" of the value. Like you can notice in below example, map["niranjan"] does not exists, hence salary value would be 0

package main

import (
	"fmt"
)

func main() {
	employeeSalary := map[string]int{
		"steve": 12000,
		"jamie": 15000,
        "mike": 9000,
	}
	employee := "jamie"
    salary := employeeSalary[employee]
	fmt.Println("Salary of", employee, "is", salary)

    employee = "niranjan"
    salary := employeeSalary[employee]
	fmt.Println("Salary of", employee, "is", salary)
}
Salary of jamie is 15000
Salary of niranjan is 0

Checking for key existence

Maps can be quried for existence of keys and such is a simple operation. When using the map[key] operator you can opt to take 2 return values and the last one is boolean indicator for key existence.

package main

import (
	"fmt"
)

func main() {
	salaried := map[string]int{
		"sumit": 12000,
		"prathamesh": 15000,
	}
	newEmp := "niranjan"
	value, ok := salaried[newEmp]
	if ok == true {
		fmt.Println("Salary of", newEmp, "is", value)
		return
	}
	fmt.Println(newEmp, "not found")
}

Iterating over the map using range

package main

import (
	"fmt"
)

func main() {
	foodApps := map[string]int{
		"swiggy": 30,
		"zomato": 28,
		"uber":  31,
		"box8":  27,
	}
	fmt.Println("Margins for various food apps..")
	for key, value := range foodApps {
		fmt.Printf("foodApps[%s] = %d\n", key, value)
	}

}
Margins for various food apps..
foodApps[swiggy] = 30
foodApps[zomato] = 28
foodApps[uber] = 31
foodApps[box8] = 27

Deleting items & length of maps

A simple call to the delete function can do the deletion. Ofcourse the key in this case is important

package main

import (
	"fmt"
)

func main() {
	employeeSalary := map[string]int{
		"steve": 12000,
		"jamie": 15000,		
        "mike": 9000,
	}
	fmt.Println("map before deletion", employeeSalary)
	delete(employeeSalary, "steve")
	fmt.Println("map after deletion", employeeSalary)
    fmt.Println("length is", len(employeeSalary))
}
map before deletion map[steve:12000 jamie:15000 mike:9000]
map after deletion map[mike:9000 jamie:15000]
length is 2

Maps are reference types

Similar to slices, maps are reference types. When a map is assigned to a new variable, they both point to the same internal data structure. Hence changes made in one will reflect in the other.

package main

import (
	"fmt"
)

func main() {
	employeeSalary := map[string]int{
		"steve": 12000,
		"jamie": 15000,		
		"mike": 9000,
	}
	fmt.Println("Original employee salary", employeeSalary)
	modified := employeeSalary
	modified["mike"] = 18000
	fmt.Println("Employee salary changed", employeeSalary)

}
Original employee salary map[jamie:15000 mike:9000 steve:12000]
Employee salary changed map[jamie:15000 mike:18000 steve:12000]

Maps and equality

Maps cannot be compared using the == operator, the only usage is that of comparing to see if the map is nil. If you still want to deep compare the maps you can use the for loop and compare all the individual elements.

package main

func main() {  
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }
    map2 := map1
    if map1 == map2 {
    }
}
invalid operation: map1 == map2 (map can only be compared to nil)

References