当前位置: 首页 > news >正文

【Golang】踩坑记录:make()创建引用类型,初始值是不是nil!!

文章目录

  • 起因
  • 二、得记住的知识点
    • 1. make()切片,初始化了吗?
    • 2. make()切片不同长度容量,append时的差别
    • 3. 切片是指向数组的指针吗?
    • 4. 切片扩容时,重新分配内存,原切片的数据怎么办?
  • 三、咳咳,总结一下


起因

序列化的时候居然给我空指针报错,哪nil啦???猛一顿查,查到了创建的结构体数组
事情是这样的(举例啊)

有一个结构体A
type A struct {fir int32sec []int32
}
还有另一个结构体B
type B struct {a []*A
}然后我判断B.a是否为nil,若为nil就为a创建切片分配内存,并且为切片赋值上默认值,如此不就规避nil异常了嘛
if B.a == nil {B.a = make([]*A,5, 5)
}
后面我就直接调用了,然后就出现了开头说的报错。怎么样,你们能看出来是什么问题吗?

先说答案:
\qquad 因为a 是一个长度为 5 的 *A(指向 A的指针)切片。由于使用 make 函数创建了这个切片,并且没有对其进行初始化,因为它们是指针类型的切片元素,但没有被分配实际的 A 实例,所以每个元素的初始值是 nil。
在这个过程中,我逐渐理清了make创建过程,以及各种引用类型的创建之后的值,来记录一下咯

二、得记住的知识点

1. make()切片,初始化了吗?

在 Go 中,使用 make() 函数创建切片时,切片本身是被初始化的,但切片中的元素可能没有被初始化为非零值。
具体来说:

  • make([]T, length, capacity) 创建了一个类型为 []T 的切片,长度为 length,容量为 capacity。
  • 切片本身是一个引用类型,make() 会为切片分配底层数组的内存,并返回一个指向该数组的切片。
  • 切片中的每个元素会被初始化为其类型的零值。

例如:

a := make([]int, 5) // 创建一个长度为5的int切片

在这个例子中,a 是一个长度为 5 的 int 切片,其中每个元素都被初始化为 0,因为 int 类型的零值是 0。

如果你创建一个指针类型的切片:

b := make([]*MyStruct, 5) // 创建一个长度为5的*MyStruct切片

在这个例子中,b 是一个长度为 5 的 *MyStruct 切片,其中每个元素都被初始化为 nil,因为指针类型的零值是 nil

Go 中,使用 make() 函数创建的结果总是初始化的,但初始化的内容取决于类型:

  • 切片:make([]T, length, capacity) 创建的切片本身是初始化的,切片中的每个元素被初始化为其类型的零值。如果 T 是指针类型,那么这些元素会是 nil。
  • 映射:make(map[K]V) 创建的映射是初始化的,但映射中的键值对需要显式添加。
  • 通道:make(chan T) 创建的通道是初始化的,可以立即使用。

2. make()切片不同长度容量,append时的差别

make([]int, 0, 5)
make([]int, 0)
make([]int, 5, 5)

这三个切片在append时的差别:

Go 中,make([]int, length, capacity) 用于创建切片,length 是切片的初始长度,capacity 是切片的容量。append 操作会根据切片的容量来决定是否需要分配新的内存。以下是这三个切片在 append 操作时的差别:

  1. make([]int, 0, 5)
  • 创建一个长度为 0,容量为 5 的切片。
  • 可以在不分配新内存的情况下 append 最多 5 个元素。
  • 当 append 超过 5 个元素时,底层数组会自动扩容,分配新的内存,切片指向新数组。
  1. make([]int, 0)
  • 创建一个长度为 0,容量为 0 的切片。
  • 任何 append 操作都会导致切片扩容,因为初始容量为 0。
  • 每次 append 操作可能会导致内存重新分配,底层数组会自动扩容,分配新的内存,切片指向新数组。
  1. make([]int, 5, 5)
  • 创建一个长度为 5,容量为 5 的切片。
  • 切片初始时已经有 5 个元素,全部被初始化为零值。
  • 可以直接访问和修改这 5 个元素。
  • append 操作会从第 6 个元素开始,底层数组会自动扩容,分配新的内存,切片指向新数组。

3. 切片是指向数组的指针吗?

切片并不是直接指向数组的指针,但它确实包含了一个指向底层数组的指针。在 Go 中,切片的底层结构可以用一个结构体来表示,尽管在实际实现中它是由编译器处理的。切片的结构通常包含以下三个字段:

  • 指针(Pointer):指向底层数组的起始位置。
  • 长度(Length):切片中元素的数量。
  • 容量(Capacity):从切片的起始位置到底层数组末尾的元素数量。

这种设计使得切片可以灵活地表示数组的一部分,并且可以动态调整大小。切片的这种结构使得它们在内存管理和性能上都非常高效。

type SliceHeader struct {Data uintptr // 底层数组的指针Len  int     // 切片的长度Cap  int     // 切片的容量
}

在实际使用中,切片是一个引用类型,SliceHeader 是一个抽象的表示,帮助理解切片的工作原理。切片的操作(如 appendcopy 等)会根据这些字段来管理内存和数据。
需要注意的是,SliceHeader 是一个概念上的结构,Go 语言中并没有直接暴露这个结构给用户。切片的实际实现和管理是由 Go 运行时负责的。

4. 切片扩容时,重新分配内存,原切片的数据怎么办?

Go 中,当切片需要扩容时,会进行以下操作:

  1. 分配新内存:Go 会分配一个更大的底层数组,以容纳更多的元素。新数组的容量通常是现有容量的两倍,但具体增长策略可能会根据实现有所不同。
  2. 复制数据:现有切片的数据会被复制到新分配的数组中。这是一个浅拷贝操作,意味着只复制数据本身,而不是数据的引用。
  3. 更新切片:切片的内部指针会更新为指向新数组的起始位置,长度和容量也会相应更新。
    原切片的数据在扩容后仍然保持不变,且新切片会包含原切片的数据。旧的底层数组会被垃圾回收机制回收(如果没有其他引用指向它)。

例如:

s := make([]int, 2, 2)
s[0] = 1
s[1] = 2s = append(s, 3) // 触发扩容

在这个例子中,s 的初始容量是 2。当 append 第三个元素时,Go 会分配一个新的数组(容量可能为 4),将原来的数据 [1, 2] 复制到新数组中,然后将 3 添加到新数组中。s 的指针会更新为指向新数组。

三、咳咳,总结一下

  1. 创建切片时的内存分配:
  • 使用 make([]T, length, capacity) 创建切片时,会根据指定的容量分配底层数组的内存。
  • 即使容量为 0,make 仍然会创建一个切片结构,但底层数组的内存不会被分配,因为没有元素需要存储。

发散问题

  1. “容量为 0,make 仍然会创建一个切片结构,只是没有分配底层数组的内存。”意思是 切片指向数组的指针为nil?
    \qquad Go 中,当你使用 make([]T, 0) 创建一个切片时,切片的内部结构确实被初始化,但它的底层数组指针并不是 nil。相反,它指向一个特殊的、零长度的数组
    具体来说:
  • 切片的长度和容量都是 0。
  • 切片的底层数组指针指向一个零长度的数组,而不是 nil。
  1. "切片的底层数组指针指向一个零长度的数组,而不是 nil"如何做到?
    这是Go 语言设计的一部分,确保切片即使在容量为 0 时也能安全地使用。
  • 零长度数组Go 运行时会为切片分配一个零长度的数组。这是一个特殊的内存区域,专门用于处理这种情况。这个数组的地址是有效的,但它不占用实际的内存空间,因为没有元素需要存储。
  • 切片结构:切片的内部结构(如 SliceHeader)会被初始化,指针字段指向这个零长度数组。长度和容量字段都设置为 0。
  • 安全性:这种设计确保了即使切片的容量为 0,切片的指针字段仍然是一个有效的地址。这意味着你可以安全地对切片进行操作(如 append),而不会导致空指针异常。
  • 扩容机制:当你对一个容量为 0 的切片进行 append 操作时,Go 会自动分配一个新的底层数组,并将数据复制到新数组中。切片的指针、长度和容量会相应更新。
  1. 元素初始化:
  • 底层数组的元素会被初始化为其类型的零值。
  • 对于指针类型的切片,元素的零值是 nil
  1. 扩容时的行为:
  • 当切片需要扩容时,Go 会分配一个更大的底层数组。
  • 原数组的元素会被复制到新数组中,这个过程是浅拷贝。
  • 切片的内部指针会更新为指向新数组,长度和容量也会相应更新。

http://www.mrgr.cn/news/51972.html

相关文章:

  • 深入解析JavaScript中的箭头函数及其在React中的应用(箭头函数与传统函数的区别、如何在不同上下文中使用箭头函数)
  • 2024年工博会精彩回顾:ANDEAWELL助力制造企业实现智能化转型!
  • Spring AI 整体介绍_关键组件快速入门_prompt_embedding等
  • 华为OD机试 - 虚拟理财游戏 - 贪心算法(Python/JS/C/C++ 2024 D卷 200分)
  • 曲线的曲率和挠率
  • Javascript 构造http请求
  • 2010年国赛高教杯数学建模D题对学生宿舍设计方案的评价解题全过程文档及程序
  • 如何配置 Jenkins 主从架构以及结合 Gerrit 和镜像操作
  • 火语言RPA流程组件介绍--检测元素是否存在
  • 使用quartz定时任务实现支付单自动关单功能,并引入多线程+分段解决扫表延迟的问题
  • C++多款质量游戏及开发建议[OIER建议]
  • Anthropic分享RAG最佳实践:Contextual Retrieval
  • 图书库存控制:Spring Boot进销存系统的应用
  • 如何测试网络带宽
  • react18中如何监听localstorage的变化获取最新的本地缓存
  • 合并与变形
  • 高德开放平台——实时路径规划优化指南
  • Google Ads API v18 发布,开发者迎来全新功能与优化
  • Python机器学习
  • 84. 拉伸ExtrudeGeometry