This continues from go back to basics.
We shall learn about important data structures Go has to offer: arrays, slices and maps.
Arrays:
Arrays are counterparts of C arrays. It is a fixed length , fixed type data structure having contiguous block of elements. Elements can be of inbuilt data types or a struct type. An array is declared as below:
var arr [10]int
: This declares an integer array variable of fixed size 10 of name arr
. Arrays are helpful as they provide locality of reference and are quick to access randomly. This declaration initializes all 10 elements to their zero value, which is 0 for int.
Or we can use short form as arr:= [10]int
. We can initialize arrays to specific values using a literal , arr:=[3]int{1,2,3}
We can access array elements using indexing operator []
. arr[2]=6
assigns 6 to 3rd element of arr. We can assign an array to another if their type and size match.
An important note when sending arrays between functions. Array is a value in Golang and when we pass it by value, we allocate size on stack and entire array will be copied to the location. When we have very large arrays this becomes an important bottleneck. We can avoid this by passing array by reference using array point.
some_func(&myarr)
///definition
func some_func(arr *[1000]int){
///blah
}
Now copy is just limited to 4 bytes of pointer data. But now both caller and callee sees the changes if made to the array as we are modifying the value at location.
Golang encourages composition. Though arrays are always single dimensional, we can compose multi dimensional arrays as below:
var twodimarray [2][2]int
twodimarray[0][0] = 1
twodimarray[0][1] = 2
...
Slices
Slices implement dynamic arrays that grow and shrink in size. Internally they refer an array. A slice has 3 fields internally: a pointer to the underlying array, length of the slice and capacity of the slice. Length can be same as capacity but not necessarily.
var slc []int
is an example of slice of type int. This is a nil slice which points to a nil array pointer and has size and capacity 0.
We usually use make
to create a slice :
slc := make([]string,2,5)
//a slice of strings with size 2 and capacity 5, which means
//underlying array has 5 elements but the slice has size 2
slc[1] = "mystring"
Accessing elements is same as that of an array using indexing operator. We can use slices to create new slices as below:
//size of 8 and capacity of 8
slice := []int{1,2,3,4,5,6,7,8}
//now slice up from this that starts at 2nd index of underlying //array
newslice := slice[2:4]
Now slices slice
and newslice
share same underlying array and modification result will be seen across slices. slice
has size and capacity of 5 and underlying array has size 5. Now when we have a new slice off this , newslice
refers to underlying array starting from index 2. Size of newslice
is 2 and capacity is 6. Between slice
and newslice
, elements starting from index 2 of the underlying array are shared. newslice
doesn’t even have access to elements at index 0 and 1.
Say we have a slice slc[a:b]
and the underlying array of size n. In that case, length of slc is b-a and capacity is n-a.
Now for the nuance of different size and capacity , capacity is the upper limit for size when we grow a slice. Slice can be grown using append
function. append
results in increased size . It can increase capacity if appended value results in overflow of the capacity.
slice = append(slice,2,3,4)
appends numbers 2 ,3 and 4 to slice
and reassigns it to slice
so that now size of slice
is increased by 3. If no capacity is available for this, append creates a new array, copy existing elements and then appends the new value and the slice now refers to this new underlying array.
To iterate over slices, we use range
along with for
.
for idx,val := range slice{
fmt.Printf("%d :%d ",idx,val)
}
To pass slices between functions, we need 4+4+4 = 12 bytes only, irrespective of size of underlying array. This is for pointer to underlying array and 4 each for length and capacity fields.This is way more efficient than passing array by value. So even if we pass slice by value, 12 bytes is efficient and fast.
Maps
Maps are dictionary kind of structures which allow us to store and retrieve key-value pairs in an iterable way. But they are unordered structures. The underlying implementation of map is hash tables which are implemented as collection of buckets. Restrictions for map are that the value
type must work with ==
operator for it to be part of map and slices or functions or struct types with slices cannot be used as keys.
To create a map, we can use make
.
mymap := make(map[int]string)//a map of key an int and value as strings
Or we can use literals as below:
mymap := map[int]string{1:"hey",2:"brother"}
maps can be used with indexing operator or with for
and range
mmap := map[string][string]
mmap["Blue"]= "Nice"
///to check if exists
value,exists := mmap["Blue"]
if exists {
fmt.Println("Present")
}
for key,value :=range mmap{
fmt.Println(value)
}
///to remove a value
delete(mmap,"Blue")
One important point to note is when passing maps between functions, implicitly maps are passed by reference and changes are seen across functions.This is efficient handling since usually maps are of big sizes.
Conclusion
This article covered 3 important data structures that Go provides us: arrays, slices and maps. These along with numbers , strings and booleans cover major built in data types that are used in regular programming. We will cover structs which provide way of creating user defined data in next article and cover type system of Go which provides an alternative for object orientation for Golang programs.