许多小伙伴在刚接触 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)分配空间!而值类型申明后会自动分配

注意 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{} 代表取地址

  1. 栈上分配
    当你在一个函数内部定义一个结构体变量时,该结构体会被分配在调用栈上。这意味着该结构体的生命周期仅限于声明它的函数。例如:
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 将不再存在,因为它是在函数的栈帧中定义的。

  1. 堆上分配
    如果结构体是通过 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, mapchan的底层结构上要求在使用它们的时候必须初始化,如果不初始化,那它们实际的值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函数
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐