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

news/2024/10/21 1:50:30/

文章目录

  • 起因
  • 二、得记住的知识点
    • 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.ppmy.cn/news/1540655.html

相关文章

CLion和Qt 联合开发环境配置教程(Windows和Linux版)

需要安装的工具CLion 和Qt CLion下载链接 :https://www.jetbrains.com.cn/clion/ 这个软件属于直接默认安装就行,很简单,不多做介绍了 Qt:https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/online_installers/ window 直接点exe Linux 先c…

http大数据post与put请求

大数据请求情况下出现post请求提交出错而put请求提交不出错 一、http方法特性差异 1、请求语义和用途不同 post通常用于 创建新资源Put一般用于更新现有资源服务器对于不同的HTTP方法可能有不同的处理逻辑和优化策略。在某些情况下,服务器可能对put请求的处理更加…

大厂面试提问:Flash Attention 是怎么做到又快又省显存的?

最近已有不少大厂都在秋招宣讲了,也有一些在 Offer 发放阶段。 节前,我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行了…

Scala中的reduce

作用:reduce是一种集合操作,用于对集合中的元素进行聚合操作,返回一个单一的结果。它通过指定的二元操作(即取两个元素进行操作)对集合中所有的元素进行递归处理,并最终将其合并为一个值。 语法&#xff1…

在合规的地方怎么用EACO地球链兑换交换价值?

地球链EACO(EarthChain,简称$E)是一种虚拟数字资产。 目前在中国大陆,虚拟资产相关业务活动属于金融活动,包括虚拟资产的交易、兑换等操作,因此应该谨慎去寻求如何用它来交换价值。 虚拟资产交易炒作活动&…

LeetCode 203 - 移除链表元素

题目描述 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回 新的头节点 。 解题思路 创建一个虚拟头节点dummyHead,并将其next指向给定的头节点head,这样可以避免处理头节点的特…

Android 禁止App字体随系统大小而更改

运营反馈,老年用户的手机多设置为大字体,在使用我们app过程中,由于字体被放大,导致布局错乱,部分功能按键遮挡,无法正常使用。   收到问题,着手解决,除了对界面布局进行改写&#…

Javascript与前端路由相关webAPI

与路由相关的浏览器Web API主要涉及前端路由的管理,这在单页应用(SPA)中尤为重要。以下是对这些API的详细解释: 一、前端路由的基本概念 前端路由是指在不重新加载页面的情况下,通过改变URL来更新页面内容的一种技术…