Golang 33_Golang内置new和make函数区别详解

一、Go 中 new() 和 make() 函数功能简介

1.1 Go 中 new() 和 make() 函数功能及区别简介

在 Go 中,new()make() 是两个常用的函数,用于创建和初始化不同类型的变量。

二者的函数签名如下:

1
2
func new(Type) *Type
func make(Type, size ...IntegerType) Type

new()make() 函数的区别主要有以下几点: new() 函数可以用于任意类型的变量,而 make() 函数只能用于 slicemapchannel 这三种引用类型的变量。 new() 函数返回的是指向类型零值的指针,而 make() 函数返回的是类型的值,已经初始化为非零值。 new() 函数只是分配内存,不涉及内存的初始化,而 make() 函数不仅分配内存,还会根据类型进行相应的初始化操作。

1.2 Go 值类型变量 和 引用类型变量

在 Go 中,使用 var 关键字可以进行变量声明,并且这些变量可以在程序中使用。当声明变量时没有为变量指定初始值,变量的默认值是它们的零值。

例如,int类型的零值为 0,字符串的零值是空字符串(""),而对于切片、映射和通道等引用类型,零值是 nil。

1
2
var i int
var s string

在示例中声明的两种类型的变量(默认值为 0 和 “"),声明变量和即可以直接使用它们(存放对应类型的值 或 从变量中读取值)。这种类型的变量称为值类型变量对于值类型,不需要分配内存,因为默认已经分配了内存。通常情况下值类型变量由 Go运行时在栈上为其分配内存空间(内存逃逸 及 全局变量等特殊情况下的变量除外)并进行自动回收。

但如果我们转而使用引用类型呢?

1
2
3
4
5
6
7
8
9
package main
import (
    "fmt"
)
func main() {
    var i *int
    *i=10
    fmt.Println(*i)
}

当运行程序时,这段代码将因为以下原因导致 panic:

1
panic: runtime error: invalid memory address or nil pointer dereference

从这个提示可以明显看出,对于引用类型变量,不仅需要声明它们,还需要为它们的内容分配内存;要分配内存,就需要使用 new()make()

二、Go 的内置 new() 函数详解

2.1 new() 函数的功能详解

new() 用于创建除了引用类型(slicemapchannel)以外的其它类型的指针变量。new()函数时在堆上申请分配内存空间并返回内存地址(指针),这种内存由Go的 GC 进行回收。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main
import "fmt"
type person struct {
    name string
    age uint8
}

func main() {
    numPtr := new(int)
    strPtr := new(string)
    personPtr := new(person)
    fmt.Println(*numPtr) // Result: 0
    fmt.Println(*strPtr) // Result: ""
    fmt.Println(*personPtr) // Result: {"" 0}
}

再来看下面的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
   "fmt"
   "unsafe"
)
func main() {
   numPtr := new(int) // 使用 new 创建一个 int 内存的 指针变量
  
   ptrValue := uintptr(unsafe.Pointer(numPtr)) // 获取 指针的地址
  
   fmt.Printf("%X\n", ptrValue) // C00001C0A8
   fmt.Printf("%P\n", numPtr)   // 0xc00001c0a8
}

在示例代码中,使用了 unsafe包中的类型,即 Pointer 和 uintptr,来处理指针。以下是代码的解析:

  • 首先使用 new(int) 创建一个指向 int 变量的指针(numPtr);
  • 接着,使用 unsafe.Pointer 将 numPtr(一个指向int的指针)转换为 unsafe.Pointer类型;
  • 然后,使用 uintptr 将 unsafe.Pointer 值转换为 uintptr 类型;
  • 最后,打印 uintptr 类型 和 指针的值,二者值等,表示创建的变量的内存地址。

2.2 new() 的 底层实现

new() 函数在底层使用了 golang 的 runtime.newobject 函数。runtime.newobject 函数会分配一块内存,大小为指定类型的大小,并将该内存清零。然后,runtime.newobject 函数会返回这块内存的指针。

runtime.newobject的实现如下:

1
2
3
4
5
6
7
8
// source file:  src/runtime/malloc.go 

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function.
func newobject(typ *_type) unsafe.Pointer {
	return mallocgc(typ.size, typ, true)
}

runtime.newobject 通过调用 mallocgc 在堆上按照 typ.size 的大小申请内存,因此 new() 只会为结构体申请一块内存空间,不会为结构体中的指针类型申请内存空间。

三、make() 函数详解

3.1 make() 函数的功能详解

make() 函数也是用于内存分配的,但是和 new() 不同,make() 函数仅用于 slice、map、channel 三种数据类型的内存创建,根据它们的类型进行内存初始化,其返回值是所创建类型的本身,而不是新的指针引用。

Tips::slice、map、channel这三种类型都是引用类型,所以没必要返回它们的指针了,必须得初始化,但是不是设置为零值。 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import "fmt"
 
func main() {
	// 使用 make 函数创建一个切片,并初始化长度为 3 的切片
	slice := make([]int, 3)
	fmt.Println(slice) // 输出 [0 0 0]
 
	// 使用 make 函数创建一个映射,并初始化键值对
	m := make(map[string]int)
	m["one"] = 1
	m["two"] = 2
	fmt.Println(m) // 输出 map[one:1 two:2]
 
	// 使用 make 函数创建一个通道,并初始化缓冲区大小为 10 的通道
	ch := make(chan int, 10)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch) // 输出 1
	fmt.Println(<-ch) // 输出 2
}

从输出结果可以看出,make 函数创建的变量都是类型的非零值,并返回该变量的值。我们可以直接使用变量进行操作。

3.2 make()函数的底层实现

make() 函数在底层使用了 golang 的 runtime.makesliceruntime.makemapruntime.makechan 函数。

下面是 make 函数的简化版本的底层实现原理示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main
import (
   "fmt"
   "reflect"
   "unsafe"
)
func main() {
   slice := make([]int, 3)
  
   sliceValue := reflect.ValueOf(slice)
   sliceData := sliceValue.Elem().UnsafeAddr()
   sliceLen := sliceValue.Len()
  
   fmt.Println(sliceData, sliceLen)
}

在示例代码中,使用了reflect包中的方法,包括 ValueElemUnsafeAddrLen,来处理切片。

  • 首先, 使用 make([]int, 3) 创建一个长度为3的切片 slice;
  • 然后,使用 reflect.ValueOf 将切片转换为 reflect.Value 类型;
  • 接着,使用 Elem 方法来访问切片的元素;
  • 进一步使用 UnsafeAddr 来获取切片底层数组的指针;
  • 最后,使用 Len 方法来获取切片的长度。

请注意,上面提供的示例代码使用了 reflect 和 unsafe包。这是为了演示make()的底层实现。在实际开发中,通常不需要频繁使用这些包。

  • runtime.makeslice 函数用于创建切片,它会分配一块连续的内存空间,并返回切片结构体。slice 的底层数据结构:
1
2
3
4
5
6
7
// source file: src/runtime/slice.go 

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

使用 make() 函数创建 slice 时,首先创建 slice struct 结构变量,并设置 len、cap 成员变量值,然后根据 cap 及 type类型 创建 slice 的底层数组 并使用 array 成员指向创建的底层数组,最后返回 slice 引用类型变量。

  • runtime.makemap 函数用于创建映射,它会分配一块哈希表内存,并返回映射结构体。

map 的底层数据相等较复杂,具体的实现可见 Golang 04_Golang映射map,其基础数据结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/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
}
  • runtime.makechan 函数用于创建通道,它会分配一块通道内存,并返回通道结构体。chan 的底层数据结构为:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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
}

具体实现 可参阅 Golang 09_Golang中协程与通道