起因:最近在翻看代码时,发现有的同时在使用golang gorm库查询单条数据时,和官方文档中有点区别:
同事的代码大致长这样:
golang">// 依据商品id查询商品详情
func (m GoodsModel) FindOneById(id uint32, field string) (*Goods, error) {info := &Goods{}db := mysqldriver.GetDB()err := db.Table(m.TableName()).Select(field).Where("id = ?", id).First(info).Errorif err != nil && err != gorm.ErrRecordNotFound {return info, err}return info, nil
}
如果这段代码是我来写大致长这样:
golang">// 依据商品id查询商品详情
func (m GoodsModel) FindOneById(id uint32, field string) (Goods, error) {info := Goods{}db := mysqldriver.GetDB()err := db.Table(m.TableName()).Select(field).Where("id = ?", id).First(&info).Errorif err != nil && err != gorm.ErrRecordNotFound {return info, err}return info, nil
}
gorm官方文档中给出的案例:
在我们的逻辑层是创建一个新的变量来接收FindOneById
的返回值
大致长这样:
golang">var res GooodsInfoResponse// 上面巴拉巴拉一堆逻辑// 查询sdb_goods商品信息
goodsInfo, _ := NewGoodsModel.FindOneById(31415, "id,name") // 下面也是巴拉巴拉一堆逻辑
// 但是在我们的逻辑层,并不会把goodsInfo返回出去
// 而是获取goodsInfo的值赋值到新的返回结构体中res.Name = goodsInfo.Namereturn res, nil
所以我的疑问点就在于, 同事为什么会在FindOneById
时将变量的指针地址返回出去
就我目前的了解,这会导致内存逃逸
重点来了
我所理解的相关知识
1.栈和堆的内存管理机制与变量的生命周期密切相关。简单来说,栈上分配的内存相对较小、生命周期较短,而堆上分配的内存较大、生命周期较长。Go 的垃圾回收器(GC)负责管理堆内存,自动回收不再使用的对象
2.逃逸分析决定了一个变量是否需要分配到堆上,主要看变量的生命周期。具体来说,如果变量 逃出了它所在的函数或者作用域(即超出了栈的作用范围),Go 会将其分配到堆上。
golang">// 由于我们返回了 a 的地址,a 会“逃逸”到堆上,因为它需要在 bar 函数外部存活
func bar() *int {a := 10 // 变量a分配在栈上return &a // 返回a的地址,导致a逃逸到堆上
}
然后我同事的解释有两点,我总结如下
1.指针地址是因为我们的变量有可能很大,在变量很大的情况下有可能会被自动分配到堆,所以返回指针地址
这一点据我了解,逃逸分析的核心是生命周期,而不是变量的大小。如果一个变量在函数外部仍然需要使用(比如返回值或者跨函数传递),那么它会被分配到堆上,不论它的大小如何。
2.在使用gorm中
First
方法时,本身就是将指针地址传递进去,干脆就直接申明指针地址,而不是直接申明变量,因为将栈上的变量指针地址传递给另一个方法时,会将栈上的变量重新分配到堆上.
这一点要懂哥来给我科普一下gorm包内部有没有什么知识点
至于
栈上的变量指针地址传递给另一个方法时,会将栈上的变量重新分配到堆上
这一点,我敢肯定的是,不会;这很明显是在狡辩,本来分配在栈上的变量怎么可能因为你获取变量的指针地址而发生逃逸呢?
获取 &a(即取地址)并不会立即将变量移到堆上,除非该地址被传递到外部,导致变量逃逸。获取指针本身通常是一个低开销的操作。
栈和堆的性能差异
栈上的内存访问速度较快,适合短生命周期的局部变量。
堆上的内存需要通过指针访问,访问速度较慢,且涉及到垃圾回收等额外开销。
虽然我们的系统跑的好好的,为什么觉得我还是觉得需要修改这段代码,是因为我始终觉得,当一个变量超出了作用域的时候就应该被销毁掉,不能过度依赖语言的垃圾回收机制,GC嘛本来就是一个开销比较大的操作,这里就不得不提Rust的强大了,人家压根就没有垃圾回收机制