漫行记Wandering
Journal 3,600 字 12 分钟 Golang

Golang unsafe

unsafe is a package in the Go standard library , and it lets you break Go’s type safety and work with raw memory directly, a bit like C. Normally, Go prevents you from doing things that could crash your program(like arbitrary pointer arithmetic). unsafe gives you back some of that power, but at the cost of safety and portability.

Before we get start talking about unsafe, let’s do a quick refresher on pointers in Go.

Pointers in Go

A pointer is just the memory address of a value. You can get a pointer to a variable using the & operator, and you can dereference a pointer (get the value it points to) using the * operator.

x := 1
p := &x  // p is a pointer to x
fmt.Println(*p) // prints 1
fmt.Printf("%p, %p\n", &x, p) // prints the same address

A pointer’s type must match, which means a *int pointer can’t point to a string variable.

So normally, we can’t do pointer arithmetic or convert one pointer type to another. We also can’t look inside the raw memory layout of types. That’s why Go code is normally safe and portable across different machines.

Now here’s where unsafe.Pointer comes in. it’s way to escape above rules and treat pointers more like raw memory addresses.

unsafe

unsafe escapes Go’s type system.

Reinterpreting Types with unsafe.Pointer

Normally Go won’t let you cast between pointer types, if you want to do that, you need to use unsafe.Pointer.

  • unsafe.Pointer is a generic pointer type that can hold any pointer value.
  • You can convert any pointer type to unsafe.Pointer and back to another pointer type.
  • Cannot do arithmetic directly on unsafe.Pointer, it’s still a pointer in essence, not a number.
f := 3.14
pf := &f
pu := (*uint64)(pf) // compile error

But with unsafe.Pointer, you can do this:

f := 3.14
pf := &f
pu := (*uint64)(unsafe.Pointer(pf)) // pu points the same memory
fmt.Println(*pu) // prints 4614253070214989087
  • Both float64 and uint64 are 8 bytes.
  • pu and pf point to the same memory location. Actually, That’s not a conversion, it’s just a reinterpretation of memory. But from Go’s type system perspective, we converted a *float64 to *uint64 type. We told Go, “treat the 8 bytes at this address as a uint64 instead of a float64.”

This is useful for low-level stuff like serialization, binary formats or other cases where you need to manipulate raw bytes. But you have to be careful that the types are compatible in size and layout.

Pointer Arithmetic with unsafe.Pointer + uintptr

unitptr is an integer type large enough to hold a pointer’s value and we can do arithmetic on it.

Please note that uintptr is just a number, if you convert a pointer to uintptr, do some arithmetic, and then please convert it back to unsafe.Pointer immediately, Otherwise, the garbage collector might move or collect the value.

type T struct {
    A int32  // 4 bytes
    B float64 // 8 bytes
    C byte   // 1 byte
}
t := T{A: 1, B: 3.14, C: 'x'}
p := unsafe.Pointer(&t) // p points to t
pb := (*float64)(unsafe.Pointer(uintptr(p) + unsafe.Offsetof(t.B))) // pB points to t.B
fmt.Println(*pb) // prints 3.14

This operation is dangerous

  • If Go moves memory(like with the garbage collector), the pointer arithmetic may be wrong.

    *If a GC happens after you converted to uintptr but before you convert back to unsafe.Pointer, it might move original value somewhere else in memory. In this case, the uintptr value is now invalid.

  • If you miscalculate, it may crash program or read garbage data.

unsafe.Sizeof and unsafe.Alignof

Let’s take a look at below struct:

type T struct {
    x byte   // 1 byte
    y int32  // 4 bytes
}

so what’s the size of T? you might think it’s 5 bytes(1 + 4), but actually it’s 8 bytes.

t := T{}
fmt.Println(unsafe.Sizeof(t)) // prints 8
fmt.Println(unsafe.Offsetof(t.x)) // prints 0
fmt.Println(unsafe.Offsetof(t.y)) // prints 4

So why it’s 8 bytes? it’s because of alignment and padding.

  • unsafe.Alignof(T) gives the alignment of the whole struct T.
  • unsafe.Alignof(t.y) gives the alignment of field y. In our cases, it’s 4 bytes.

The rule here is that each field must start at an offset that’s a multiple of its alignment.

  • x(byte) has alignment of 1, so it starts at offset 0.
  • y(int32) has alignment of 4, so it must start at an address divisible by 4. Since x takes 1 byte, there are 3 bytes of padding after x to align y to offset 4.

So compiler inserts 3 padding bytes after x to make sure y is aligned.

Struct Field Reordering

From above example, we can see that field order matters. So we can reorder fields to reduce padding. Here is an example:

type T struct {
    x byte   // 1 byte
    y int64 // 8 bytes
    z int32  // 4 bytes
}
println(unsafe.Sizeof(T{})) // prints 24

If we reorder fields like below, we can reduce size to 16 bytes:

type T struct {
    y int64 // 8 bytes
    z int32  // 4 bytes
    x byte   // 1 byte
}
println(unsafe.Sizeof(T{})) // prints 16

Or we can reorder like below, it’s 16 bytes as well

type T struct {
    x byte   // 1 byte
    z int32  // 4 bytes
    y int64  // 8 bytes
}
println(unsafe.Sizeof(T{})) // prints 16
← 随笔