Golang中使用unsafe.Pointer实现类型别名的方法

Golang中使用unsafe.Pointer实现类型别名的方法 我想将一个包含单个接口的结构体重新解释为包含两个指针大小整数的结构体。

我对 unsafe.Pointer 规则的理解表明,这是一种有效的用法:(*T2)(unsafe.Pointer(&T1{...})),其中 T1T2 共享相同的内存布局。然而,我不禁怀疑垃圾回收器是否会混淆 x 实际指向的内容,从而导致错误地回收 val 的内容。

以下代码似乎按预期工作,但它安全吗?

package main

import (
	"fmt"
	"runtime"
	"time"
	"unsafe"
)

type Interface struct{ val interface{} }
type Uintptrs struct{ a, b uintptr }

func t() *Interface { return &Interface{val: "t=" + time.Now().String()} }

func main() {
	var x *Uintptrs = (*Uintptrs)(unsafe.Pointer(t()))
	runtime.GC()
	y := (*Interface)(unsafe.Pointer(x))
	fmt.Println(y.val)
}

https://goplay.space/#GKeGDekSXQH


更多关于Golang中使用unsafe.Pointer实现类型别名的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

这是一个很酷的解决方案。我可能会直接莽一下,在每个新的Go版本上进行测试,因为这种情况发生变化并导致破坏的风险非常低。你的解决方案至少可以防止uintptr神奇地变得比interface{}更大。

更多关于Golang中使用unsafe.Pointer实现类型别名的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据 unsafe package - unsafe - Go Packages 文档。

unsafe 包包含了一些可以绕过 Go 程序类型安全性的操作。

导入 unsafe 的包可能是不可移植的,并且不受 Go 1 兼容性准则的保护。

我认为你可以在某些特定情况下使用 unsafe 包,但不应作为常规做法。

这是有效的,垃圾回收器会正常工作。但我认为并不能保证你的两种结构体类型实际上具有等效的内存布局和大小。(即使目前情况如此。)

啊,是的,当 [0]byte 是结构体中的最后一个元素时,它会占用空间,否则指向它的指针将是指向结构体之外的指针,如果它在数组中,则可能是指向下一个结构体的指针,这会造成混淆。

感谢。有没有办法保证相同的内存布局?下面的代码怎么样?

type Interface struct{ val interface{} }
type SameLayout struct {
	a uintptr
	_ [unsafe.Sizeof(interface{}(nil)) - unsafe.Sizeof(uintptr(0))]byte
}

我还发现零填充是不可能的。看起来 [0]byte 在结构体中会占用空间,所以我还添加了:

const padding = unsafe.Sizeof(interface{}(nil)) - unsafe.Sizeof(uintptr(0))
const _ = -uint(padding) // assert(padding == 0)

type SameLayout struct {
	a uintptr
	// _ [padding]byte // only required if padding != 0
}

请注意,这可能变得有点过于复杂了。原始的带有注释的 Sizeof 断言可能足以避免未来的麻烦。

感谢 @calmh!我更进一步,添加了一些编译时断言。至少,如果将来有任何变化,它不会让我措手不及。 🙂

// Compile-time assert that the two types have the same size and alignment.
const _ = -uint(unsafe.Sizeof(Interface{}) ^ unsafe.Sizeof(SameLayout{}))
const _ = -uint(unsafe.Alignof(Interface{}) ^ unsafe.Alignof(SameLayout{}))

根据Go语言规范,你展示的代码存在潜在的内存安全问题。问题在于Interface.val字段包含一个接口值,而接口值在内存中实际上由两个指针组成:一个指向类型信息,一个指向实际数据。

package main

import (
    "fmt"
    "runtime"
    "time"
    "unsafe"
)

type Interface struct{ val interface{} }
type Uintptrs struct{ a, b uintptr }

func t() *Interface { 
    return &Interface{val: "t=" + time.Now().String()}
}

func main() {
    // 这里存在潜在问题:t()返回的指针可能被优化掉
    var x *Uintptrs = (*Uintptrs)(unsafe.Pointer(t()))
    
    // 强制GC可能会回收底层字符串数据
    runtime.GC()
    runtime.GC()
    
    y := (*Interface)(unsafe.Pointer(x))
    fmt.Println(y.val) // 可能访问已回收内存
}

更安全的做法是确保原始对象在整个生命周期内保持活跃:

func main() {
    // 保持对原始对象的引用
    orig := t()
    var x *Uintptrs = (*Uintptrs)(unsafe.Pointer(orig))
    
    runtime.GC()
    
    // 直接使用orig,而不是通过x转换回来
    fmt.Println(orig.val)
    
    // 或者确保转换后的使用不会超过原始对象的生命周期
    y := (*Interface)(unsafe.Pointer(x))
    _ = y // 使用y.val需要确保orig仍然存活
}

对于接口值的具体内存布局,可以使用以下代码验证:

func inspectInterface() {
    var iface interface{} = "test"
    
    // 接口值的内存表示
    type ifaceStruct struct {
        typ  uintptr
        data uintptr
    }
    
    ptr := (*ifaceStruct)(unsafe.Pointer(&iface))
    fmt.Printf("Type pointer: %#x, Data pointer: %#x\n", ptr.typ, ptr.data)
}

关键点:

  1. Interface.val是接口类型,在64位系统上占用16字节(两个指针)
  2. Uintptrs也是两个uintptr,布局匹配
  3. 但GC只跟踪Interface对象,不知道x指向同一内存
  4. 如果Interface对象被回收,通过x访问将导致未定义行为

安全使用unsafe.Pointer进行类型转换的前提是:

  • 确保原始对象在整个使用期间保持活跃
  • 两种类型确实具有相同的内存布局
  • 不违反Go的指针写入规则
回到顶部