文章目录
起因
序列化的时候居然给我空指针报错,哪nil啦???猛一顿查,查到了创建的结构体数组
事情是这样的(举例啊):有一个结构体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()
会为切片分配底层数组的内存,并返回一个指向该数组的切片。 - 切片中的每个元素会被初始化为其类型的零值。
例如:
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 操作时的差别:
make([]int, 0, 5)
:
- 创建一个长度为 0,容量为 5 的切片。
- 可以在不分配新内存的情况下 append 最多 5 个元素。
- 当 append 超过 5 个元素时,底层数组会自动扩容,分配新的内存,切片指向新数组。
make([]int, 0)
:
- 创建一个长度为 0,容量为 0 的切片。
- 任何 append 操作都会导致切片扩容,因为初始容量为 0。
- 每次 append 操作可能会导致内存重新分配,底层数组会自动扩容,分配新的内存,切片指向新数组。
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
是一个抽象的表示,帮助理解切片的工作原理。切片的操作(如 append
、copy
等)会根据这些字段来管理内存和数据。
需要注意的是,SliceHeader
是一个概念上的结构,Go 语言中并没有直接暴露这个结构给用户。切片的实际实现和管理是由 Go 运行时
负责的。
4. 切片扩容时,重新分配内存,原切片的数据怎么办?
在 Go
中,当切片需要扩容时,会进行以下操作:
- 分配新内存:
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
的指针会更新为指向新数组。
三、咳咳,总结一下
- 创建切片时的内存分配:
- 使用
make([]T, length, capacity)
创建切片时,会根据指定的容量分配底层数组的内存。 - 即使容量为 0,
make
仍然会创建一个切片结构,但底层数组的内存不会被分配,因为没有元素需要存储。
发散问题:
- “容量为 0,make 仍然会创建一个切片结构,只是没有分配底层数组的内存。”意思是 切片指向数组的指针为nil?
\qquad 在Go
中,当你使用make([]T, 0)
创建一个切片时,切片的内部结构确实被初始化,但它的底层数组指针并不是nil
。相反,它指向一个特殊的、零长度的数组。
具体来说:
- 切片的长度和容量都是 0。
- 切片的底层数组指针指向一个零长度的数组,而不是 nil。
- "切片的底层数组指针指向一个零长度的数组,而不是 nil"如何做到?
这是Go 语言设计的一部分,确保切片即使在容量为 0 时也能安全地使用。
- 零长度数组:
Go 运行时
会为切片分配一个零长度的数组。这是一个特殊的内存区域,专门用于处理这种情况。这个数组的地址是有效的,但它不占用实际的内存空间,因为没有元素需要存储。- 切片结构:切片的内部结构(如 SliceHeader)会被初始化,指针字段指向这个零长度数组。长度和容量字段都设置为 0。
- 安全性:这种设计确保了即使切片的容量为 0,切片的指针字段仍然是一个有效的地址。这意味着你可以安全地对切片进行操作(如 append),而不会导致空指针异常。
- 扩容机制:当你对一个容量为 0 的切片进行 append 操作时,Go 会自动分配一个新的底层数组,并将数据复制到新数组中。切片的指针、长度和容量会相应更新。
- 元素初始化:
- 底层数组的元素会被初始化为其类型的零值。
- 对于指针类型的切片,元素的零值是
nil
。
- 扩容时的行为:
- 当切片需要扩容时,Go 会分配一个更大的底层数组。
- 原数组的元素会被复制到新数组中,这个过程是浅拷贝。
- 切片的内部指针会更新为指向新数组,长度和容量也会相应更新。