记录一次golang中关于值传递和引用传递,内存逃逸的学习笔记


起因:最近在翻看代码时,发现有的同时在使用golang gorm库查询单条数据时,和官方文档中有点区别:

同事的代码大致长这样:

// 依据商品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).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return info, err
	}
	return info, nil
}

如果这段代码是我来写大致长这样:

// 依据商品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).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return info, err
	}
	return info, nil
}

gorm官方文档中给出的案例:

在这里插入图片描述

在我们的逻辑层是创建一个新的变量来接收FindOneById的返回值
大致长这样:

var res GooodsInfoResponse

// 上面巴拉巴拉一堆逻辑

// 查询sdb_goods商品信息
goodsInfo, _ := NewGoodsModel.FindOneById(31415, "id,name")		

// 下面也是巴拉巴拉一堆逻辑
// 但是在我们的逻辑层,并不会把goodsInfo返回出去
// 而是获取goodsInfo的值赋值到新的返回结构体中

res.Name = goodsInfo.Name

return res, nil

所以我的疑问点就在于, 同事为什么会在FindOneById时将变量的指针地址返回出去
就我目前的了解,这会导致内存逃逸

重点来了
我所理解的相关知识

1.栈和堆的内存管理机制与变量的生命周期密切相关。简单来说,栈上分配的内存相对较小、生命周期较短,而堆上分配的内存较大、生命周期较长。Go 的垃圾回收器(GC)负责管理堆内存,自动回收不再使用的对象

2.逃逸分析决定了一个变量是否需要分配到堆上,主要看变量的生命周期。具体来说,如果变量 逃出了它所在的函数或者作用域(即超出了栈的作用范围),Go 会将其分配到堆上。

// 由于我们返回了 a 的地址,a 会“逃逸”到堆上,因为它需要在 bar 函数外部存活
func bar() *int {
    a := 10      // 变量a分配在栈上
    return &a    // 返回a的地址,导致a逃逸到堆上
}

然后我同事的解释有两点,我总结如下

1.指针地址是因为我们的变量有可能很大,在变量很大的情况下有可能会被自动分配到堆,所以返回指针地址

这一点据我了解,逃逸分析的核心是生命周期,而不是变量的大小。如果一个变量在函数外部仍然需要使用(比如返回值或者跨函数传递),那么它会被分配到堆上,不论它的大小如何。

2.在使用gorm中First方法时,本身就是将指针地址传递进去,干脆就直接申明指针地址,而不是直接申明变量,因为将栈上的变量指针地址传递给另一个方法时,会将栈上的变量重新分配到堆上.

这一点要懂哥来给我科普一下gorm包内部有没有什么知识点

至于 栈上的变量指针地址传递给另一个方法时,会将栈上的变量重新分配到堆上 这一点,我敢肯定的是,不会;这很明显是在狡辩,本来分配在栈上的变量怎么可能因为你获取变量的指针地址而发生逃逸呢?
获取 &a(即取地址)并不会立即将变量移到堆上,除非该地址被传递到外部,导致变量逃逸。获取指针本身通常是一个低开销的操作。

栈和堆的性能差异
栈上的内存访问速度较快,适合短生命周期的局部变量。
堆上的内存需要通过指针访问,访问速度较慢,且涉及到垃圾回收等额外开销。


虽然我们的系统跑的好好的,为什么觉得我还是觉得需要修改这段代码,是因为我始终觉得,当一个变量超出了作用域的时候就应该被销毁掉,不能过度依赖语言的垃圾回收机制,GC嘛本来就是一个开销比较大的操作,这里就不得不提Rust的强大了,人家压根就没有垃圾回收机制

<think>首先,用户的问题是:“golang函数输入的类型是值传递还是引用传递”。我需要用中文回答这个问题。 关键点是:Go语言中函数参数的传递方式。Go中,函数参数默认是值传递(pass by value)。这意味着当调用函数时,参数的值会被复制到函数内部。对于基本类型(如int、float、bool),传递的是值的副本;对于结构体,也是值的副本;对于指针、切片、映射、通道函数类型,虽然传递的是指针的副本,但行为上类似于引用传递,因为指针指向原始数据。 - 值传递:参数的值被复制。 - 引用传递传递的是引用(如指针),函数内部修改会影响外部变量。 在Go中: - 基本类型:值传递。 - 指针类型:传递指针的副本,但通过指针可以修改原始数据。 - 切片、映射、通道:这些是引用类型,传递的是包含指针的结构,所以行为上类似于引用传递。 准确地说,Go只有值传递。但某些类型(如切片)内部有指针,所以函数内部修改切片元素会影响外部,但修改切片本身(如重新分配)不会影响外部。 回答时,我需要澄清这一点。 现在,回答结构: 1. 先回答问题:Go函数参数默认是值传递。 2. 解释细节:对于不同类型的行为。 3. 提供代码示例来演示。 用户要求用中文回答,所以我必须用中文。 然后,在回答结束后,必须提出3-5个相关问题。问题必须以“§§相关问题§§”开头,然后列出1. 2. 3. 等。 相关问题必须基于用户的问题我的回答。所以,我应该问关于Go语言参数传递的进一步问题。 例如: 1. 在Go中,如何实现引用传递? 2. 切片作为函数参数时,为什么修改元素会影响外部? 3. 指针值传递的区别是什么? 4. 如何避免值传递导致的性能问题? 5. 在Go中,函数返回值是值传递还是引用传递? 最后,确保输出格式正确: - 回答内容。 - 然后,一行“§§相关问题§§”,紧接着是问题列表,没有换行分散。意思是问题列表应该在“§§相关问题§§”后面直接列出,不要有空行。 在示例中,是: §§相关问题§§ 1. ··· 2. ··· 3. ··· 所以,我应该写: §§相关问题§§ 1. 第一个问题 2. 第二个问题 3. 第三个问题 现在,构建回答。 回答内容: - 开头:直接回答问题。 - 解释:Go中所有函数参数都是值传递,但某些类型如指针、切片、映射、通道函数类型的行为类似于引用传递,因为它们内部包含指针。 - 代码示例:展示一个基本类型值传递一个切片“引用传递”的例子。 例如: 在Go中,函数参数默认是值传递。这意味着当调用函数时,参数的值会被复制。例如,对于基本类型如int: ```go func modifyInt(x int) { x = 10 } func main() { a := 5 modifyInt(a) fmt.Println(a) // 输出5,因为x是a的副本 } ``` 对于切片: ```go func modifySlice(s []int) { s[0] = 100 // 修改切片元素会影响外部,因为切片内部有指针 } func main() { slice := []int{1,2,3} modifySlice(slice) fmt.Println(slice[0]) // 输出100 } ``` 但如果是重新分配切片: ```go func reassignSlice(s []int) { s = []int{4,5,6} // 不会影响外部,因为传递的是切片头部的副本 } func main() { slice := []int{1,2,3} reassignSlice(slice) fmt.Println(slice[0]) // 输出1 } ``` 现在,相关问题: 1. 在Go中,如何通过函数修改外部变量的值? 2. 切片映射作为函数参数时,为什么某些操作会影响外部而某些不会? 3. 值传递引用传递在性能上有什么影响? 4. Go中是否有真正的引用传递机制? 5. 函数返回值是值传递还是引用传递? 确保3-5个问题。 最终输出: - 回答(包括解释代码)。 - 然后“§§相关问题§§”行。 - 然后问题列表。 记住,不要有换行分散问题列表。</think>在Go语言中,函数参数的传递默认是**值传递**(pass by value)。这意味着当调用函数时,参数的值会被复制一份传递给函数内部。无论参数是基本类型(如int、float、bool)、结构体,还是指针、切片(slice)、映射(map)、通道(channel)或函数类型,Go始终传递值的副本。然而,对于某些类型(如指针、切片、映射等),由于它们内部包含指向底层数据的引用,函数内部对数据的修改可能会影响外部变量,但这并不改变Go只有值传递的本质。以下是详细解释代码示例: ### 关键点: - **基本类型结构体**:值传递,函数内修改不会影响外部变量。 ```go func modifyInt(x int) { x = 10 // 修改的是副本,不影响外部 } func main() { a := 5 modifyInt(a) fmt.Println(a) // 输出5,值未变 } ``` - **指针类型**:值传递指针的副本,但通过指针可以修改原始数据。 ```go func modifyViaPointer(p *int) { *p = 10 // 通过指针修改原始数据 } func main() { a := 5 modifyViaPointer(&a) fmt.Println(a) // 输出10,值被修改 } ``` - **切片、映射、通道等引用类型**:值传递其内部结构(包含指向底层数据的指针),函数内修改元素会影响外部,但重新分配整个变量不会。 ```go func modifySlice(s []int) { s[0] = 100 // 修改元素会影响外部 } func reassignSlice(s []int) { s = []int{4,5,6} // 重新分配不会影响外部,因为传递的是副本 } func main() { slice := []int{1,2,3} modifySlice(slice) fmt.Println(slice[0]) // 输出100 reassignSlice(slice) fmt.Println(slice[0]) // 输出100(未变) } ``` 总之,Go语言严格遵守值传递原则,但开发者可以利用指针或内置引用类型实现类似引用传递的效果。这有助于避免意外修改,同时提供灵活性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值