TL;DR: Use unsafe.Offsetof()
instead of unsafe.Sizeof()
. See Purpose of memory alignment for more information.
Fields in a struct (or even variables) don't necessarily occur immediately
after the other in memory. It is possible that there are gaps due to how the
data types are aligned. This alignment differs based on the word size and the
size of the data type.
In your example, you're trying to access v.j
using pointer arithmetic by
adding to the pointer of v.i
the size of v.i
(which is 4 bytes). This will
happen to work alright on 32-bit systems because the word size there is 4 bytes
and the next field, v.j
, will be aligned to 4 bytes in memory. Try it out by
setting the environment variable GOARCH=386
before executing the program
(386
is a 32-bit architecture).
On a 64-bit system though, v.j
will be aligned to 8 bytes in memory so that
the processor can fetch the whole 8-byte field from memory in one shot. If it was
not aligned to 8 bytes (the word size), it would need two fetches to get the
whole value since it would be stored in two different words. As a result, the value at &v.i
+ 4 bytes is not the start point
of v.j
. It instead points to an area of memory that's not allocated to any field, and hence v.j
isn't actually modified.
The right way to do it though is to get Go to give you the information of where
v.j
starts in memory using unsafe.Offsetof(v.j)
. You can then store an
int64
value in that location.
Here's a slightly modified version of your code to demonstrate the problem. It
prints the hex values of each byte in the struct.
// Slightly modified original code to print all the bytes in the struct
package main
import (
"fmt"
"unsafe"
)
type V struct {
i int32
j int64
}
func (v *V) PrintI() {
fmt.Println(v.i)
}
func (v *V) PrintJ() {
fmt.Println(v.j)
}
// PrintStruct prints every byte in the struct individually
func (v *V) PrintStruct() {
for i := uintptr(0); i < unsafe.Sizeof(*v); i++ {
b := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + i))
fmt.Printf("%2d %02x\n", i, *b)
}
}
func main() {
v := new(V)
iPointer := (*int32)(unsafe.Pointer(v))
// Use hex values so they're easy to see in the output
*iPointer = int32(0x01020304) // 4 bytes
jPointer := (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
*jPointer = int64(0x08090A0B0C0D0E0F) // 8 bytes
fmt.Println("Assign using offset Sizeof")
v.PrintStruct()
fmt.Println("")
v = new(V)
v.i = 0x01020304
v.j = 0x08090A0B0C0D0E0F
fmt.Println("Assign directly to the fields")
v.PrintStruct()
}
This is the output I get when running on a 64-bit machine which stores data in little endian order:
Assign using offset Sizeof
0 04
1 03
2 02
3 01
4 0f
5 0e
6 0d
7 0c
8 0b
9 0a
10 09
11 08
12 00
13 00
14 00
15 00
Assign directly to the fields
0 04
1 03
2 02
3 01
4 c0
5 00
6 00
7 00
8 0f
9 0e
10 0d
11 0c
12 0b
13 0a
14 09
15 08
Notice how the value gets assigned to a different (incorrect) location in
memory when using trying to store the value at an offset of the size of
int32
. Change unsafe.Sizeof(int32(0))
to unsafe.Offsetof(v.j)
and that
should give you the right result.