Book Image

Mastering Go – Third Edition - Third Edition

By : Mihalis Tsoukalos
5 (2)
Book Image

Mastering Go – Third Edition - Third Edition

5 (2)
By: Mihalis Tsoukalos

Overview of this book

Mastering Go is the essential guide to putting Go to work on real production systems. This freshly updated third edition includes topics like creating RESTful servers and clients, understanding Go generics, and developing gRPC servers and clients. Mastering Go was written for programmers who want to explore the capabilities of Go in practice. As you work your way through the chapters, you’ll gain confidence and a deep understanding of advanced Go concepts, including concurrency and the operation of the Go Garbage Collector, using Go with Docker, writing powerful command-line utilities, working with JavaScript Object Notation (JSON) data, and interacting with databases. You’ll also improve your understanding of Go internals to optimize Go code and use data types and data structures in new and unexpected ways. This essential Go programming book will also take you through the nuances and idioms of Go with exercises and resources to fully embed your newly acquired knowledge. With the help of Mastering Go, you’ll become an expert Go programmer by building Go systems and implementing advanced Go techniques in your projects.
Table of Contents (17 chapters)
Other Books You May Enjoy

Grouping similar data

There are times when you want to keep multiple values of the same data type under a single variable and access them using an index number. The simplest way to do that in Go is by using arrays or slices.

Arrays are the most widely used data structures and can be found in almost all programming languages due to their simplicity and speed of access. Go provides an alternative to arrays that is called a slice. The subsections that follow help you understand the differences between arrays and slices so that you know which data structure to use and when.

The quick answer is that you can use slices instead of arrays almost anywhere in Go but we are also demonstrating arrays because they can still be useful and because slices are implemented by Go using arrays!


Arrays in Go have the following characteristics and limitations:

  • When defining an array variable, you must define its size. Otherwise, you should put [...] in the array declaration and let the Go compiler find out the length for you. So you can create an array with 4 string elements either as [4]string{"Zero", "One", "Two", "Three"} or as [...]string{"Zero", "One", "Two", "Three"}. If you put nothing in the square brackets, then a slice is going to be created instead. The (valid) indexes for that particular array are 0, 1, 2, and 3.
  • You cannot change the size of an array after you have created it.
  • When you pass an array to a function, what is happening is that Go creates a copy of that array and passes that copy to that function—therefore any changes you make to an array inside a function are lost when the function returns.

As a result, arrays in Go are not very powerful, which is the main reason that Go has introduced an additional data structure named slice that is similar to an array but is dynamic in nature and is explained in the next subsection. However, data in both arrays and slices is accessed the same way.


Slices in Go are more powerful than arrays mainly because they are dynamic, which means that they can grow or shrink after creation if needed. Additionally, any changes you make to a slice inside a function also affect the original slice. But how does this happen? Strictly speaking, all parameters in Go are passed by value—there is no other way to pass parameters in Go.

In reality, a slice value is a header that contains a pointer to an underlying array where the elements are actually stored, the length of the array, and its capacity—the capacity of a slice is explained in the next subsection. Note that the slice value does not include its elements, just a pointer to the underlying array. So, when you pass a slice to a function, Go makes a copy of that header and passes it to the function. This copy of the slice header includes the pointer to the underlying array. That slice header is defined in the reflect package ( as follows:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int

A side effect of passing the slice header is that it is faster to pass a slice to a function because Go does not need to make a copy of the slice and its elements, just the slice header.

You can create a slice using make() or like an array without specifying its size or using [...]. If you do not want to initialize a slice, then using make() is better and faster. However, if you want to initialize it at the time of creation, then make() cannot help you. As a result, you can create a slice with three float64 elements as aSlice := []float64{1.2, 3.2, -4.5}. Creating a slice with space for three float64 elements with make() is as simple as executing make([]float64, 3). Each element of that slice has a value of 0, which is the zero value of the float64 data type.

Both slices and arrays can have many dimensions—creating a slice with two dimensions with make() is as simple as writing make([][]int, 2). This returns a slice with two dimensions where the first dimension is 2 (rows) and the second dimension (columns) is unspecified and should be explicitly specified when adding data to it.

If you want to define and initialize a slice with two dimensions at the same time, you should execute something similar to twoD := [][]int{{1, 2, 3}, {4, 5, 6}}.

You can find the length of an array or a slice using len(). As you will find out in the next subsection, slices have an additional property named capacity. You can add new elements to a full slice using the append() function. append() automatically allocates the required memory space.

The example that follows clarifies many things about slices—feel free to experiment with it. Type the following code and save it as goSlices.go.

package main
import "fmt"
func main() {
    // Create an empty slice
    aSlice := []float64{}
    // Both length and capacity are 0 because aSlice is empty
    fmt.Println(aSlice, len(aSlice), cap(aSlice))
    // Add elements to a slice
    aSlice = append(aSlice, 1234.56)
    aSlice = append(aSlice, -34.0)
    fmt.Println(aSlice, "with length", len(aSlice))

The append() commands add two new elements to aSlice. You should save the return value of append() to an existing variable or a new one.

    // A slice with length 4
    t := make([]int, 4)
    t[0] = -1
    t[1] = -2
    t[2] = -3
    t[3] = -4
    // Now you will need to use append
    t = append(t, -5)

Once a slice has no place left for more elements, you should add new elements to it using append().

    // A 2D slice
    // You can have as many dimensions as needed
    twoD := [][]int{{1, 2, 3}, {4, 5, 6}}
    // Visiting all elements of a 2D slice
    // with a double for loop
    for _, i := range twoD {
            for _, k := range i {
                fmt.Print(k, " ")

The previous code shows how to create a 2D slice variable named twoD and initialize it at the same time.

    make2D := make([][]int, 2)
    make2D[0] = []int{1, 2, 3, 4}
    make2D[1] = []int{-1, -2, -3, -4}

The previous part shows how to create a 2D slice with make(). What makes the make2D a 2D slice is the use of [][]int in make().

Running goSlices.go produces the next output:

$ go run goSlices.go 
[] 0 0
[1234.56 -34] with length 2
[-1 -2 -3 -4 -5]
1 2 3 
4 5 6 
[[] []]
[[1 2 3 4] [-1 -2 -3 -4]]

About slice length and capacity

Both arrays and slices support the len() function for finding out their length. However, slices also have an additional property called capacity that can be found using the cap() function.

The capacity of a slice is really important when you want to select a part of a slice or when you want to reference an array using a slice. Both subjects will be discussed over the next few sections.

The capacity shows how much a slice can be expanded without the need to allocate more memory and change the underlying array. Although after slice creation the capacity of a slice is handled by Go, a developer can define the capacity of a slice at creation time using the make() function—after that the capacity of the slice doubles each time the length of the slice is about to become bigger than its current capacity. The first argument of make() is the type of the slice and its dimensions, the second is its initial length and the third, which is optional, is the capacity of the slice. Although the data type of a slice cannot change after creation, the other two properties can change.

Writing something like make([]int, 3, 2) generates an error message because at any given time the capacity of a slice (2) cannot be smaller than its length (3).

But what happens when you want to append a slice or an array to an existing slice? Should you do that element by element? Go supports the ... operator, which is used for exploding a slice or an array into multiple arguments before appending it to an existing slice.

The figure that follows illustrates with a graphical representation how length and capacity work in slices.

A picture containing diagram

Description automatically generated

Figure 2.1: How slice length and capacity are related

For those of you that prefer code, here is a small Go program that showcases the length and capacity properties of slices. Type it and save it as capLen.go.

package main
import "fmt"
func main() {
    // Only length is defined. Capacity = length
    a := make([]int, 4)

In this case, the capacity of a is the same as its length, which is 4.

    fmt.Println("L:", len(a), "C:", cap(a))
    // Initialize slice. Capacity = length
    b := []int{0, 1, 2, 3, 4}
    fmt.Println("L:", len(b), "C:", cap(b))

Once again, the capacity of slice b is the same as its length, which is 5.

    // Same length and capacity
    aSlice := make([]int, 4, 4)

This time the capacity of slice aSlice is the same as its length, not because Go decided to do so but because we specified it.

    // Add an element
    aSlice = append(aSlice, 5)

When you add a new element to slice aSlice, its capacity is doubled and becomes 8.

    // The capacity is doubled
    fmt.Println("L:", len(aSlice), "C:", cap(aSlice))
    // Now add four elements
    aSlice = append(aSlice, []int{-1, -2, -3, -4}...)

The ... operator expands []int{-1, -2, -3, -4} into multiple arguments and append() appends each argument one by one to aSlice.

    // The capacity is doubled
    fmt.Println("L:", len(aSlice), "C:", cap(aSlice))

Running capLen.go produces the next output:

$ go run capLen.go 
L: 4 C: 4
L: 5 C: 5
[0 0 0 0]
[0 0 0 0 5]
L: 5 C: 8
[0 0 0 0 5 -1 -2 -3 -4]
L: 9 C: 16

Setting the correct capacity of a slice, if known in advance, will make your programs faster because Go will not have to allocate a new underlying array and have all the data copied over.

Working with slices is good but what happens when you want to work with a continuous part of an existing slice? Is there a practical way to select a part of a slice? Fortunately, the answer is yes—the next subsection sheds some light on selecting a continuous part of a slice.

Selecting a part of a slice

Go allows you to select parts of a slice, provided that all desired elements are next to each other. This can be pretty handy when you select a range of elements and you do not want to give their indexes one by one. In Go you select a part of a slice by defining two indexes, the first one is the beginning of the selection whereas the second one is the end of the selection, without including the element at that index, separated by :.

If you want to process all the command-line arguments of a utility apart from the first one, which is its name, you can assign it to a new variable (arguments := os.Args) for ease of use and use the arguments[1:] notation to skip the first command-line argument.

However, there is a variation where you can add a third parameter that controls the capacity of the resulting slice. So, using aSlice[0:2:4] selects the first 2 elements of a slice (at indexes 0 and 1) and creates a new slice with a maximum capacity of 4. The resulting capacity is defined as the result of the 4-0 subtraction where 4 is the maximum capacity and 0 is the first index—if the first index is omitted, it is automatically set to 0. In this case, the capacity of the result slice will be 4 because 4-0 equals 4.

If we would have used aSlice[2:4:4], we would have created a new slice with the aSlice[2] and aSlice[3] elements and with a capacity of 4-2. Lastly, the resulting capacity cannot be bigger than the capacity of the original slice because in that case, you would need a different underlying array.

Type the following code using your favorite editor and save it as partSlice.go.

package main
import "fmt"
func main() {
    aSlice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    l := len(aSlice)
    // First 5 elements
    // First 5 elements

In this first part, we define a new slice named aSlice that has 10 elements. Its capacity is the same as its length. Both 0:5 and :5 notations select the first 5 elements of the slice, which are the elements found at indexes 0, 1, 2, 3, and 4.

    // Last 2 elements
    fmt.Println(aSlice[l-2 : l])
    // Last 2 elements

Given the length of the slice (l), we can select the last two elements of the slice either as l-2 : l or as l-2:.

    // First 5 elements
    t := aSlice[0:5:10]
    fmt.Println(len(t), cap(t))
    // Elements at indexes 2,3,4
    // Capacity will be 10-2
    t = aSlice[2:5:10]
    fmt.Println(len(t), cap(t))

Initially, the capacity of t will be 10-0, which is 10. In the second case, the capacity of t will be 10-2.

    // Elements at indexes 0,1,2,3,4
    // New capacity will be 6-0
    t = aSlice[:5:6]
    fmt.Println(len(t), cap(t))

The capacity of t is now 6-0 and its length is going to be 5 because we have selected the first 5 elements of slice aSlice.

Running partSlice.go generates the next output:

$ go run partSlice.go 
[0 1 2 3 4 5 6 7 8 9]

The previous line is the output of fmt.Println(aSlice).

[0 1 2 3 4]
[0 1 2 3 4]

The previous two lines are generated from fmt.Println(aSlice[0:5]) and fmt.Println(aSlice[:5]).

[8 9]
[8 9]

Analogously, the previous two lines are generated from fmt.Println(aSlice[l-2 : l]) and fmt.Println(aSlice[l-2:]).

5 10
3 8
5 6

The last three lines print the length and the capacity of aSlice[0:5:10], aSlice[2:5:10] and aSlice[:5:6].

Byte slices

A byte slice is a slice of the byte data type ([]byte). Go knows that most byte slices are used to store strings and so makes it easy to switch between this type and the string type. There is nothing special in the way you can access a byte slice compared to the other types of slices. What is special is that Go uses byte slices for performing file I/O operations because they allow you to determine with precision the amount of data you want to read or write to a file. This happens because bytes are a universal unit among computer systems.

As Go does not have a char data type, it uses byte and rune for storing character values. A single byte can only store a single ASCII character whereas a rune can store Unicode characters. However, a rune can occupy multiple bytes.

The small program that follows illustrates how you can convert a byte slice into a string and vice versa, which you need for most File I/O operations—type it and save it as byteSlices.go.

package main
import "fmt"
func main() {
    // Byte slice
    b := make([]byte, 12)
    fmt.Println("Byte slice:", b)

An empty byte slice contains zeros—in this case, 12 zeros.

    b = []byte("Byte slice €")
    fmt.Println("Byte slice:", b)

In this case, the size of b is the size of the string "Byte slice €", without the double quotes—b now points to a different memory location than before, which is where "Byte slice €" is stored. This is how you convert a string into a byte slice.

As Unicode characters like € need more than one byte for their representation, the length of the byte slice might not be the same as the length of the string that it stores.

    // Print byte slice contents as text
    fmt.Printf("Byte slice as text: %s\n", b)
    fmt.Println("Byte slice as text:", string(b))

The previous code shows how to print the contents of a byte slice as text using two techniques. The first one is by using the %s control string and the second one using string().

    // Length of b
    fmt.Println("Length of b:", len(b))

The previous code prints the real length of the byte slice.

Running byteSlices.go produces the next output:

$ go run byteSlices.go 
Byte slice: [0 0 0 0 0 0 0 0 0 0 0 0]
Byte slice: [66 121 116 101 32 115 108 105 99 101 32 226 130 172]
Byte slice as text: Byte slice €
Byte slice as text: Byte slice €
Length of b: 14

The last line of the output proves that although the b byte slice has 12 characters, it has a size of 14.

Deleting an element from a slice

There is no default function for deleting an element from a slice, which means that if you need to delete an element from a slice, you must write your own code. Deleting an element from a slice can be tricky, so this subsection presents two techniques for doing so. The first technique virtually divides the original slice into two slices, split at the index of the element that needs to be deleted. Neither of the two slices includes the element that is going to be deleted. After that, we concatenate these two slices and creates a new one. The second technique copies the last element at the place of the element that is going to be deleted and creates a new slice by excluding the last element from the original slice.

The next figure shows a graphical representation of the two techniques for deleting an element from a slice.

A picture containing text, sign

Description automatically generated

Figure 2.2: Deleting an element from a slice

The following program shows the two techniques that can be used for deleting an element from a slice. Create a text file by typing the following code—save it as deleteSlice.go.

package main
import (
func main() {
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Need an integer value.")
    index := arguments[1]
    i, err := strconv.Atoi(index)
    if err != nil {
    fmt.Println("Using index", i)
    aSlice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    fmt.Println("Original slice:", aSlice)
    // Delete element at index i
    if i > len(aSlice)-1 {
        fmt.Println("Cannot delete element", i)
    // The ... operator auto expands aSlice[i+1:] so that
    // its elements can be appended to aSlice[:i] one by one
    aSlice = append(aSlice[:i], aSlice[i+1:]...)
    fmt.Println("After 1st deletion:", aSlice)

Here we logically divide the original slice into two slices. The two slices are split at the index of the element that needs to be deleted. After that, we concatenate these two slices with the help of .... Next, we see the second technique in action.

    // Delete element at index i
    if i > len(aSlice)-1 {
        fmt.Println("Cannot delete element", i)
    // Replace element at index i with last element
    aSlice[i] = aSlice[len(aSlice)-1]
    // Remove last element
    aSlice = aSlice[:len(aSlice)-1]
    fmt.Println("After 2nd deletion:", aSlice)

We replace the element that we want to delete with the last element using the aSlice[i] = aSlice[len(aSlice)-1] statement and then we remove the last element with the aSlice = aSlice[:len(aSlice)-1] statement.

Running deleteSlice.go produces the following kind of output, depending on your input:

$ go run deleteSlice.go 1
Using index 1
Original slice: [0 1 2 3 4 5 6 7 8]
After 1st deletion: [0 2 3 4 5 6 7 8]
After 2nd deletion: [0 8 3 4 5 6 7]

As the slice has 9 elements, you can delete the element at index value 1.

$ go run deleteSlice.go 10
Using index 10
Original slice: [0 1 2 3 4 5 6 7 8]
Cannot delete element 10

As the slice has only 9 elements, you cannot delete an element with an index value of 10 from the slice.

How slices are connected to arrays

As mentioned before, behind the scenes, each slice is implemented using an underlying array. The length of the underlying array is the same as the capacity of the slice and there exist pointers that connect the slice elements to the appropriate array elements.

You can understand that by connecting an existing array with a slice, Go allows you to reference an array or a part of an array using a slice. This has some strange capabilities including the fact that the changes to the slice affect the referenced array! However, when the capacity of the slice changes, the connection to the array ceases to exist! This happens because when the capacity of a slice changes, so does the underlying array, and the connection between the slice and the original array does not exist anymore.

Type the following code and save it as sliceArrays.go.

package main
import (
func change(s []string) {
    s[0] = "Change_function"

This is a function that changes the first element of a slice.

func main() {
    a := [4]string{"Zero", "One", "Two", "Three"}
    fmt.Println("a:", a)

Here we define an array named a with 4 elements.

    var S0 = a[0:1]
    S0[0] = "S0"

Here we connect S0 with the first element of the array a and we print it. Then we change the value of S0[0].

    var S12 = a[1:3]
    S12[0] = "S12_0"
    S12[1] = "S12_1"

In this part, we associate S12 with a[1] and a[2]. Therefore S12[0] = a[1] and S12[1] = a[2]. Then, we change the values of both S12[0] and S12[1]. These two changes will also change the contents of a. Put simply, a[1] takes the new value of S12[0] and a[2] takes the new value of S12[1].

    fmt.Println("a:", a)

And we print variable a, which has not changed at all in a direct way. However, due to the connections of a with S0 and S12, the contents of a have changed!

    // Changes to slice -> changes to array
    fmt.Println("a:", a)

As the slice and the array are connected, any changes you make to the slice will also affect the array even if the changes take place inside a function.

    // capacity of S0
    fmt.Println("Capacity of S0:", cap(S0), "Length of S0:", len(S0))
    // Adding 4 elements to S0
    S0 = append(S0, "N1")
    S0 = append(S0, "N2")
    S0 = append(S0, "N3")
    a[0] = "-N1"

As the capacity of S0 changes, it is no longer connected to the same underlying array (a).

    // Changing the capacity of S0
    // Not the same underlying array anymore!
    S0 = append(S0, "N4")
    fmt.Println("Capacity of S0:", cap(S0), "Length of S0:", len(S0))
    // This change does not go to S0
    a[0] = "-N1-"
    // This change does not go to S12
    a[1] = "-N2-"

However, array a and slice S12 are still connected because the capacity of S12 has not changed.

    fmt.Println("S0:", S0)
    fmt.Println("a: ", a)
    fmt.Println("S12:", S12)

Lastly, we print the final versions of a, S0, and S12.

Running sliceArrays.go produces the following output:

$ go run sliceArrays.go 
a: [Zero One Two Three]
[One Two]
a: [S0 S12_0 S12_1 Three]
a: [S0 Change_function S12_1 Three]
Capacity of S0: 4 Length of S0: 1
Capacity of S0: 8 Length of S0: 5
S0: [-N1 N1 N2 N3 N4]
a:  [-N1- -N2- N2 N3]
S12: [-N2- N2]

Let us now discuss the use of the copy() function in the next subsection.

The copy() function

Go offers the copy() function for copying an existing array to a slice or an existing slice to another slice. However, the use of copy() can be tricky because the destination slice is not auto-expanded if the source slice is bigger than the destination slice. Additionally, if the destination slice is bigger than the source slice, then copy() does not empty the elements from the destination slice that did not get copied. This is better illustrated in the figure that follows.

A picture containing text, sign

Description automatically generated

Figure 2.3: The use of the copy() function

The following program illustrates the use of copy()—type it in your favorite text editor and save it as copySlice.go.

package main
import "fmt"
func main() {
    a1 := []int{1}
    a2 := []int{-1, -2}
    a5 := []int{10, 11, 12, 13, 14}
    fmt.Println("a1", a1)
    fmt.Println("a2", a2)
    fmt.Println("a5", a5)
    // copy(destination, input)
    // len(a2) > len(a1)
    copy(a1, a2)
    fmt.Println("a1", a1)
    fmt.Println("a2", a2)

Here we run the copy(a1, a2) command. In this case, the a2 slice is bigger than a1. After copy(a1, a2), a2 remains the same, which makes perfect sense as a2 is the input slice, whereas the first element of a2 is copied to the first element of a1 because a1 has space for a single element only.

    // len(a5) > len(a1)
    copy(a1, a5)
    fmt.Println("a1", a1)
    fmt.Println("a5", a5)

In this case, a5 is bigger than a1. Once again, after copy(a1, a5), a5 remains the same whereas a5[0] is copied to a1[0].

    // len(a2) < len(a5) -> OK
    copy(a5, a2)
    fmt.Println("a2", a2)
    fmt.Println("a5", a5)

In this last case, a2 is shorter than a5. This means that the entire a2 is copied into a5. As the length of a2 is 2, only the first 2 elements of a5 change.

Running copySlice.go produces the next output:

$ go run copySlice.go 
a1 [1]
a2 [-1 -2]
a5 [10 11 12 13 14]
a1 [-1]
a2 [-1 -2]

The copy(a1, a2) statement does not alter the a2 slice, just a1. As the size of a1 is 1, only the first element from a2 is copied.

a1 [10]
a5 [10 11 12 13 14]

Similarly, copy(a1, a5) alters a1 only. As the size of a1 is 1, only the first element from a5 is copied to a1.

a2 [-1 -2]
a5 [-1 -2 12 13 14]

Last, copy(a5, a2) alters a5 only. As the size of a5 is 5, only the first two elements from a5 are altered and become the same as the first two elements of a2, which has a size of 2.

Sorting slices

There are times when you want to present your information sorted and you want Go to do the job for you. In this subsection, we'll see how to sort slices of various standard data types using the functionality offered by the sort package.

The sort package can sort slices of built-in data types without the need to write any extra code. Additionally, Go provides the sort.Reverse() function for sorting in the reverse order than the default. However, what is really interesting is that sort allows you to write your own sorting functions for custom data types by implementing the sort.Interface interface—you will learn more about the sort.Interface interface and interfaces in general in Chapter 4, Reflection and Interfaces.

So, you can sort a slice of integers saved as sInts by typing sort.Ints(sInts). When sorting a slice of integers in reverse order using sort.Reverse(), you need to pass the desired slice to sort.Reverse() using sort.IntSlice(sInts) because the IntSlice type implements the sort.Interface internally, which allows you to sort in a different way than usual. The same applies to the other standard Go data types.

Create a text file with the code that illustrates the use of sort and name it sortSlice.go.

package main
import (
func main() {
    sInts := []int{1, 0, 2, -3, 4, -20}
    sFloats := []float64{1.0, 0.2, 0.22, -3, 4.1, -0.1}
    sStrings := []string{"aa", "a", "A", "Aa", "aab", "AAa"}
    fmt.Println("sInts original:", sInts)
    fmt.Println("sInts:", sInts)
    fmt.Println("Reverse:", sInts)

As sort.Interface knows how to sort integers, it is trivial to sort them in reverse order. Sorting in reverse order is as simple as calling the sort.Reverse() function.

    fmt.Println("sFloats original:", sFloats)
    fmt.Println("sFloats:", sFloats)
    fmt.Println("Reverse:", sFloats)
    fmt.Println("sStrings original:", sStrings)
    fmt.Println("sStrings:", sStrings)
    fmt.Println("Reverse:", sStrings)

The same rules apply when sorting floating point numbers and strings.

Running sortSlice.go produces the next output:

$ go run sortSlice.go
sInts original: [1 0 2 -3 4 -20]
sInts: [-20 -3 0 1 2 4]
Reverse: [4 2 1 0 -3 -20]
sFloats original: [1 0.2 0.22 -3 4.1 -0.1]
sFloats: [-3 -0.1 0.2 0.22 1 4.1]
Reverse: [4.1 1 0.22 0.2 -0.1 -3]
sStrings original: [aa a A Aa aab AAa]
sStrings: [A AAa Aa a aa aab]
Reverse: [aab aa a Aa AAa A]

The output illustrates how the original slices were sorted in both normal and reverse order.