1
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)
}

func main() {
    v := new(V)
    iPointer := (*int32)(unsafe.Pointer(v))
    *iPointer = int32(666)
    v.PrintI()
    jPointer := (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
    *jPointer = int64(777)
    v.PrintJ()
}

I get the sample code from [https://studygolang.com/articles/1414], I can't figure out why I always get j's value is 0.

Layne Liu
  • 452
  • 5
  • 10

1 Answers1

4

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.

svsd
  • 1,831
  • 9
  • 14