Search icon CANCEL
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 Programming - From Beginner to Professional

You're reading from   Go Programming - From Beginner to Professional Learn everything you need to build modern software using Go

Arrow left icon
Product type Paperback
Published in Mar 2024
Publisher Packt
ISBN-13 9781803243054
Length 680 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Samantha Coyle Samantha Coyle
Author Profile Icon Samantha Coyle
Samantha Coyle
Arrow right icon
View More author details
Toc

Table of Contents (30) Chapters Close

Preface 1. Part 1: Scripts
2. Chapter 1: Variables and Operators FREE CHAPTER 3. Chapter 2: Command and Control 4. Chapter 3: Core Types 5. Chapter 4: Complex Types 6. Part 2: Components
7. Chapter 5: Functions – Reduce, Reuse, and Recycle 8. Chapter 6: Don’t Panic! Handle Your Errors 9. Chapter 7: Interfaces 10. Chapter 8: Generic Algorithm Superpowers 11. Part 3: Modules
12. Chapter 9: Using Go Modules to Define a Project 13. Chapter 10: Packages Keep Projects Manageable 14. Chapter 11: Bug-Busting Debugging Skills 15. Chapter 12: About Time 16. Part 4: Applications
17. Chapter 13: Programming from the Command Line 18. Chapter 14: File and Systems 19. Chapter 15: SQL and Databases 20. Part 5: Building For The Web
21. Chapter 16: Web Servers 22. Chapter 17: Using the Go HTTP Client 23. Part 6: Professional
24. Chapter 18: Concurrent Work 25. Chapter 19: Testing 26. Chapter 20: Using Go Tools 27. Chapter 21: Go in the Cloud 28. Index 29. Other Books You May Enjoy

Value versus pointer

With values such as int, bool, and string, when you pass them to a function, Go makes a copy of the value, and it’s the copy that’s used in the function. This copying means that a change that’s made to the value in the function doesn’t affect the value that you used when calling the function.

Passing values by copying tends to result in code that has fewer bugs. With this method of passing values, Go can use its simple memory management system, called the stack. The downside is that copying uses up more and more memory as values get passed from function to function. In real-world code, functions tend to be small, and values get passed to lots of functions, so copying by value can sometimes end up using much more memory than is needed.

There is an alternative to copying that uses less memory. Instead of passing a value, we create something called a pointer and then pass that to functions. A pointer is not a value itself, and you can’t do anything useful with a pointer other than getting a value using it. You can think of a pointer as the address of the value you want, and to get to the value, you must go to the address. If you use a pointer, Go won’t make a copy of the value when passing a pointer to a function.

When creating a pointer to a value, Go can’t manage the value’s memory using the stack. This is because the stack relies on simple scope logic to know when it can reclaim the memory that’s used by a value, and having a pointer to a variable means these rules don’t work. Instead, Go puts the value on the heap. The heap allows the value to exist until no part of your software has a pointer to it anymore. Go reclaims these values in what it calls its garbage collection process. This process happens periodically in the background, and you don’t need to worry about it.

Having a pointer to a value means that a value is put on the heap, but that’s not the only reason that happens. Working out whether a value needs to be put on the heap is called escape analysis. There are times when a value with no pointers is put on the heap, and it’s not always clear why.

You have no direct control over whether a value is put on the stack or the heap. Memory management is not part of Go’s language specification. Memory management is considered an internal implementation detail. This means it could be changed at any time, and that what we’ve spoken about are only general guidelines and not fixed rules and could change at a later date.

While the benefits of using a pointer over a value that gets passed to lots of functions are clear for memory usage, it’s not so clear for CPU usage. When a value gets copied, Go needs CPU cycles to get that memory and then release it later. Using a pointer avoids this CPU usage when passing it to a function. On the other hand, having a value on the heap means that it then needs to be managed by the complex garbage collection process. This process can become a CPU bottleneck in certain situations – for example, if there are lots of values on the heap. When this happens, the garbage collector has to do lots of checking, which uses up CPU cycles. There is no correct answer here, and the best approach is the classic performance optimization one. First, don’t prematurely optimize. When you do have a performance problem, measure before you make a change, and then measure after you’ve made a change.

Beyond performance, you can use pointers to change your code’s design. Sometimes, using pointers allows for a cleaner interface and simplifies your code. For example, if you need to know whether a value is present or not, a non-pointer value always has at least its zero value, which could be valid in your logic. You can use a pointer to allow for an is not set state as well as holding a value. This is because pointers, as well as holding the address to a value, can also be nil, which means there is no value. In Go, nil is a special type that represents something not having a value.

The ability for a pointer to be nil also means that it’s possible to get the value of a pointer when it doesn’t have a value associated with it, which means you’ll get a runtime error. To prevent runtime errors, you can compare a pointer to nil before trying to get its value. This looks like <pointer> != nil. You can compare pointers with other pointers of the same type, but they only result in true if you are comparing a pointer to itself. No comparison of the associated values gets made.

Pointers are powerful tools in the language thanks to their efficiency, ability to pass by reference (instead of pass by value) to allow functions to modify the original values, and how they allow for dynamic memory allocation using the garbage collector. However, with any great tool comes great responsibility. Pointers can be dangerous if misused, such as in the event memory is freed (deallocated) and the pointer becomes a “dangling pointer,” which could lead to undefined behavior if accessed. There is also the potential for memory leaks, unsafe operations due to direct memory access, and concurrency challenges if there are shared pointers that could introduce data races. Overall, Go’s pointers are generally straightforward and less error-prone compared to other languages such as C.

Getting a pointer

To get a pointer, you have a few options. You can declare a variable as being a pointer type using a var statement. You can do this by adding * at the front of most types. This notation looks like var <name> *<type>. The initial value of a variable that uses this method is nil. You can use the built-in new function for this. This function is intended to be used to get some memory for a type and return a pointer to that address. The notation looks like <name> := new(<type>). The new function can be used with var too. You can also get a pointer from an existing variable using &, which you can read as "address of". This looks like <var1> := &<var2>.

Exercise 1.13 – getting a pointer

In this exercise, we’ll use each of the methods we can use to get a pointer variable. Then, we’ll print them to the console using fmt.Printf to see what their types and value are. Let’s get started:

  1. Create a new folder and add a main.go file to it.
  2. In main.go, add the main package name to the top of the file:
    package main
  3. Import the packages we’ll need:
    import (
      "fmt"
      "time"
    )
  4. Create the main() function:
    func main() {
  5. Declare a pointer using a var statement:
      var count1 *int
  6. Create a variable using new:
      count2 := new(int)
  7. You can’t take the address of a literal number. Create a temporary variable to hold a number:
      countTemp := 5
  8. Using &, create a pointer from the existing variable:
      count3 := &countTemp
  9. It’s possible to create a pointer from some types without a temporary variable. Here, we’re using our trusty time struct:
      t := &time.Time{}
  10. Print each out using fmt.Printf:
      fmt.Printf("count1: %#v\n", count1)
      fmt.Printf("count2: %#v\n", count2)
      fmt.Printf("count3: %#v\n", count3)
      fmt.Printf("time : %#v\n", t)
  11. Close the main() function:
    }
  12. Save the file. Then, in the new folder, run the following:
    go run .

The following is the output:

Figure 1.19: Output showing pointers

Figure 1.19: Output showing pointers

In this exercise, we looked at three different ways of creating a pointer. Each one is useful, depending on what your code needs. With the var statement, the pointer has a value of nil, while the others already have a value address associated with them. For the time variable, we can see the value, but we can tell it’s a pointer because its output starts with &.

Next, we’ll see how we can get a value from a pointer.

Getting a value from a pointer

In the previous exercise, when we printed out the pointer variables for the int pointers to the console, we either got nil or saw a memory address. To get to the value a pointer is associated with, you must dereference the value using * in front of the variable name. This looks like fmt.Println(*<val>).

Dereferencing a zero or nil pointer is a common bug in Go software as the compiler can’t warn you about it, and it happens when the app is running. Therefore, it’s always best practice to check that a pointer is not nil before dereferencing it unless you are certain it’s not nil.

You don’t always need to dereference – for example, when a property or function is on a struct. Don’t worry too much about when you shouldn’t be dereferencing as Go gives you clear errors regarding when you can and can’t dereference a value.

Exercise 1.14 – getting a value from a pointer

In this exercise, we’ll update our previous exercise to dereference the values from the pointers. We’ll also add nil checks to prevent us from getting any errors. Let’s get started:

  1. Create a new folder and add a main.go file to it.
  2. In main.go, add the main package name to the top of the file:
    package main
  3. Import the packages we’ll need:
    import (
      "fmt"
      "time"
    )
  4. Create the main() function:
    func main() {
  5. Our pointers are declared in the same way as they were previously:
      var count1 *int
      count2 := new(int)
      countTemp := 5
      count3 := &countTemp
      t := &time.Time{}
  6. For counts 1, 2, and 3, we need to add a nil check and add * in front of the variable name:
      if count1 != nil {
        fmt.Printf("count1: %#v\n", *count1)
      }
      if count2 != nil {
        fmt.Printf("count2: %#v\n", *count2)
      }
      if count3 != nil {
        fmt.Printf("count3: %#v\n", *count3)
      }
  7. We’ll also add a nil check for our time variable:
      if t != nil {
  8. We’ll dereference the variable using *, just like we did with the count variables:
        fmt.Printf("time : %#v\n", *t)
  9. Here, we’re calling a function on our time variable. This time, we don’t need to dereference it:
        fmt.Printf("time : %#v\n", t.String())
  10. Close the nil check:
      }
  11. Close the main() function:
    }
  12. Save the file. Then, in the new folder, run the following:
    go run .

The following is the output:

Figure 1.20: Output showing getting values from pointers

Figure 1.20: Output showing getting values from pointers

In this exercise, we used dereferencing to get the values from our pointers. We also used nil checks to prevent dereferencing errors. From the output of this exercise, we can see that count1 was a nil value and that we’d have gotten an error if we tried to dereference. count2 was created using new, and its value is a zero value for its type. count3 also had a value that matched the value of the variable we got the pointer from. With our time variable, we were able to dereference the whole struct, which is why our output doesn’t start with &.

Next, we’ll look at how using a pointer allows us to change the design of our code.

Function design with pointers

We’ll cover functions in more detail later in this book, but you know enough from what we’ve done so far to see how using a pointer can change how you use a function. A function must be coded to accept pointers, and it’s not something that you can choose whether to do or not. If you have a pointer variable or have passed a pointer of a variable to a function, any changes that are made to the value of the variable in the function also affect the value of the variable outside of the function.

Exercise 1.15 – function design with pointers

In this exercise, we’ll create two functions: one that accepts a number by value, adds 5 to it, and then prints the number to the console; and another function that accepts a number as a pointer, adds 5 to it, and then prints the number out. We’ll also print the number out after calling each function to assess what effect it has on the variable that was passed to the function. Let’s get started:

  1. Create a new folder and add a main.go file to it.
  2. In main.go, add the main package name to the top of the file:
    package main
  3. Import the packages we’ll need:
    import "fmt"
  4. Create a function that takes an int pointer as an argument:
    func add5Value(count int) {
  5. Add 5 to the passed number:
      count += 5
  6. Print the updated number to the console:
      fmt.Println("add5Value   :", count)
  7. Close the function:
    }
  8. Create another function that takes an int pointer:
    func add5Point(count *int) {
  9. Dereference the value and add 5 to it:
      *count += 5
  10. Print out the updated value of count and dereference it:
      fmt.Println("add5Point   :", *count)
  11. Close the function:
    }
  12. Create the main() function:
    func main() {
  13. Declare an int variable:
      var count int
  14. Call the first function with the variable:
      add5Value(count)
  15. Print the current value of the variable:
      fmt.Println("add5Value post:", count)
  16. Call the second function. This time, you’ll need to use & to pass a pointer to the variable:
      add5Point(&count)
  17. Print the current value of the variable:
      fmt.Println("add5Point post:", count)
  18. Close the main() function:
    }
  19. Save the file. Then, in the new folder, run the following:
    go run .

The following is the output:

Figure 1.21: Output displaying the current value of the variable

Figure 1.21: Output displaying the current value of the variable

In this exercise, we showed you how passing values by a pointer can affect the value variables that are passed to them. We saw that, when passing by value, the changes you make to the value in a function do not affect the value of the variable that’s passed to the function, while passing a pointer to a value does change the value of the variable passed to the function.

You can use this fact to overcome awkward design problems and sometimes simplify the design of your code. Passing values by a pointer has traditionally been shown to be more error-prone, so use this design sparingly. It’s also common to use pointers in functions to create more efficient code, which Go’s standard library does a lot.

Activity 1.02 – pointer value swap

In this activity, your job is to finish some code a co-worker started. Here, we have some unfinished code for you to complete. Your task is to fill in the missing code, where the comments are to swap the values of a and b. The swap function only accepts pointers and doesn’t return anything:

package main
import "fmt"
func main() {
  a, b := 5, 10
  // call swap here
  fmt.Println(a == 10, b == 5)
}
func swap(a *int, b *int) {
  // swap the values here
}

Follow these steps:

  1. Call the swap function, ensuring you are passing a pointer.
  2. In the swap function, assign the values to the other pointer, ensuring you dereference the values.

The following is the expected output:

true true

Next, we’ll look at how we can create variables with a fixed value.

You have been reading a chapter from
Go Programming - From Beginner to Professional - Second Edition
Published in: Mar 2024
Publisher: Packt
ISBN-13: 9781803243054
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