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

文章目录

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

起因

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

go 复制代码
有一个结构体A
type A struct {
	fir int32
	sec []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() 会为切片分配底层数组的内存,并返回一个指向该数组的切片。
  • 切片中的每个元素会被初始化为其类型的零值。

例如:

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

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

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

go 复制代码
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时的差别

go 复制代码
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):从切片的起始位置到底层数组末尾的元素数量。

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

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

在实际使用中,切片是一个引用类型,SliceHeader 是一个抽象的表示,帮助理解切片的工作原理。切片的操作(如 appendcopy 等)会根据这些字段来管理内存和数据。

需要注意的是,SliceHeader 是一个概念上的结构,Go 语言中并没有直接暴露这个结构给用户。切片的实际实现和管理是由 Go 运行时负责的。

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

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

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

例如:

go 复制代码
s := make([]int, 2, 2)
s[0] = 1
s[1] = 2

s = 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 会分配一个更大的底层数组。
  • 原数组的元素会被复制到新数组中,这个过程是浅拷贝。
  • 切片的内部指针会更新为指向新数组,长度和容量也会相应更新。
相关推荐
CocoaKier1 天前
苹果谷歌商店:如何监控并维护用户评分评论
ios·google·apple
iOS日常1 天前
iOS设备崩溃日志获取与查看
ios·xcode
wangruofeng2 天前
AI 助力 Flutter 3.27 升级到 3.38 完整指南:两周踩坑与实战复盘
flutter·ios·ai编程
iOS日常2 天前
Xcode 垃圾清理
ios·xcode
开心就好20252 天前
不越狱能抓到 HTTPS 吗?在未越狱 iPhone 上抓取 HTTPS
后端·ios
傅里叶3 天前
iOS相机权限获取
flutter·ios
花酒锄作田3 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
zhangkai3 天前
flutter存储知识点总结
flutter·ios
齐生14 天前
网络知识点 - TCP/IP 四层模型知识大扫盲
笔记·ios
IT技术分享社区4 天前
数码资讯:iPhone 18 Pro,十大升级细节浮出水面
ios·手机·iphone