Featured image of post Decrypt Go:  Understand the Three Pointer's in the Go

Decrypt Go: Understand the Three Pointer's in the Go

 

Go language has three types of pointers. In the normal development process, we only encounter the ordinary pointer. However, in the low-level source code of the Go language, there are a lot of operations involving three types of pointer conversion and manipulation. Let’s clarify these points first.

In the C language, pointer are crucial. Although pointers make operations highly flexible and efficient, there are many security risks associated with accessing memory through pointer operations, such as accessing memory out of bounds and compromising the atomicity of types in the type system. Here are some examples of incorrect usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Example 1
 int arr[2];
 *(arr+2) = 1;   // Accessing memory address out of bounds
 
 // Example 2
 int a = 4;
 int* ap = &a;  // Taking the starting address of variable a (4 bytes)
 *(short*)ap = 2; // Modifying the first 2 bytes of the 4-byte variable a directly through type casting, thus breaking the atomicity of the int variable
 
 // The code in Example 2 may occur in certain scenarios, but it has portability issues on machines with different endianness

The reason for these security risks in the C language is that it supports pointer operations and pointer type conversions. Therefore, in Go language, the most commonly used ordinary pointers, which have types, have eliminated pointer arithmetic and type conversion operations to ensure type safety. Here’s an example:

1
2
3
4
var a int32 = 10
var ap *int32 = &a  // Ordinary pointer with type ​
ap++        // Illegal, pointer arithmetic is not allowed
p := (*int16)(ap) // Illegal, *int32 cannot be directly converted to *int16

This ensures that pointers always point to valid addresses with allocated memory and preserves type independence and atomicity.

In addition to ordinary pointers, Go language also retains two other types of pointers that allow bypassing the type system and achieving the same level of memory manipulation as in C language. The other two types of pointers are:

  • unsafe.Pointer
  • uintptr
    To understand these two, we need to establish a concept: a pointer is essentially a number that stores a memory address. The addressing space is 32 bits for a 32-bit machine and 64 bits for a 64-bit machine, so the size of a pointer is equal to the number of bits in the machine.

uintptr is straightforward; it is simply a number that stores a memory address. It is equivalent to uint32 a 32-bit machine and uint64 on a 64-bit machine. Since it is a number, it naturally supports arithmetic operations, which allows it to represent any memory location. However, the problem is that data cannot be operated solely based on its memory address; you also need to know its size. In other words, we cannot manipulate data solely based on a uintptr pointer. On the other hand, an ordinary typed pointer not only provides the address but also informs the compiler about the size of the data pointed to. For example, *int32 and *int64 pointers tell the compiler that they operate on 4-byte and 8-byte data, respectively.

Now that we have explained ordinary pointers and uintptr pointers in Go language, what is this additional unsafe.Pointer compared to C language?

unsafe.Pointer is a generic pointer that, like uintptr, only keeps the memory address without concerning itself with the type. However, the difference between unsafe.Pointer and uintptr is that the former refers to an object that will be referenced by the garbage collector (GC), so it will not be collected as garbage by the GC. In contrast, the latter only represents the memory address as a number, which means that if a data address is saved by uintptr, it will be mercilessly collected by the garbage collector.

Summary of the three types of pointers in Go language:

  • Ordinary pointer: This does not support pointer arithmetic, saves the address and type information, and the data it points to will not be garbage collected by the GC.
  • unsafe.Pointer: Does not support pointer arithmetic, saves the address but not the type information, and the data it points to will not be garbage collected by the GC.
  • uintptr: Supports address arithmetic, saves the address but not the type information, and the data it points to will be garbage collected by the GC.

In practical usage, uintptr cannot be directly converted to an ordinary pointer, and both must be first converted to unsafe.Pointer as an intermediate step before further conversion.

Here’s a simple example:

1
2
3
4
5
6
7
8
type Foo struct{
     a int32
     b int32
 }
 foo := &Foo{}
 bp := uintptr(unsafe.Pointer(foo)) + 4  // Add 4 to the address of foo to locate foo.b
 *(*int32)(unsafe.Pointer(bp)) = 1   // Convert to *int32 ordinary pointer and modify the value
 fmt.Println(foo.b)  // foo.b = 1
Built with Hugo
Theme Stack designed by Jimmy