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)
14
Other Books You May Enjoy
15
Index

Pointers

Go has support for pointers but not for pointer arithmetic, which is the cause of many bugs and errors in programming languages like C. A pointer is the memory address of a variable. You need to dereference a pointer in order to get its value—dereferencing is performed using the * character in front of the pointer variable. Additionally, you can get the memory address of a normal variable using an & in front of it.

The next diagram shows the difference between a pointer to an int and an int variable.

Graphical user interface

Description automatically generated

Figure 2.4: An int variable and a pointer to an int

If a pointer variable points to an existing regular variable, then any changes you make to the stored value using the pointer variable will modify the regular variable.

The format and the values of memory addresses might be different between different machines, different operating systems, and different architectures.

You might ask, what is the point of using pointers since there is no support for pointer arithmetic. The main benefit you get from pointers is that passing a variable to a function as a pointer (we can call that by reference) does not discard any changes you make to the value of that variable inside that function when the function returns. There exist times where you want that functionality because it simplifies your code, but the price you pay for that simplicity is being extra careful with what you do with a pointer variable. Remember that slices are passed to functions without the need to use a pointer—it is Go that passes the pointer to the underlying array of a slice and there is no way to change that behavior.

Apart from reasons of simplicity, there exist three more reasons for using pointers:

  • Pointers allow you to share data between functions. However, when sharing data between functions and goroutines, you should be extra careful with race condition issues.
  • Pointers are also very handy when you want to tell the difference between the zero value of a variable and a value that is not set (nil). This is particularly useful with structures because pointers (and therefore pointers to structures, which are fully covered in the next chapter), can have the nil value, which means that you can compare a pointer to a structure with the nil value, which is not allowed for normal structure variables.
  • Having support for pointers and, more specifically, pointers to structures allows Go to support data structures such as linked lists and binary trees, which are widely used in computer science. Therefore, you are allowed to define a structure field of a Node structure as Next *Node, which is a pointer to another Node structure. Without pointers, this would have been difficult to implement and may be too slow.

The following code illustrates how you can use pointers in Go—create a text file named pointers.go and type the presented code.

package main
import "fmt"
type aStructure struct {
    field1 complex128
    field2 int
}

This is a structure with two fields named field1 and field2.

func processPointer(x *float64) {
    *x = *x * *x
}

This is a function that gets a pointer to a float64 variable as input. As we are using a pointer, all changes to the function parameter inside the function are persistent. Additionally, there is no need to return something.

func returnPointer(x float64) *float64 {
    temp := 2 * x
    return &temp
}

This is a function that requires a float64 parameter as input and returns a pointer to a float64. In order to return the memory address of a regular variable, you need to use & (&temp).

func bothPointers(x *float64) *float64 {
    temp := 2 * *x
    return &temp
}

This is a function that requires a pointer to a float64 as input and returns a pointer to a float64 as output. The *x notation is used for getting the value stored in the memory address stored in x.

func main() {
    var f float64 = 12.123
    fmt.Println("Memory address of f:", &f)

To get the memory address of a regular variable named f, you should use the &f notation.

    // Pointer to f
    fP := &f
    fmt.Println("Memory address of f:", fP)
    fmt.Println("Value of f:", *fP)
    // The value of f changes
    processPointer(fP)
    fmt.Printf("Value of f: %.2f\n", f)

fP is now a pointer to the memory address of the f variable. Any changes to the value stored in the fP memory address have an effect on the f value as well. However, this is only true for as long as fP points to the memory address of the f variable.

    // The value of f does not change
    x := returnPointer(f)
    fmt.Printf("Value of x: %.2f\n", *x)

The value of f does not change because the function only uses its value.

    // The value of f does not change
    xx := bothPointers(fP)
    fmt.Printf("Value of xx: %.2f\n", *xx)

In this case, the value of f, as well as the value stored in the fP memory address, does not change because the bothPointers() function does not make any changes to the value stored in the fP memory address.

    // Check for empty structure
    var k *aStructure

The k variable is a pointer to an aStructure structure. As k points to nowhere, Go makes it point to nil, which is the zero value for pointers.

    // This is nil because currently k points to nowhere
    fmt.Println(k)
    // Therefore you are allowed to do this:
    if k == nil {
        k = new(aStructure)
    }

As k is nil, we are allowed to assign it to an empty aStructure value with new(aStructure) without losing any data. Now, k is no longer nil but both fields of aStructure have the zero values of their data types.

    fmt.Printf("%+v\n", k)
    if k != nil {
        fmt.Println("k is not nil!")
    }
}

Just make sure that k is not nil—you might consider that check redundant, but it does not hurt to double-check.

Running pointers.go generates the following kind of output:

Memory address of f: 0xc000014090
Memory address of f: 0xc000014090
Value of f: 12.123
Value of f: 146.97
Value of x: 293.93
Value of xx: 293.93
<nil>
&{field1:(0+0i) field2:0}
k is not nil!

We revisit pointers in the next chapter where we discuss structures. Next, we discuss generating random numbers and random strings.