The Go Pointer Magic

  sonic0002        2021-10-03 02:18:57       4,205        0    

Go is a language with the pointer type, by which we can

  • Pass pointer into a function and update value in-place.
  • Add methods to a struct as (* T) A, which is different from (T) A().

However, the pointer is type-safe in Go, meaning that there are such restrictions of the pointer.

  • Different types of pointers are unconvertible.
  • Pointer type cannot be used for calculation.
  • Pointer types cannot be compared, either == nor !=.
  • No mutual assignment between different pointer-type variables.

For example, *int cannot be converted to *int32in the following code.

func main() {
  i := int32(1)  ip := &i  var iip *int8 = (*int8)(ip)
}
// cannot convert ip (type *int) to type *int32

Therefore, the only function of the pointer in Go is to point to the value’s memory address. This restriction improves code safety, simplifies usage, ultimately improving the overall robustness. But Go does provide a related workaround, among which uintptr and unsafe.Pointer are the most important types.

unsafe.Pointer

Go unsafe package is one of the most crucial Go packages, and we can play some magic with it.

Package unsafe contains operations that step around the type safety of Go programs.

Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.

— from Go unsafe package

In short, we use unsafe.Pointer to achieve the read and write operations on memory.

You will often see it when reading Go source. For example, in the latest 1.17, it is used for achieving the conversion from slice to array pointer.

The package unsafe enhancements were added to simplify writing code that conforms to unsafe.Pointer’s safety rules, but the rules remain unchanged.

— from Go 1.17 Release Notes.

What’s more, it optimizes the WriteString method in file.go, and replaces the previous copy with thein-place operation.

func (f *File) WriteString(s string) (n int, err error) {
  var b []byte  hdr := (*unsafeheader.Slice)(unsafe.Pointer(&b)) 
  hdr.Data = (*unsafeheader.String)(unsafe.Pointer(&s)).Data
  hdr.Cap = len(s)
  hdr.Len = len(s)
  
  return f.Write(b)
}

Type conversion

In the pointer-type conversion, unsafe.Pointer plays the following four roles.

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.

— from Go unsafe package

As is seen, we bypass the previous problem that different pointer types cannot be converted, and finish the conversion between*int and *int32 by rewriting the above code.

func main() {  i := int32(1)
  ip := &i
  var iip = (*int8)(unsafe.Pointer(ip))  fmt.Println("sip", *iip)
}
// output 1

Pointer Arithmetic

Unsafe package offers theSizeof and Offsetof methods for pointer arithmetic and movement. Here is the explanation from the official API.

Sizeof takes an expression x of any type and returns the size in bytes of a hypothetical variable v as if v was declared via var v = x. The size does not include any memory possibly referenced by x. For instance, if x is a slice, Sizeof returns the size of the slice descriptor, not the size of the memory referenced by the slice. The return value of Sizeof is a Go constant.

Offsetof returns the offset within the struct of the field represented by x, which must be of the form structValue.field. In other words, it returns the number of bytes between the start of the struct and the start of the field. The return value of Offsetof is a Go constant.

— from https://pkg.go.dev/unsafe#Offsetof

The example below is a more complicated one, which shows us how we modify two continuous variables in a struct with these two methods.

type Test struct{
  str string
  num int64
}func main(){
  t:= Test{str: "hello", num: 1}
  tp := unsafe.Pointer(&t)  tsp := (*string)(unsafe.Pointer(tp))
  *tsp = "world"  tip := (*int64)(unsafe.Pointer(uintptr(tp) + unsafe.Offsetof(t.num)))
  *tip = 2  fmt.Printf("t.str: %s, t.num: %d", t.str, t.num)
}

Keep the key of success in mind, that is, the attributes in a struct object are stored continuously, so we can directly assign the second num pointer variable through calculation and then complete the value update.

uintptr

The uintptr type is applied in the above example. And what is ·uintptr?

type uintptr uintptr

This is an unsigned integer type which is large enough to hold any pointer address. Therefore its size is platform dependent. It is just an integer representation of an address.

from — https://golangbyexample.com/understanding-uintptr-golang/

Theoretically, the value of any pointer is a value representing the address value, that is, uint. But the pointer, even unsafe.Pointer, cannot be converted to uint directly. So here comes the “bridge”, uintptr.

Back to the example, calculating the address of the second pointer. Only when we convert the tpof unsafe.Pointer type to the uint value via uintptr , can we perform the subsequent operation of adding the Offset(int64) result.

Conversion

As we can conclude from the above example, the conversion among*T, unsafe.Pointer, and uintptr are as follows. With the “switching hub” unsafe.Pointer and its methods, we can easily complete the pointer type conversion, movement, assignment, and other operations.

More Magic

A classic operation in Go is the zero-copy conversion betweenstring and byte[].

First, let’s look at the underlying implementations of string and slice, both of which rely on the uintptr type and lay the groundwork for the zero-copy implementation.

type StringHeader struct {
  Data uintptr
  Len  int
}type SliceHeader struct {
  Data uintptr
  Len  int
  Cap  int
}

See the implementation.

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

type Test struct {
	str string
	num int64
}

func main() {
	s := "abc"
	fmt.Println("string to bytes[]", string2bytes(s))
	b := []byte{'a', 'b', 'c'}
	fmt.Println("bytes[] to string", bytes2string(b))
}

func string2bytes(s string) []byte {
	sp := (*reflect.StringHeader)(unsafe.Pointer(&s))

	bp := reflect.SliceHeader{
		Data: sp.Data,
		Len:  sp.Len,
		Cap:  sp.Len,
	}

	return *(*[]byte)(unsafe.Pointer(&bp))
}

func bytes2string(b []byte) string {
	bp := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	sp := reflect.StringHeader{
		Data: bp.Data,
		Len:  bp.Len,
	}

	return *(*string)(unsafe.Pointer(&sp))
}

The end

Unsafe’s advantages are obvious, though it is not recommended officially to be used to manipulate pointers directly. It is fast, simple, and straightforward.

Even if you have to “refuse” unsafe because of the code specifications, it is still worth digging into. And you will come across it in various source codes, no matter in Go or in Kubernetes. After thoroughly test, find an opportune time to introduce it into your code!

Note: The post is authorized by original author to republish on our site. Original author is Stefanie Lai who is currently a Spotify engineer and lives in Stockholm, original post is published here.

POINTER  GOLANG  UNSAFE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

PHP best practices