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

ops/2024/10/21 2:55:17/

文章目录

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

相关文章

【论文阅读】03-Diffusion Models and Representation Learning: A Survey

Abstract(摘要) 扩散模型是各种视觉任务中流行的生成建模方法,引起了人们的广泛关注它们可以被认为是 自监督学习方法【通过数据本身的结构和特征来训练模型,而不是依赖外部标签】 的一个独特实例,因为它们独立于标签注…

博科测试IPO上市丨为行业提供智能测试综合解决方案

近年来,汽车制造、大型基础设施建设以及新能源开发等领域,对高精度、高效率的测试解决方案需求迫切。为推动行业发展,博科测试通过多年的技术积累以及自主创新,围绕伺服液压测试和汽车测试试验领域,积累了多项核心技术…

spring如何解决bean循环依赖的问题

1、概述 spring中,存在A依赖B,同时B又依赖A的情况,这种情况下,spring如何进行bean初始化呢? Service public class A {Autowiredprivate B b; }Service public class B {Autowiredprivate A a; } 本文来解释这个问题…

vector的模拟实现

1.迭代器失效 在上一篇中因为插入导致的扩容,扩容则pos指向的是之前的空间,导致了野指针的出现,没有扩容,使pos的位置意义改变,由于数据挪动,pos不再指向原来的位置,认为上面俩种迭代器失效。(…

重构复杂简单变量之用子类替换类型码

子类替换类型码 是一种用于将类型码替换为子类。当代码使用类型码(通常是 int、string 或 enum)来表示对象的不同类别,并且这些类别的行为有所不同时,使用子类可以更加清晰地表达这些差异并减少复杂的条件判断。 一、什么时候使用…

音视频入门基础:H.264专题(19)——FFmpeg源码中,获取avcC封装的H.264码流中每个NALU的长度的实现

一、引言 从《音视频入门基础:H.264专题(18)——AVCDecoderConfigurationRecord简介》中可以知道,avcC跟AnnexB不一样,avcC包装的H.264码流中,每个NALU前面没有起始码。avcC通过在每个NALU前加上NALUnitL…

Whisper 音视频转写

Whisper 音视频转写 API 接口文档 api.py import os import shutil import socket import torch import whisper from moviepy.editor import VideoFileClip import opencc from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request from fastapi.respons…

vue入门四-pinia

参考&#xff1a;丁丁的哔哩哔哩 vue3中如何设置状态管理 provide/infect 跨级通信1. vue2实现 <!-- index.js --> // 状态集中管理 // 数据实现响应式 // ref reactive--->对象中存储着状态msg,age,counterimport {reactive} from vue const store{state:reactive…