Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Go for DevOps

You're reading from   Go for DevOps Learn how to use the Go language to automate servers, the cloud, Kubernetes, GitHub, Packer, and Terraform

Arrow left icon
Product type Paperback
Published in Jul 2022
Publisher Packt
ISBN-13 9781801818896
Length 634 pages
Edition 1st Edition
Languages
Tools
Concepts
Arrow right icon
Authors (2):
Arrow left icon
John Doak John Doak
Author Profile Icon John Doak
John Doak
David Justice David Justice
Author Profile Icon David Justice
David Justice
Arrow right icon
View More author details
Toc

Table of Contents (22) Chapters Close

Preface 1. Section 1: Getting Up and Running with Go
2. Chapter 1: Go Language Basics FREE CHAPTER 3. Chapter 2: Go Language Essentials 4. Chapter 3: Setting Up Your Environment 5. Chapter 4: Filesystem Interactions 6. Chapter 5: Using Common Data Formats 7. Chapter 6: Interacting with Remote Data Sources 8. Chapter 7: Writing Command-Line Tooling 9. Chapter 8: Automating Command-Line Tasks 10. Section 2: Instrumenting, Observing, and Responding
11. Chapter 9: Observability with OpenTelemetry 12. Chapter 10: Automating Workflows with GitHub Actions 13. Chapter 11: Using ChatOps to Increase Efficiency 14. Section 3: Cloud ready Go
15. Chapter 12: Creating Immutable Infrastructure Using Packer 16. Chapter 13: Infrastructure as Code with Terraform 17. Chapter 14: Deploying and Building Applications in Kubernetes 18. Chapter 15: Programming the Cloud 19. Chapter 16: Designing for Chaos 20. Index 21. Other Books You May Enjoy

Using arrays and slices

Languages require more than the basic types to hold data. The array type is one of the core building blocks in lower-level languages, providing the base sequential data type. For most day-to-day use, Go's slice type provides a flexible array that can grow as data needs grow and can be sliced into sections in order to share views of the data.

In this section, we will talk about arrays as the building blocks of slices, the difference between the two, and how to utilize them in your code.

Arrays

The base sequential type in Go is the array (important to know, but rarely used). Arrays are statically sized (if you create one that holds 10 int types, it will always hold exactly 10 int types).

Go provides an array type designated by putting [size] before the type you wish to create an array of. For example, var x [5]int or x := [5]int{} creates an array holding five integers, indexed from 0 to 4.

An assignment into an array is as easy as choosing the index. x[0] = 3 assigns 3 to index 0. Retrieving that value is as simple as referring to the index; fmt.Println(x[0] + 2) will output 5.

Arrays, unlike slices, are not pointer wrapper types. Passing an array as a function argument passes a copy:

func changeValueAtZeroIndex(array [2]int) {
     array[0] = 3
     fmt.Println("inside: ", array[0]) // Will print 3
}
func main() {
     x := [2]int{}
     changeValueAtZeroIndex(x)
     fmt.Println(x) // Will print 0
}

Arrays present the following two problems in Go:

  • Arrays are typed by size – [2]int is distinct from [3]int. You cannot use [3]int where [2]int is required.
  • Arrays are a set size. If you need more room, you must make a new array.

While it is important to know what arrays are, the most common sequential type used in Go is the slice.

Slices

The easiest way to understand a slice is to see it as a type that is built on top of arrays. A slice is a view into an array. Changing what you can see in your slice's view changes the underlying array's value. The most basic use of slices acts like arrays, with two exceptions:

  • A slice is not statically sized.
  • A slice can grow to accommodate new values.

A slice tracks its array, and when it needs more room, it will create a new array that can accommodate the new values and copies the values from the current array into the new array. This happens invisibly to the user.

Creating a slice can be done similarly to an array, var x = []int or x := []int{} . This creates a slice of integers with a length of 0 (which has no room to store values). You can retrieve the size of the slice using len(x).

We can create a slice with initial values easily: x := []int{8,4,5,6}. Now, we have len(x) == 4, indexed from 0 to 3.

Similar to arrays, we can change a value at an index by simply referencing the index. x[2] = 12 will change the preceding slice to []int{8,4,12,6}.

Unlike arrays, we can add a new value to the slice using the append command. x = append(x, 2) will cause the underlying x array references to be copied to a new array and assigns the new view of the array back to x. The new value is []int{8,4,12,6,2}. You may append multiple values by just putting more comma-delimited values in append (that is, x = append(x, 2, 3, 4, 5)).

Remember that slices are simply views into a trackable array. We can create new limited views of the array. y := x[1:3] creates a view (y) of the array, yielding []int{4, 12} (1 is inclusive and 3 is exclusive in [1:3]). Changing the value at y[0] will change x[1]. Appending a single value to y via y = append(y, 10)will change x[3], yielding []int{8,4,12,10,2}.

This kind of use isn't common (and is confusing), but the important part is to understand that slices are simply views into an array.

While slices are a pointer-wrapped type (values in a slice passed to a function that are changed will change in the caller as well), a slice's view will not change.

func doAppend(sl []int) {
     sl = append(sl, 100)
     fmt.Println("inside: ", sl) // inside:  [1 2 3 100]
}
func main() { 
     x := []int{1, 2, 3}
     doAppend(x)
     fmt.Println("outside: ", x) // outside:  [1 2 3]
}

In this example, the sl and x variables both use the same underlying array (which has changed in both), but the view for x does not get updated in doAppend(). To update x to see the addition to the slice would require passing a pointer to the slice (pointers are covered in a future chapter) or returning the new slice as seen here:

func doAppend(sl []int) []int {
     return append(sl, 100)
}
func main() {
     x := []int{1, 2, 3}
     x = doAppend(x)
     fmt.Println("outside: ", x) // outside:  [1 2 3 100]
}

Now that you see how to create and add to a slice, let's look at how to extract the values.

Extracting all values

To extract values from a slice, we can use the older C-type for loop or the more common for...range syntax.

The older C style is as follows:

for i := 0; i < len(someSlice); i++{
     fmt.Printf("slice entry %d: %s\n", i, someSlice[i])
}

The more common approach in Go uses range:

for index, val := range someSlice {
     fmt.Printf("slice entry %d: %s\n", index, val)
}

With range, we often want to use only the value, but not the index. In Go, you must use variables that are declared in a function, or the compiler will complain with the following:

index declared but not used

To only extract the values, we can use _, (which tells the compiler not to store the output), as follows:

for _, val := range someSlice {
     fmt.Printf("slice entry: %s\n", val)
}

On very rare occasions, you may want to only print out indexes and not values. This is uncommon because it will simply count from zero to the number of items. However, this can be achieved by simply removing val from the for statement: for index := range someSlice.

In this section, you have discovered what arrays are, how to create them, and how they relate to slices. In addition, you've acquired the skills to create slices, add data to slices, and extract data from slices. Let's move on to learning about maps next.

Understanding maps

Maps are a collection of key-value pairs that a user can use to store some data and retrieve it with a key. In some languages, these are called dictionaries (Python) or hashes (Perl). In contrast to an array/slice, finding an entry in a map requires a single lookup versus iterating over the entire slice comparing values. With a large set of items, this can give you significant time savings.

Declaring a map

There are several ways to declare a map. Let's first look at using make:

var counters = make(map[string]int, 10)

The example just shared creates a map with string keys and stores data that is an int type. 10 signifies that we want to pre-size for 10 entries. The map can grow beyond 10 entries and the 10 can be omitted.

Another way of declaring a map is by using a composite literal:

modelToMake := map[string]string{
     "prius": "toyota",
     "chevelle": "chevy",
}

This creates a map with string keys and stores the string data. We also pre-populate the entry with two key-value entries. You can omit the entries to have an empty map.

Accessing values

You can retrieve a value as follows:

carMake := modelToMake["chevelle"]
fmt.Println(carMake) // Prints "chevy"

This assigns the chevy value to carMake.

But what happens if the key isn't in the map? In that case, we will receive the zero value of the data type:

carMake := modelToMake["outback"]
fmt.Println(carMake)

The preceding code will print an empty string, which is the zero value of the string type that is used as values in our map.

We can also detect if the value is in the map:

if carMake, ok := modelToMake["outback"]; ok {
     fmt.Printf("car model \"outback\" has make %q", carMake)
}else{
     fmt.Printf("car model \"outback\" has an unknown make")
}

Here we assign two values. The first (carMake) is the data stored in the key (or zero value if not set), and the second (ok) is a Boolean that indicates if the key was found.

Adding new values

Adding a new key-value pair or updating a key's value, is done the same way:

modelToMake["outback"] = "subaru"
counters["pageHits"] = 10

Now that we can change a key-value pair, let's look at extracting values from a map.

Extracting all values

To extract values from a map, we can use the for...range syntax that we used for slices. There are a few key differences with maps:

  • Instead of an index, you will get the map's key.
  • Maps have a non-deterministic order.

Non-deterministic order means that iterating over the data will return the same data but not in the same order.

Let's print out all the values in our carMake map:

for key, val := range modelToMake {
     fmt.Printf("car model %q has make %q\n", key, val)
}

This will yield the following, but maybe not in the same order:

car model "prius" has make "toyota"
car model "chevelle" has make "chevy"
car model "outback" has make "subaru"

Note

Similar to a slice, if you don't need the key, you may use _ instead. If you simply want the keys, you can omit the value val variable, such as for key := range modelToMake.

In this section, you have learned about the map type, how to declare them, add values to them, and finally how to extract values from them. Let's dive into learning about pointers.

You have been reading a chapter from
Go for DevOps
Published in: Jul 2022
Publisher: Packt
ISBN-13: 9781801818896
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime