SoFunction
Updated on 2025-03-06

Detailed explanation of the    and uintptr in Golang

Preface

In daily development, I often see bigwigs using various uintptr to do all kinds of fun work. As a novice, I get angry when I see unsafe. I don’t understand the differences and scenes of the two, so I naturally don’t know what to do. Today we will learn this part of the knowledge.

uintptr

The definition of uintptr is under the builtin package and is defined as follows:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

Referring to the comments we know:

  • uintptr is an integer type (this is very important), note that it is not a pointer;
  • But it's enough to save any pointer type.

The unsafe package supports these methods to complete the conversion of [type]=>uintptr:

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

You can transfer any type variable into it and get the corresponding semantic uintptr to subsequently calculate the memory address (such as obtaining the next field address based on a structure field address, etc.).

Let’s take a look at what the Pointer under unsafe package is:

// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
//	- A pointer value of any type can be converted to a Pointer.
//	- A Pointer can be converted to a pointer value of any type.
//	- A uintptr can be converted to a Pointer.
//	- A Pointer can be converted to a uintptr.
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.
type Pointer *ArbitraryType

The ArbitraryType here is just for the sake of easy understanding by developers. Semantically, you can understand Pointer as a pointer that can point to any type.

This is very important. The scenarios we encountered before were usually: first define a type, and then we have a pointer corresponding to this type. And it is a general solution, no matter what type you are. By breaking through this limitation, we can have more capabilities during operation and facilitate adaptation to some general scenarios.

The official provides four scenarios supported by Pointer:

  • Pointer of any type can be converted to a Pointer;
  • A Pointer can also be converted into a pointer of any type;
  • uintptr can be converted to Pointer;
  • Pointer can also be converted to uintptr.

This powerful ability allows us to bypass the [type system] and lose the verification during the compilation period, so be careful when using it.

Use posture

General type transfer

func Float64bits(f float64) uint64 {
    return *(*uint64)((&f))
}

We take the pointer of f, convert it to , and then convert it to a pointer of uint64, and finally solve the value.

In fact, the essence is to treat it as a medium. When used, it can be converted from any type or converted to any type.

There are certain prerequisites for this usage:

  • The size of the converted target type (uint64) must not be larger than the original type (float64) (both sizes are 8 bytes);
  • There are equivalent memory layouts in the first and second types;

For example, converting int8 to int64 is not supported, let's test it:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	("int8 => int64", Int8To64(5))
	("int64 => int8", Int64To8(5))
}
func Int64To8(f int64) int8 {
	return *(*int8)((&f))
}
func Int8To64(f int8) int64 {
	return *(*int64)((&f))
}

After running, you will find that int64 => int8 conversion is normal, and there will be problems from small to large:

int8 => int64 1079252997
int64 => int8 5

Program exited.

Pointer => uintptr

What is essentially produced from Pointer to uintptr is the memory address of the value pointed to by this Pointer, an integer.

I would like to emphasize here:

  • uintptr refers to the specific memory address, not a pointer, and has no pointer semantics. You can print uintptr to compare whether the address is the same.
  • Even if an object is recycled for GC or other reasons, the value of uintptr will not change at all.
  • Objects associated with the uintptr address can be garbage collected. GC does not consider uintptr to be a live reference, so the object pointed to by the unitptr address can be garbage collected.

Pointer arithmetic calculation: Pointer => uintptr => Pointer

Converting a pointer to uintptr will get the memory address it points to, and we can calculate another uintptr in combination with SizeOf, AlignOf, Offsetof.

The most common scenarios in this type are [get variables in structures] or [elements in arrays].

for example:

f := (&) 
f := (uintptr((&s)) + ())

e := (&x[i])
e := (uintptr((&x[0])) + i*(x[0]))

The above two sets of operations are essentially the same. One is to directly take the address, and the other is to calculate size and offset.

Notice:The conversion and calculation of variables to uintptr must be done in one expression (atomicity needs to be guaranteed):

Error cases:

u := uintptr(p)
p = (u + offset)

The conversion from uintptr to Pointer must be in one expression. It cannot be saved with uintptr, and the next expression must be converted.

uintptr + offset calculates the address, and then converting it with Pointer is actually a very powerful ability. ILet's take a look at another practical example:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	length := 6
	arr := make([]int, length)
	for i := 0; i < length; i++ {
		arr[i] = i
	}
	(arr)
	// [0 1 2 3 4 5]
	// Take the 5th element of slice: By calculating the size of the first element + 4 elements, it is obtained	end := (uintptr((&arr[0])) + 4*(arr[0]))

	(*(*int)(end)) // 4
	(arr[4]) // 4
	
}

Arithmetic calculation cannot be performed, uintptr is actually a good supplement.

reflect package from uintptr => Ptr

We know that reflect's Value provides two methods Pointer and UnsafeAddr to return uintptr. The purpose of not using it here is to avoid users from converting the result to any type without importing the unsafe package, but this also brings problems.

As mentioned above, you must not save a uintptr first and then turn it. This result is very unreliable. So we have to turn it right after calling Pointer/UnsafeAddr .

Positive example:

p := (*int)(((new(int)).Pointer()))

Counterexample:

u := (new(int)).Pointer()
p := (*int)((u))

Practical cases

string vs []byte

Learn and apply it effectively, in fact, it can be achieved by referring to the first case converted above, without uintptr. The same idea is still the same. Use it as a medium. After the pointer conversion is completed, you can just solve the pointer and get the value.

import (
	"unsafe"
)
func BytesToString(b []byte) string {
	return *(*string)((&b))
}
func StringToBytes(s string) []byte {
	return *(*[]byte)((&s))
}

In fact, the operation of converting from []byte to string here is consistent with the design of the Builder under the strings package:

// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
	addr *Builder // of receiver, to detect copies by value
	buf  []byte
}
// String returns the accumulated string.
func (b *Builder) String() string {
	return *(*string)((&))
}

// Reset resets the Builder to be empty.
func (b *Builder) Reset() {
	 = nil
	 = nil
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
	()
	 = append(, p...)
	return len(p), nil
}

// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {
	()
	 = append(, s...)
	return len(s), nil
}

The design is to minimize memory copying. The essence is to maintain a byte array of buf.

In the design of the local pool, if there is no element that can return Get, another poolLocal will steal an element back. The operation of jumping to other pools is implemented using + uintptr + SizeOf. For reference:

func indexLocal(l , i int) *poolLocal {
	lp := (uintptr(l) + uintptr(i)*(poolLocal{}))
	return (*poolLocal)(lp)
}

This is the end of this article about detailed explanations of Golang and uintptr. For more related Golang uintptr content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!