Lesson 10
Working with Pointers

Go supports pointers. A pointer is a variable that stores the memory address of another variable where the data is stored, rather than storing a value itself. In this lesson, we will look at the use of pointers.

CREATING A POINTER

All variables in Go other than maps and slices are value types. What that means is that if you have a variable that you are passing to a function and you want to make changes to the variable outside of the function, you can't change the variable directly. A copy gets passed in whenever you send it to a function.

If you want to modify the variable, the general approach is to use a pointer to the memory address instead of trying to affect the variable itself. In other words, you can pass in a pointer to the address of the variable, rather than passing a copy of the value itself. When you pass in a pointer, both the original value and the value used in the function are pointing to the same part of memory. Because they're pointing to the same spot in memory, when you change one value, both variables are changed.

Pointers are also used to pass in larger variables. If you have a large struct, then your application would take time and memory to copy the struct and all of its fields over to a function when passed, whereas if you pass in a pointer, the function will just receive the memory address of the struct. The memory address is going to be much smaller than the struct, so the application will be more efficient.

The syntax used to define a pointer is:

var pointerName *dataType

The pointerName is the name of the variable that will hold the pointer. The dataType is the type that is associated with the value stored in the pointer. In Listing 10.1, you create two variables:

  • The variable a is a normal int variable, to which you assign the value 20.
  • The variable b is an int pointer, as designated by *int.

When you run the code, the output is:

20
<nil>

Nothing has been assigned to the b variable, so it is empty. While Go does assign the default value of 0 to an int variable, it does not assign any value at all to a pointer. As a result, you will see that it is shown to be equal to nil.

Initializing a Pointer

Once you define a pointer, you can initialize it to point to the memory address of another variable by using the syntax:

pointerName = &variableName

In Listing 10.2, the pointer b points to the memory address of variable a.

Running the program now gives you this output:

20
0xc0000100b0

The second value is the hexadecimal address of the memory where the value for a is stored. This value will likely be different each time you run the program, depending on available memory blocks.

Declaring and Initializing a Pointer

You can also combine the definition and initialization of a pointer in one step. Listing 10.3 shows your pointer b being declared and assigned the address of a within one statement.

You again declare a as an int and store the value of 20 within it. You then declare b as a pointer to an int. Within the declaration, you then assign the address of a to the pointer b. When you run this listing, you see output similar to the previous listing. Again, the address printed in the second print statement will vary:

20
0xc0000aa058

Using Dynamic Typing

You can also create a pointer variable using dynamic typing in the same way you can declare other types of variables. As you may recall, dynamic typing is when you create a variable and assign a value without specifying its type. Listing 10.4 illustrates dynamic typing, where variable b is defined without a type. The compiler will infer the type based on the initial value.

This listing operates identically to the previous listings. The only difference is that the compiler will infer that b is a pointer to an int.

Pointers of Different Types

Up to this point, you've used a pointer to an int. You can use pointers to any other data types in the exact manner, as illustrated in Listing 10.5.

This listing simply adds declarations to the previous listing to define variables of types float32, string, and uint along with pointers to each. When you run this listing, you see that the values of the variables are printed, and each pointer variable has a unique address:

20
0xc000014098
-------------
10.3
0xc0000140b0
-------------
My string
0xc00003a230
-------------
42
0xc0000140b8
-------------

Looking at Listing 10.5, you can see that the type declared for each pointer matches the type being assigned. What happens if you try to assign a variable of one type to a pointer of a different type, such as trying to assign a variable of type float32 to a pointer to an int? Modify Listing 10.5 to assign the value in the variable c (a float32 value) to the pointer called h, which is defined as a pointer to an uint. What happens when you do this? Listing 10.6 shows this modification.

If you make the change and run the new listing, you will get an error:

# command-line-arguments
.Listing1006.go:16:9: cannot use &c (type *float32) as type *uint in assignment

While a pointer might hold an address, the value stored in the pointer needs to still match the defined type.

ACCESSING THE STORED VALUE OF A POINTER

In order to access the value stored in the memory address stored in a pointer, you can use the * operator (the dereferencing operator), as shown in Listing 10.7.

In this listing, a variable of type int called myVar is declared and set to the value of 20. A second variable called b is also declared and it is set to the address of myVar. Because a type is not included when b is declared, the compiler will dynamically type b to be a pointer to type int.

After your two variables are declared, two print statements are executed. You first print out the value of b, which will be the memory address where myVar is located. In the second print statement, you pass *b instead of b. Instead of the address being printed, the dereferencing operator provides the value stored in the variable called b to be retrieved and printed, which is the value stored in myVar. Based on this dereferencing, the output this time looks like this:

0xc0000100b0
20

UNDERSTANDING NIL POINTERS

You saw earlier that when we define a pointer without initializing it, it contains a nil value by default. Listing 10.8 declares a pointer but does not initialize it.

The output for this program is:

<nil>

The nil value means that the pointer itself is empty and contains no real value. In other words, it doesn't point to anything. What would happen if you added a print statement to dereference the value stored in b?

fmt.Println(*b)

If you try to access the value in a nil pointer, you will get an error similar to the following:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x2ec5d5]

To avoid this error, you can use an if statement to verify that a pointer doesn't equal nil. Listing 10.9 checks to see if the pointer called b is nil before trying to access its value.

You can see that to make sure b has a value, it is simply compared to the Go keyword nil. If b is equal to nil, then you know it doesn't point to anything, so you can avoid doing something that would cause an error.

USING POINTERS TO CHANGE VARIABLES

Because the dereferencing operator (*) assigns a variable's value to a pointer, you can also use it to assign new values to the variables themselves. Listing 10.10 illustrates this.

In this listing, you assign a value of 20 to the variable a. You create a variable called b as a pointer to a. The listing prints a, b (the memory address), and *b (the value stored at the address stored in b, which is the value of a).

After printing these values, the following line is executed:

*b = 30

This statement changes the value stored at the memory address in b. As a result of using * on the pointer variable b, you change the value assigned to a. You can confirm this by reviewing the output from the listing:

The value stored in a: 20
Memory address: 0xc0000ae058
Value stored in a (via pointer): 20
New value of a: 30

Again, the memory address will be different, but you can see that the value of a was changed from 20 to 30.

COMPARING POINTERS

You can use the == operator to check if two pointers are equal. Pointers are equal if they point to the same storage location, which typically implies that they point to the same variable. In Listing 10.11, you create two pointers that refer to the variable a, as well as a nil pointer.

Both b and c are declared as pointers that contain the address of a. You also declare a pointer called x as a pointer to an int. You do not assign x a value, so it will contain nil.

You then compare the pointers to each other. First, you compare c to b. You then compare x to b. The output is:

Is c == b? true
Is x == b? false

WORKING WITH AN ARRAY OF POINTERS

In the previous lesson, you learned about arrays. In Go, you can also create an array of pointers. Each element of such an array contains an address to a value stored in memory. You can use an array like this in the same way you use traditional arrays. Listing 10.12 illustrates the use of an array of pointers.

In this example, you create an int array called numbers that contains three elements: 100, 1000, and 10000. You then use a for loop to print these values.

You then create a second array called numbersptr that holds three pointers to variables of type int:

var numbersptr [3]*int

You use a second for loop to assign a pointer to each value in the original array. This assignment is done using the same index offset:

numbersptr[ctr] = &numbers[ctr]

Once these assignments are done, you print the new array, which displays the addresses of the pointers in your numbersptr array.

Finally, you print the individual values the pointers in the array point to. You do this by using the * operator. The output from the complete listing is:

100
1000
10000
[0xc00000a3c0 0xc00000a3c8 0xc00000a3d0]
100
1000
10000

Changing Values in an Array

You can manipulate the values of an array using an array of pointers. The code in Listing 10.13 takes the previous example and refactors it to use the pointer to update a value in the original array.

Most of this listing is the same as the previous one. However, instead of printing the individual elements of the array of pointers, you assign the value of 200 to the dereferenced pointer in the first element of the numbersptr (position 0):

*numbersptr[0]=200

You can then see in the output that the value stored in the first element of the numbers array has indeed changed from 100 to 200:

100
1000
10000
[0xc00000a3c0 0xc00000a3c8 0xc00000a3d0]
[200 1000 10000]

USING POINTERS WITH FUNCTIONS

You can pass pointers to functions. By passing a pointer to a function, you are able to access and use the value stored in the variable the pointer references. Listing 10.14 includes a function called isupper that accepts a pointer to a string as input and returns true if the string is in uppercase and false otherwise.

Here, instead of passing the message variable directly to the function, the isupper function is set up to accept a pointer to that variable, as indicated by the *string argument. Because the original string stored in message is in all caps, the program returns true.

As a side note, this listing imports strings, which includes routines for working with and manipulating strings. In this case, the strings.ToUpper method is used, which takes a string as a parameter and returns a copy of the string in all uppercase letters. For this listing, the copy of the string in all uppercase is then compared to the original string (*x) to see if they are the same.

Changing Values from a Function

We mentioned before that variables are passed by value to a function, which means a copy of the variable is passed. You just saw that by passing a pointer to a function, you are able to access and use the value of a variable. The previous example retrieved a value stored in memory without changing that value. However, as you have seen, you can also use pointers to change the value stored in a variable.

Listing 10.15 converts a string to uppercase. The function (called upper in this listing) is again defined to use a pointer as an argument rather than the variable's value itself. This time, however, the function replaces the variable's value with an uppercase version of the original string.

In this code, you initialize message with a string in lowercase. You then use messageptr in the upper function to convert the string to uppercase. The final output (HELLO WORLD) shows that the variable's value has been changed, rather than just presenting the original string in uppercase.

SUMMARY

In this lesson you learned about pointers, which are variables that contain the memory address of another variable or nil if the pointer is unassigned. Not only did you learn how to declare and initialize pointers, but you also saw how to use them to access and manipulate the values in other variables. You even went further and saw how to use pointers in arrays as well as how to use them with functions. You learned that by passing a pointer to a function, you are able to change values in variables outside of the function.

EXERCISES

The following exercises are provided to allow you to experiment with the tools and concepts presented in this lesson. For each exercise, write a program that meets the specified requirements and verify that the program runs as expected. The exercises are:

Exercise 10.1: Name, Age, and Gender

Create a program that contains three variables: name, age, and gender. Assign values to each. Make each of these variables a different data type. Create pointers that point to each variable. Use fmt.Println statements to print the values from each of the three variables both by using the original variables and by dereferencing the pointers.

Exercise 10.2: User Input

In a previous lesson, you prompted the user to enter values. You used the fmt.Scanln function for this. Enter and run Listing 10.16, which prompts the user to enter a name, age, and gender:

After running the listing, change it to use pointers to each of the three variables instead of directly passing the address of each.

Exercise 10.3: Playing with Pointers

Enter and run the program in Listing 10.14 if you haven't already. Make the following changes to the listing and review what happens. Try to predict what will happen before making the changes.

  • Change the value assigned to message to be "Hello World". What is the output?
  • Change the value assigned to message to be "HELLO 123 WORLD". What is the output?
  • What happens if the function uses message instead of messageptr in the last line of code?
fmt.Println(isupper(message))

Exercise 10.4: Full Name

Create a function that takes three parameters. The first should be a pointer to a string variable that will hold a full name. The second and third parameters should be strings that contain a first name and a last name, respectively. The function should first combine the first and last name strings with a space between to create a full name, which should be applied to the variable in the location of the pointer passed to the function.

In addition to writing the function, create a main program that creates the full name variable and a pointer to the full name. Pass these along with first and last name strings to the function. After calling the function, print the full name.

Exercise 10.5: What's It Doing?

Take a look at Listing 10.17. Most of this code is similar to what you saw in this lesson. Without running the code, what do the last three statements in the listing do?

Exercise 10.6: Reverse

Expand the listing in Exercise 10.5 so that you use the pointers to change the order of the array. Using the pointers, reverse the values so that when the array is displayed, the results are:

[444 666 888 555 999 222 777 333 111 123]

Exercise 10.7: Sort

Rewrite the listing in Exercise 10.6 so that you use the pointers to change the order of the array. This time, use the pointers and the values to which they point to sort the list from highest to lowest.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset