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)