golang - new和make和&T{}的区别 & 指针可能出现的nil报错、声明的本质、指针的内存地址、nil的本质
许多小伙伴在刚接触 Golang 的时候不理解为什么会有两个用于分配内存的函数: new 和 make,俗话说:存在即合理,下面就来详细说明下这两者的区别。一、new先看函数声明:func new(Type) *Typenew 是 Golang 的内建函数,用于分配内存,其中,第一个参数是类型,返回值是类型的指针,其值被初始化为“零”(类型对应的零值,int 初始化为0,bool初始化为 fals
许多小伙伴在刚接触 Golang 的时候不理解为什么会有两个用于分配内存的函数: new 和 make,俗话说:存在即合理,下面就来详细说明下这两者的区别。
引入——为什么需要new和make
还有 堆内存和栈内存的 先看看另一篇 讲引用传递还是值传递的博客
基础知识
package main
import "fmt"
func main() {
var p *int
p = new(int)
*p = 1
fmt.Println(p, &p, *p)
}
输出
0xc04204a080 0xc042068018 1
在 Go 中 *
代表取指针地址中存的值,&
代表取一个值的地址
对于指针,我们一定要明白指针变量储存的是一个值的地址,但本身这个指针变量也需要地址来储存
如上
- p 是一个指针,他的值为内存地址
0xc04204a080
- 而 p这个指针也有一个内存地址, 它的内存地址为
0xc042068018
- 内存地址
0xc04204a080
储存的值为 1
由此,我们可以得到下面的结论
- 指针变量本身也有上层地址
- nil的本质是指针的初始默认值 代表没有内存地址
- 声明变量和初始化或者赋值不同
- 声明变量实际上就是给这个变量赋初始值和用的时候自动分配空间,但是指针类型比较特殊,它的默认值是nil,也就是相当于声明后没有给它(
*i
)分配空间!而值类型申明后会自动分配
- 声明变量实际上就是给这个变量赋初始值和用的时候自动分配空间,但是指针类型比较特殊,它的默认值是nil,也就是相当于声明后没有给它(
注意 go中数组和结构体都是值类型,在这个和Java是有区别的
错误实例
在 golang 中如果我们定义一个指针并像普通变量那样给他赋值,例如下方的代码
package main
import "fmt"
func main() {
var i *int
*i = 1
fmt.Println(i, &i, *i)
}
就会报这样的一个错误
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x498025]
报这个错的原因是 go 初始化指针的时候会为指针 i 的值赋为 nil ,但 i 的值代表的是 *i 的地址, nil 的话系统还并没有给 *i 分配地址,所以这时给 *i 赋值肯定会出错
解决这个问题非常简单,在给指针赋值前可以先创建一块内存分配给赋值对象即可
package main
import "fmt"
func main() {
var i *int
i = new(int)
*i = 1
fmt.Println(i, &i, *i)
}
对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢?这就是上面错误提示的原因。
但是对于值类型的声明不需要,是因为已经默认帮我们分配好了。
package main
import "fmt"
func main() {
var i int
fmt.Println(i)//默认为0
}
一、new
先看函数声明:
func new(Type) *Type
//eg
id := new(int)
new 是 Golang 的内建函数,用于分配内存,其中:
- 第一个参数是类型
- 返回值是对应类型的指针,其值被初始化为“零”(类型对应的零值)
- int 初始化为0
- bool初始化为 false
- 引用类型被初始化为nil(所以new对引用的引用没有意义,后面会讲))。
例如:
package main
import "fmt"
func main() {
id := new(int)
name := new(string)
flag := new(bool)
fmt.Printf("id type: %T value: %v\n", id, *id)
fmt.Printf("name type: %T value: %v\n", name, *name)
fmt.Printf("flag type: %T value: %v\n", flag, *flag)
}
输出:
id type: *int value: 0
name type: *string value:
flag type: *bool value: false
从上述例子可以看到,初始化的“零”值根据类型不同而不同,整数初始化为 0,字符串初始化为空,bool类型初始化为 false。
var a *string
a = new(string)
上面实际上是这样
new函数的本质是给指针变量所指向的位置分配内存,同时把分配的内存置为该类型的零值。换句话说,new函数可以帮我们做之前系统自动为值类型数据类型做的事。
说白了,new函数就是为了解决引用类型的零值问题,nil算不上是真正意义上的零值,所以需要new函数为其“仙人指路”。
二、make
先看函数声明:
func make(t Type, size ...IntegerType) Type
s := make([]int, 5) // 创建长度为5的slice
m := make(map[string]int) // 创建map
c := make(chan int) // 创建channel
// make 是必须的,不然下面会 panic:
var m map[string]int
m["a"] = 1 // ❌ panic: assignment to entry in nil map
m2 := make(map[string]int)
m2["a"] = 1 // ✅ ok
make 是 Golang 的内建函数,仅用于分配和初始化 slice、map 以及 channel 类型的对象,三种类型都是结构。返回值为类型对象,而不是指针。
因为这三种类型的实际数据层就是引用类型,所以就没有必要返回他们的指针了。
而且注意 结构体不包含在内
附录 slice map channel的底层结构
slice 源码结构:
type slice struct {
array unsafe.Pointer //指针
len int //长度
cap int //容量
}
map 源码结构:
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
channel 源码结构:
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
三、T{}与&T{} 、堆内存与栈内存
{}代表初始化,T是类型
所以 &T{} 代表取地址
- 栈上分配
当你在一个函数内部定义一个结构体变量时,该结构体会被分配在调用栈上。这意味着该结构体的生命周期仅限于声明它的函数。例如:
package main
import "fmt"
func main() {
var s struct {
a int
b string
}
s.a = 10
s.b = "hello"
fmt.Println(s)
}
package main
import "fmt"
func main() {
var s struct {
a int
b string
}
s = s{}
fmt.Println(s)
}
在这个例子中,s 是在 main 函数的栈上分配的。当 main 函数返回时,s 将不再存在,因为它是在函数的栈帧中定义的。
- 堆上分配
如果结构体是通过 new 关键字、使用 & 操作符与字面量(例如 &struct{})或者使用 make(尽管通常 make 用于切片、映射和通道,但理论上也可以用于自定义类型)创建的,那么这个结构体会被分配在堆上。例如:
package main
import "fmt"
func main() {
s := new(struct {
a int
b string
})
s.a = 10
s.b = "hello"
fmt.Println(s)
}
或者:
package main
import "fmt"
func main() {
s := &struct {
a int
b string
}{a: 10, b: "hello"}
fmt.Println(s)
}
在这些例子中,s 是指向堆上分配的结构体的指针。即使结构体很小,使用指针也可以避免在栈上分配大量数据。堆内存的生命周期通常与整个程序的运行时间相同,除非有明确的垃圾回收(GC)来回收不再使用的内存。
四 new与&{}、&t、{}
{}代表初始化
可以认为new(T)
是语法糖:
1、对基本简单类型,是 var t T; xxx:=&t
的语法糖。
// 方式1
ptr := new(T)
// 方式2
var t T //不是指针类型的话 声明的时候就初始化了
ptr := &t
2、对复杂类型而言 是 var t T; xxx:=&t{}
的语法糖
// 方式1
s := new(struct {
a int
b string
})
// 方式2
s := &struct {
a int
b string
}{}
而{}与&{}的区别在于一个是正常结构体,数据在栈上 一个是指针,数据在堆上
其实现实的编码中,new是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如:
u:=user{}//注意 有{}代表初始化 和上面例子有不同
这样更简洁方便,而且不会涉及到指针这种比麻烦的操作。
package main
import "fmt"
func main() {
a := map[int]string{}
fmt.Printf("%T, %v\n", a, a == nil)
a[1] = "ok"
fmt.Println(a)
}
new和make的对比
为什么针对slice, map和chan类型专门定义一个make函数?
答案:
首先 访问这几个结构 实际上是访问 xxx.data
这是因为slice
, map
和chan
的底层结构上要求在使用它们的时候必须初始化,如果不初始化,那它们实际的值data就是零值,也就是nil。
我们知道:
- map如果是nil,是不能往map插入元素的,插入元素会引发panic
- chan如果是nil,往chan发送数据或者从chan接收数据都会阻塞
- slice会有点特殊,理论上slice如果是nil,也是没法用的。但是append函数处理了nil slice的情况,可以调用append函数对nil slice做扩容。但是我们使用slice,总是会希望可以自定义长度或者容量,这个时候就需要用到make。
可以用new来创建slice, map和chan么? 答案:可以,但是没用
package main
import "fmt"
func main() {
a := *new([]int)
fmt.Printf("%T, %v\n", a, a==nil)//[]int, true
b := *new(map[string]int)
fmt.Printf("%T, %v\n", b, b==nil)//map[string]int, true
c := *new(chan int)
fmt.Printf("%T, %v\n", c, c==nil)//chan int, true
}
我们平常直接用slice的时候 不用写成xxxslice.data xxxslice.len的方式访问,这是因为我们平常用它的时候,相当于是语法糖,xxxslice相当于是
*xxx.Data
但是 初始化的时候 是根据其底层结构来的 new的赋值 是len cap为0 而data为nil
虽然new可以用来创建slice, map和chan,但实际上并没有卵用,因为new创建的slice, map和chan的值(实际上是xxx.data)都是零值,也就是nil。
这3种类型如果是nil,那遇到的问题我们在引入部分那里已经解答过了,这里不再赘述。
不重要-为什么slice是nil也可以直接append?
答案:对于nil slice,append会对slice的底层数组做扩容,通过调用mallocgc向Go的内存管理器申请内存空间,再赋值给原来的nil slice。slice用make创建的时候,如果指定的长度len>0,则make创建的slice下标索引从0到len-1的值都是对应slice里元素类型的零值。参考下例:
package main
import "fmt"
func main() {
s := make([]int, 2, 3)
fmt.Println(s) // [0 0]
}
结构体需要用new么?——此处注意!对指针类型无法嵌套赋值
注意 go中数组和结构体都是值类型,在这个和Java是有区别的
数值类型以及嵌套的数值类型不需要,指针类型需要
package main
import (
"fmt"
)
func main() {
type ManType struct{
name string
age int
}
type PointType struct{
a *int
}
type UserType struct{
id int
man ManType
point PointType
}
var user UserType
fmt.Println(user.id)//0
fmt.Println(user.man.age)//0 可以正常出来
fmt.Println(user.point)//{}
*user.point.a = 1//报错 panic: runtime error: invalid memory address or nil pointer dereference
}
package main
func main() {
type ManType struct{
name string
age int
}
type PointType struct{
a *int
}
type UserType struct{
id int
man ManType
point PointType
}
user := new(UserType)
*user.point.a = 1
}
同样报错 panic: runtime error: invalid memory address or nil pointer dereference
package main
func main() {
type ManType struct{
name string
age int
}
type PointType struct{
a *int
}
type UserType struct{
id int
man ManType
point PointType
}
user := UserType{}
*user.point.a = 1
}
同样报错 panic: runtime error: invalid memory address or nil pointer dereference
package main
func main() {
type ManType struct{
name string
age *int
}
user := ManType{}
*user.age = 1
}
同样报错 panic: runtime error: invalid memory address or nil pointer dereference
package main
func main() {
type ManType struct{
name string
age *int
}
user := ManType{}
user.age = new(int)
*user.age = 1
}
这样就正常了
结构体中的指针类型如何初始化
方法1:直接初始化
如果你在声明结构体时就直接初始化其指针字段,可以使用&操作符来分配内存,并使用new函数或复合字面值。
使用new函数
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
// 使用new分配内存
node := Node{Value: 1}
node.Next = new(Node) // 为Next分配内存
fmt.Println(node)
}
使用复合字面值
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
// 使用复合字面值分配内存并初始化指针字段
node := Node{Value: 1, Next: &Node{}} // 直接初始化Next指针指向的Node结构体
fmt.Println(node)
}
方法2:使用工厂函数或构造函数
你可以创建一个工厂函数或构造函数来初始化包含指针的结构体,这样可以更清晰地控制初始化过程,特别是在需要分配多个相关结构体时。
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
// 工厂函数来创建Node实例并初始化其Next指针
func NewNode(value int) *Node {
return &Node{Value: value, Next: nil} // 可以选择在这里进一步初始化Next指针,例如NewNode(value).Next = NewNode(someOtherValue)等。
}
func main() {
node := NewNode(1) // 创建并初始化Node实例及其Next指针为nil。如果需要,可以在这里进一步设置Next。
fmt.Println(node)
}
方法3:使用make(适用于切片和映射)但不适用于结构体指针的直接初始化,因为结构体不是引用类型。对于切片和映射的初始化,可以使用make。对于结构体指针,通常使用new或复合字面值。
结论:
对于结构体指针的初始化,最常用的是使用new函数或复合字面值。这样可以确保你的指针被正确地指向一个已分配的内存地址。使用工厂函数或构造函数也是一种很好的实践,尤其是在你需要进行更复杂的初始化逻辑时。选择哪种方法取决于你的具体需求和偏好。
用海象操作符不就可以直接赋值了吗?为什么还要new和make
package main
import "fmt"
func main() {
a := map[int]string{}
fmt.Printf("%T, %v\n", a, a == nil)
a[1] = "ok"
fmt.Println(a)
}
程序返回:
map[int]string, false
map[1:ok]
没错,就算没用make函数,我们也可以“人为”的给字典分配内存,因为海象操作符其实是声明加赋值的连贯操作,后面的空字典就是在为变量申请内存空间。但为什么系统还要保留new和make函数呢?事实上,这是一个分配内存的时机问题,声明之后,没有任何规定必须要立刻赋值,赋值后的变量会消耗系统的内存资源,所以声明以后并不分配内存,而是在适当的时候再分配,这也是new和make的意义所在,所谓千石之弓,引而不发,就是这个道理。
总结
分配的类型
- new 函数用于任何类型的分配,并返回一个指向该类型的指针。
- make 函数只用于分配slice、map和channel,并返回初始化后的切片、映射或通道对象。
返回值类型
- new 函数返回指向分配类型的指针。
- make 返回分配类型的初始化后的非零值。
要区分数值类和指针类
操作建议
- 尽量不使用new
- 对于slice, map和chan的定义和初始化,优先考虑使用make函数
更多推荐
所有评论(0)