Golang如何创建只读的任意数据结构
Golang如何创建只读的任意数据结构 假设目前无法实现这一点,那么另一个问题可能是:
如何在Go语言自身的堆/内存管理之外分配和使用Go数据结构?
我希望实现的方式:
通常,Go数据结构会被分配在栈上或堆上。但如果我想创建并管理自己的堆,当Go语言要在其堆上分配数据结构时,我能否以某种方式将其重定向,使其分配到我自己的堆上?
在我的堆上分配之后,就可以对数据结构进行写入,最后可以将内存标记为只读,前提是我的堆最初是作为操作系统内存映射分配的。
此时,只读数据结构理论上可以被任意的Go函数读取,但永远无法写入,因为内存是只读的,写入会导致段错误。
之后,当不再需要这个只读的任意数据结构时,由于底层堆由我控制,我需要自行管理它,很可能需要解除内存映射。
我为什么要这样做?
我希望拥有不可变的任意数据结构,这些结构保证永远不会改变,即使是任意的内存损坏也无法改变它们。这对于区块链应用可能很有用,因为数据结构一旦创建就永不改变。
操作系统内存映射如何提供帮助?
- 内存映射可以被标记为只读内存,这样即使是意外写入也不可能。
- 单个内存映射可以高效地增长到任意大小,而无需在需要增长时重新复制数据。例如,如果你有一个已经很大的内存映射无法再增长(因为其两侧已经分配了其他内存),操作系统会将其页面重新映射到一个不同的、不受阻碍且可以增长的内存地址,这比必须将内存复制到其他地方要高效得多。
或者,是否有其他方法可以实现相同的结果?
例如,假设我在Go堆上创建了一个任意的数据结构。有没有办法将其复制到我控制的内存映射中?所有的指针都必须被更新……以某种方式……怎么做?理论上,复制之后,我可以忽略/垃圾回收原始数据结构,只剩下只读副本?
还有其他建议吗?
更多关于Golang如何创建只读的任意数据结构的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你想保护内存免受何种变化的影响?是语言的正常使用?编程错误?反射访问?具有调试权限的进程?硬件故障?宇宙射线?还是FBI?这里需要更多信息。
更多关于Golang如何创建只读的任意数据结构的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
更新: 我尝试了复制 runtime 包的想法,并更改了包名,以有效地尝试创建一个可以查看所有私有结构体等的 runtime 包的工作副本。大约有 500 个文件,仅仅为了获取 map 结构体,这显得过于复杂。然而,复制的包最终无法编译。为什么?我遇到了这个问题:[1]。似乎 runtime 包无法独立编译,需要先运行某种 Golang 编译预脚本。我决定暂时不再深入这个兔子洞。
相反,我尝试从 runtime 中挑选出必要的常量、结构体和函数,以查看私有的 map 结构体并遍历它们。这被证明更成功,我没有遇到预脚本问题,也不需要任何 cgo 或汇编文件等。下一步是实际使用挑选并复制的 runtime 源代码 🙂
[1] go - Why “undefined: StackGuardMultiplierDefault” error? - Stack Overflow
我还不理解您的使用场景。您给出的其中一个理由是:
我希望拥有不可变的任意数据结构,这些结构保证永远不会改变,即使是任意的内存损坏也无法改变。
但我不明白您所说的“任意的内存损坏”是什么意思。如果您的内存出现硬件错误,或者存在强烈的电磁干扰,无论内存页的安全性如何,都可能损坏您的内存。
我不熟悉区块链,但根据我自己的(可能不适用)经验,我希望数据结构是只读的,以便我能在编译时捕获尝试修改状态的行为。
根据您给出的示例数据结构:
type A struct {
A int8
B string
C float32
}
type B struct {
D int8
E string
F float32
}
您能否将它们重构为类似这样的形式:Go Playground - The Go Programming Language
这以“更笨拙”的语法为代价,为您提供了编译时的安全性。尽管它不像普通的旧结构体和切片那样易于使用,但代码传达了“这里发生了一些奇怪的事情,您不能将这些数据结构视为简单的结构体和切片”的信息。
只读内存映射很有趣,只要您从不将任何指向 Go 内存的指针放入结构体中(这可能包括也可能不包括字符串),它就可以工作。据我所知,Go 垃圾收集器目前还不会移动对象并更新指向新位置的指针,但这在未来是很有可能的。如果您将指向某些 Go 内存分配的指针放入只读内存中,Go 可能会移动被指向的对象,从而导致:您的指针现在指向未初始化的内存,或者 Go 垃圾收集器将尝试更新只读内存中的指针,即使您的代码没有尝试写入该结构体,您也会遇到分段错误。只要您在只读内存中处理标量值或手动构建字符串(和/或数据结构中的任何其他指针类型),这可能没问题,但没有任何编译时安全检查,我能看到的唯一好处是,每当您修改内存时,您会得到一个分段错误并崩溃,而不是允许写入并悄无声息/无法检测地破坏程序的状态。
感谢您的回复 @skillian!
但我没明白您的意思;您说的“任意内存损坏”是指什么?如果您的内存出现硬件错误,或者存在强烈的电磁干扰,无论内存页的安全性如何,都可能导致内存损坏。
在这种情况下,我只是指,如果某些其他有缺陷的代码(例如,包装成 Golang 包的 C 库)极其罕见地和/或随机地向内存写入一个字节,那么如果该内存是只读的,这种损坏就会立即被发现。
您不能将它们重构为类似这样的形式吗:…… 这以“更笨拙”的语法为代价,为您提供了编译时的安全性。
感谢您的建议!
是的,问题在于现实中的结构要大得多,并且可能是分层的。当前的使用模式是:
- 许多函数对一个较大的结构进行读写,直到某个时间点,即构建阶段。
- 在某个时间点,较大的结构会变得固定/冻结/不可更改。
- 冻结之后,相同的函数应像以前一样正常工作以遍历这些结构,并且不应再进行任何更新。
保证最后一部分“并且不应再进行任何更新”是棘手之处。目前在代码中,这是通过对序列化后的数据结构计算加密哈希值来实现的。理论上,哈希值只需计算一次,就能唯一标识冻结的结构。但实际上,哈希值会被多次计算,以确保冻结的数据结构没有以某种方式发生改变,例如由于有缺陷的代码或内存损坏等原因。
只读内存映射很有趣,只要您从不将任何指向 Go 内存的指针放入结构体中(这可能包括字符串),它就可能有效。
谢谢,我也是这么想的。我有一个临时的概念验证,它将结构体、切片和字符串复制到自己的内存映射中。在这种情况下,存在指针,但它们是指向我自己在内存映射中创建的内存(而不是 Golang 的内存)。所以这意味着内存映射中的数据结构确实包含指针,但所有指针都指向内存映射内部的东西,并且所有这些东西都是我创建的。
这似乎可行,但是……我想对 Golang 的 map 实现同样的功能,现在问题开始了。如果任意结构体包含一个指向 map 的指针,那么我也想在内内存映射中重新创建这个 map。然而,map 的多个数据结构以及遍历它们的业务逻辑,都隐藏在 Golang 自己的 runtime 包中。所以不确定如何在自己的内存映射中重新创建/复制一个由 Golang 管理的 map。因为 Golang 管理的 map 使用了多个隐藏在 runtime 包内部的结构体,我无法在内存映射中重新创建它们。
到目前为止,我唯一的想法——可能很难实现——如下:
- 对于给定平台上给定版本的 Golang,我的包
xyz: - 获取
runtime源文件并将它们复制到我的包xyz中。 - 自动修改复制的源文件,使其包名变为
xyz而不是runtime。 - 假设我能让它编译,那么现在我有了一个内置到我包中的 runtime 的‘副本’。
- 这也意味着我可以从我的包中看到所有隐藏的 map 内部数据结构。
- 理论上,我可以自动修改
map.go的副本,使其在分配其结构时,不分配到堆或栈上,而是分配到我的内存映射中 🙂
作为附带功能,该代码不仅能够转储结构体,还能转储与这些结构体相关的所有 Golang 内部内存。例如,像 spew 这样的包已经可以转储任意复杂的 Golang 数据结构,但只是以相对高级的格式。是的,它会转储指针值,但它不会转储与 Golang 内部数据结构(如结构体内部填充、切片、字符串,以及更复杂的 map 内部结构体)相关的内存和指针。
所以我的计划是:
- 步骤 1:使用复制
runtime的技术来访问内部的 map 结构体。 - 步骤 2:完成我的‘转储器’,它可以转储与任意用户结构体相关的所有内存。
- 步骤 3:使用
runtime副本在任意内存位置重新创建 map,并使用步骤 2 的转储器来检查与克隆数据结构相关的所有内存是否都在我选择的内存映射中。
这里有很多问题需要解决,但理论上,在我将任意 map 重新创建到我选择的数据位置(例如,在我的内存映射中)之后,理论上,如果我能用‘真正的’ runtime map 函数访问内存映射中的那个 map,它应该就能工作。如果写入失败,要么是因为内存映射是只读的,要么是因为它试图更新非 Golang 管理的内部结构。
您怎么看?有替代方法吗?
我尝试使用一种快速但不严谨的方式,将一个简单的Golang数据结构深拷贝到内存映射中,使其成为自引用的:
$ cat unsafe-demo.go
package main
/*
$ go mod init unsafe-demo
$ go get github.com/edsrzf/mmap-go
$ go get github.com/jinzhu/copier
$ go run unsafe-demo.go
*/
import (
"fmt"
"unsafe"
"github.com/jinzhu/copier"
"github.com/edsrzf/mmap-go"
)
type A struct {
A int8
B string
C float32
}
type B struct {
D int8
E string
F float32
}
func main() {
a := A{A: 1, B: `foo`, C: 1.234567}
//b := B(a) cannot convert a (type A) to type B
b := *(*B)(unsafe.Pointer(&a))
a.A = 2 // only changes a.A and not b.D because b is a copy
fmt.Printf("- &a=%p a.A=%d a.B=%s a.C=%f\n", &a, a.A, a.B, a.C)
fmt.Printf("- &b=%p b.D=%d b.E=%s b.F=%f\n", &b, b.D, b.E, b.F)
memory, err := mmap.MapRegion(nil, 4096, mmap.RDWR, mmap.ANON, 0)
if err != nil {
panic(err)
}
memory[0] = 123
fmt.Printf("- &memory=%p memory=%p memory[0]=%d\n", &memory, memory, memory[0])
c := (*B)(unsafe.Pointer(&memory[0])) // c is now a pointer to type B struct in the memory map!
c.D = 111 // changes mmap byte[0] which is memory[0] AND c.D
fmt.Printf("- &c=%p c.D=%d c.E=%s c.F=%f // c=%p memory[0]=%d\n", &c, c.D, c.E, c.F, c, memory[0])
b.D = -1 // changes b.D to -1
copier.Copy(c, b) // deep copy b to c
fmt.Printf("- &c=%p c.D=%d c.E=%s c.F=%f // c=%p memory[0]=%d\n", &c, c.D, c.E, c.F, c, memory[0])
dumpByteSlice(memory[:64])
fmt.Printf("- &c.E=%p &c.F=%p\n", &c.E, &c.F)
sizeOfC := unsafe.Sizeof(*c)
fmt.Printf("- unsafe.Sizeof(c)=%d\n", sizeOfC)
fakeString := (*[3]byte)(unsafe.Pointer(&memory[sizeOfC]))
copy(fakeString[:], b.E) // copy string 'foo' to fake string in mmap
fakeString[0] = 'z'
p := (*uint64)(unsafe.Pointer(&memory[8]))
// fakeStringPtr := 0x0102030405060708
fakeStringPtr := (uintptr(unsafe.Pointer(&memory[sizeOfC])))
fakeStringU64 := uint64(fakeStringPtr)
*p = fakeStringU64 // store pointer to our fake Golang string
dumpByteSlice(memory[:64])
fmt.Printf("- c=%+v\n", c)
}
func dumpByteSlice(b []byte) {
var a [16]byte
n := (len(b) + 15) &^ 15
for i := 0; i < n; i++ {
if i%16 == 0 {
fmt.Printf("%4d", i)
}
if i%8 == 0 {
fmt.Print(" ")
}
if i < len(b) {
fmt.Printf(" %02X", b[i])
} else {
fmt.Print(" ")
}
if i >= len(b) {
a[i%16] = ' '
} else if b[i] < 32 || b[i] > 126 {
a[i%16] = '.'
} else {
a[i%16] = b[i]
}
if i%16 == 15 {
fmt.Printf(" %s\n", string(a[:]))
}
}
}
输出显示,内存映射中克隆的自引用数据结构似乎可以在Golang中使用:
$ go run unsafe-demo.go
- &a=0xc000136000 a.A=2 a.B=foo a.C=1.234567
- &b=0xc000136020 b.D=1 b.E=foo b.F=1.234567
- &memory=0xc000128018 memory=0x12e8000 memory[0]=123
- &c=0xc000130020 c.D=111 c.E= c.F=0.000000 // c=0x12e8000 memory[0]=111
- &c=0xc000130020 c.D=-1 c.E=foo c.F=1.234567 // c=0x12e8000 memory[0]=255
0 FF 00 00 00 00 00 00 00 47 16 10 01 00 00 00 00 ........G.......
16 03 00 00 00 00 00 00 00 4B 06 9E 3F 00 00 00 00 ........K..?....
32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- &c.E=0x12e8008 &c.F=0x12e8018
- unsafe.Sizeof(c)=32
0 FF 00 00 00 00 00 00 00 20 80 2E 01 00 00 00 00 ........ .......
16 03 00 00 00 00 00 00 00 4B 06 9E 3F 00 00 00 00 ........K..?....
32 7A 6F 6F 00 00 00 00 00 00 00 00 00 00 00 00 00 zoo.............
48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- c=&{D:-1 E:zoo F:1.234567}
继续沿着这条路走下去是否值得,还是说会遇到难以解决甚至无法解决的问题?
在Go中实现只读的任意数据结构确实是一个有挑战性的需求。虽然Go本身不直接支持这种内存管理方式,但可以通过一些系统级调用来实现类似的功能。以下是几种可能的实现方案:
方案一:使用mmap创建只读内存区域
package main
import (
"fmt"
"golang.org/x/sys/unix"
"os"
"unsafe"
)
type ROData struct {
data []byte
size int
}
func CreateReadOnlyData(size int) (*ROData, error) {
// 创建临时文件作为内存映射的后备存储
tmpfile, err := os.CreateTemp("", "rodata-*")
if err != nil {
return nil, err
}
defer os.Remove(tmpfile.Name())
// 扩展文件到所需大小
if err := tmpfile.Truncate(int64(size)); err != nil {
return nil, err
}
// 内存映射文件
data, err := unix.Mmap(int(tmpfile.Fd()), 0, size,
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
return nil, err
}
// 写入数据
copy(data, []byte("Hello, Read-Only World!"))
// 重新映射为只读
if err := unix.Mprotect(data, unix.PROT_READ); err != nil {
unix.Munmap(data)
return nil, err
}
return &ROData{data: data, size: size}, nil
}
func main() {
roData, err := CreateReadOnlyData(4096)
if err != nil {
panic(err)
}
// 读取数据 - 正常工作
fmt.Println(string(roData.data[:25]))
// 尝试写入 - 会触发段错误
// roData.data[0] = 'X' // 取消注释会导致panic
}
方案二:使用cgo分配和管理自定义堆
package main
/*
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
void* alloc_readonly_memory(size_t size, void* data, size_t data_size) {
void* mem = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) return NULL;
if (data != NULL && data_size > 0) {
memcpy(mem, data, data_size);
}
if (mprotect(mem, size, PROT_READ) == -1) {
munmap(mem, size);
return NULL;
}
return mem;
}
void free_readonly_memory(void* mem, size_t size) {
munmap(mem, size);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
type ImmutableStruct struct {
ID uint64
Value float64
Data [32]byte
}
func CreateImmutable(s *ImmutableStruct) unsafe.Pointer {
size := C.size_t(unsafe.Sizeof(*s))
ptr := C.alloc_readonly_memory(size, unsafe.Pointer(s), size)
if ptr == nil {
panic("Failed to allocate readonly memory")
}
return ptr
}
func main() {
data := &ImmutableStruct{
ID: 12345,
Value: 3.14159,
Data: [32]byte{'i', 'm', 'm', 'u', 't', 'a', 'b', 'l', 'e'},
}
roPtr := CreateImmutable(data)
// 通过指针读取数据
roData := (*ImmutableStruct)(roPtr)
fmt.Printf("ID: %d, Value: %f\n", roData.ID, roData.Value)
// 注意:不能写入roData,否则会段错误
}
方案三:序列化到只读内存映射
package main
import (
"encoding/gob"
"golang.org/x/sys/unix"
"bytes"
"fmt"
)
func SerializeToReadOnly(v interface{}) ([]byte, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(v); err != nil {
return nil, err
}
data := buf.Bytes()
size := len(data)
// 分配可写内存
mem, err := unix.Mmap(-1, 0, size,
unix.PROT_READ|unix.PROT_WRITE,
unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)
if err != nil {
return nil, err
}
// 复制数据
copy(mem, data)
// 设置为只读
if err := unix.Mprotect(mem, unix.PROT_READ); err != nil {
unix.Munmap(mem)
return nil, err
}
return mem, nil
}
type ComplexData struct {
MapData map[string]int
SliceData []float64
Nested *ComplexData
}
func main() {
data := &ComplexData{
MapData: map[string]int{"a": 1, "b": 2},
SliceData: []float64{1.1, 2.2, 3.3},
}
roMem, err := SerializeToReadOnly(data)
if err != nil {
panic(err)
}
// 反序列化只读数据
var decoded ComplexData
decoder := gob.NewDecoder(bytes.NewReader(roMem))
if err := decoder.Decode(&decoded); err != nil {
panic(err)
}
fmt.Printf("Decoded: %v\n", decoded.MapData)
}
重要注意事项
-
指针重定位问题:当复制包含指针的Go数据结构时,所有指针都需要重新计算偏移量。这通常需要自定义的序列化/反序列化逻辑。
-
内存对齐:确保自定义分配的内存满足Go类型的内存对齐要求。
-
垃圾回收:Go的垃圾回收器不会管理自定义分配的内存,需要手动管理生命周期。
-
平台依赖:这些方案依赖于系统调用(mmap/mprotect),在不同操作系统上可能需要调整。
-
性能考虑:频繁创建只读内存区域可能影响性能,建议批量处理或使用内存池。
这些方案提供了在Go运行时之外管理只读数据结构的方法,但都需要仔细处理内存管理和错误处理。对于区块链这类需要高度确定性和安全性的应用,这种底层内存控制是合理的。

