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

server/2024/10/20 16:33:50/

文章目录

  • 起因
  • 二、得记住的知识点
    • 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/server/133390.html

相关文章

blender 金币基站 建模 学习笔记

一、学习blender视频教程链接 案例3:金币基站_建模_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Bt4y1E7qn?vd_sourced0ea58f1127eed138a4ba5421c577eb1&p12&spm_id_from333.788.videopod.episodes 二、金币基站建模过程 (1&#x…

Spring AI Java程序员的AI之Spring AI(三)RAG实战

Spring AI之RAG实战与原理分析 前言RAGDocumentDocumentReaderDocumentTransformerDocumentWriter VectorStoreSimpleVectorStoreRedisVectorStore元数据搜索组装提示词 前言 检索增强生成(RAG)是一种结合信息检索和生成模型的技术,用于将相…

MYSQL 学习(四):数据库管理

MYSQL 学习(四):数据库管理 文章目录 MYSQL 学习(四):数据库管理1. 数据库表的创建与管理1.1 创建数据库命名规范创建语法 1.2 管理语法 2. 创建和管理表2.1 创建语法结构2.2 基于现有表创建表2.3 查看表结…

chat_gpt回答:python从bin文件里读四字节整型

要从一个二进制文件(.bin 文件)中读取四字节的整型数值,你可以使用 Python 的 struct 模块,这个模块专门用于处理二进制数据的打包和解包。 下面是一个简单的示例,展示如何从二进制文件中读取四字节整型: …

【代理模式使用场景】

一般来说,代理模式使用场景是远程代理、虚拟代理、安全代理等。下面来详细介绍下这三个场景是什么,实现原理和特点。不过在介绍三个场景前,我们还是先来回顾下代理模式。 代理模式 定义 是结构型设计模式,引入一个对象控制对另…

深度学习基础知识-02 数据预处理

深度学习的数据预处理通常包括: 1.数据清洗:去除错误或不完整的数据。 2.归一化:调整数据范围,如将像素值缩放到0-1。 3.数据增强:通过旋转、缩放等方法增加数据多样性。 4.数据划分:将数据分为训练集、验证…

使用LLM和RAG进行数据库查询(文本到SQL)的四大挑战及解决方案

大型语言模型(LLM)的出现展示了机器理解自然语言的能力。这些能力帮助工程师完成了许多令人惊叹的工作,比如编写代码文档和代码审查,而最常见的用例之一是代码生成;GitHub Copilot展示了AI理解工程师代码生成意图的能力…

微信开发者工具:音乐小程序报错

报错信息 GET http://localhost:3000/1.mp3 net::ERR CONNECTION REFUSED (env: Windows,mp,1.06.2303220;lib:3.6.0) 原因:小程序没有直接获取本地文件,为了提高访问速度,而采用放到网络服务器中网络访问的方式获取文件内容 解决办法&#…