Golang中基结构体作为指针还是子结构体作为指针的选择

Golang中基结构体作为指针还是子结构体作为指针的选择 我想知道下面两个版本在内存使用效率方面是否有任何区别。第一个版本返回指针,但其内部对象不是指针。第二个版本将内部对象作为指针使用,但基础对象不是指针。

注意:对象可能会变得很大,目前我保持它们较小。供参考 - BulkResponse 完全不打算被修改。它只是从一个包返回到另一个包,仅此而已。

谢谢

V1

type Response struct {
	Duration   int 
	Success    bool
	Operations []Operation
}

type Operation struct {
	Create Item
	Update Item
	Delete Item
	List   Item
}

type Item struct {
	ID     string 
	Errors []Error
}

type Error struct {
	Code    int
	Reason  string
	Message string
}

func Version1() *BulkResponse {
	res := &BulkResponse{}
	// Build response ....
	return res
}

V2

type Response struct {
	Duration   int 
	Success    bool
	Operations []*Operation
}

type Operation struct {
	Create Item
	Update Item
	Delete Item
	List   Item
}

type Item struct {
	ID     string 
	Errors []*Error
}

type Error struct {
	Code    int
	Reason  string
	Message string
}

func Version2() BulkResponse {
	res := BulkResponse{}
	// Build response ....
	return res
}

更多关于Golang中基结构体作为指针还是子结构体作为指针的选择的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

以下是一些思考,如果你想进行推理(但只有基准测试才能为你提供实际案例的真相):

  • 额外的指针层会增加内存使用量,因为除了实际数据外,你还保存了另一个值(指针)。如果你有很多小对象,为每个切片对象设置一个指针将产生大量额外的内存消耗。
  • 你的结构体非常小(切片只是带有长度的指针,字符串也只是切片)
  • 整体内存占用很大程度上取决于你的字符串。如果你的字符串是动态且较长的,它们很可能远远主导你的内存占用(使得字符串驻留成为减少内存使用最有希望的途径)
  • 如果你更改切片的内容(添加/删除项),使用结构体指针会产生差异,因为纯结构体将被内联到一个连续的内存块中,而使用指针时,切片只包含指针,所有项作为单独分配的内存片段散布在堆中(这可能会大大增加内存占用,具体取决于每次内存分配的大小/对齐方式)

一个好的经验法则是,如果你没有确凿的理由说明为什么需要指针,或者为什么它们更快,通常应避免使用指针。如果你创建这些结构体一次后不再更改,将它们放在一个连续的内存块中(甚至可能在栈上)通常比将它们作为碎片散布在堆中更可取。

更多关于Golang中基结构体作为指针还是子结构体作为指针的选择的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这与我在另一个问题中发布的内容性质相似,其中包含了一些可能对你有帮助的信息:

标题: 有人能通俗地解释一下使用指针切片和结构体切片的区别吗? 获取帮助

我认为答案是“有时,也许”。在使用指针的情况下,你创建的是指针的副本(这会增加最小的开销,但确实存在开销)。因此,遍历你的结构体而不捕获其值,并通过索引访问它们可能是更好的做法:

func Structs(animals []Animal) {
	// 这里没有指针/结构体分配;只是索引
	for i := range animals {
		// 你也可以在这里捕获一个变量,例如 a := animals[i]
		fmt.Printf("%s %s %.2f", animals[i].Name, animals[i].Breed, animals[i…

在那个讨论中,我建议提问者进行基准测试:

来源: github.com

缩略图

GitHub - DeanPDX/struct-benchmarks

通过创建GitHub账户为DeanPDX/struct-benchmarks的开发做出贡献。

这正是我认为应该做的。编写一些基准测试并检查内存使用情况。另外,如果你真的想深入优化结构体的内存使用,可以看看填充/对齐。有一个工具可以用来分析你的项目:

来源: github.com

缩略图

GitHub - dkorunic/betteralign: 让你的Go程序使用更少内存(也许)

让你的Go程序使用更少内存(也许)

在内存使用效率方面,两个版本确实存在差异,主要区别在于内存分配模式和缓存局部性。

内存分配分析

V1版本(外层指针,内层值):

func Version1() *BulkResponse {
    res := &BulkResponse{}  // 1次堆分配
    // Operations切片本身在栈上,但底层数组在堆上
    // 所有Operation、Item、Error都是连续内存
    return res
}

V2版本(外层值,内层指针):

func Version2() BulkResponse {
    res := BulkResponse{}  // 栈上分配
    // 每个[]*Operation中的指针需要额外的堆分配
    // 每个*Error也需要单独的堆分配
    return res  // 值拷贝返回
}

性能差异示例

// 测试V1版本的内存分配
func benchmarkV1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        resp := Version1()
        _ = resp
    }
}

// 测试V2版本的内存分配
func benchmarkV2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        resp := Version2()
        _ = resp
    }
}

具体差异

  1. 内存局部性

    • V1:OperationItemError在内存中连续存储,CPU缓存友好
    • V2:指针解引用导致缓存不命中,访问开销更大
  2. 分配次数

    • V1:1次主要堆分配(BulkResponse),内部对象在同一个内存块
    • V2:多次堆分配(每个*Operation*Error都是独立分配)
  3. 垃圾回收压力

    • V1:单个大对象,GC跟踪简单
    • V2:多个小对象,GC需要跟踪更多指针

推荐方案

根据你的描述(对象可能变大,且不需要修改),建议使用V3混合方案

type Response struct {
    Duration   int 
    Success    bool
    Operations []Operation  // 保持值类型,连续存储
}

type Operation struct {
    Create Item
    Update Item
    Delete Item
    List   Item
}

type Item struct {
    ID     string 
    Errors []Error  // 小结构体用值类型
}

type Error struct {
    Code    int
    Reason  string
    Message string
}

// 返回指针避免大结构体拷贝
func CreateResponse() *Response {
    return &Response{
        Operations: make([]Operation, 0, estimatedSize),  // 预分配容量
    }
}

这个方案结合了两者优点:外层指针避免返回时拷贝,内层值类型保证内存连续性和缓存友好性。对于Error这种小结构体(24字节),值类型比指针更高效。

回到顶部