一、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()
函数只能用于 slice
、map
和 channel
这三种引用类型的变量。
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()
用于创建除了引用类型(slice
、map
和 channel
)以外的其它类型的指针变量。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.makeslice
、runtime.makemap
和 runtime.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包中的方法,包括 Value
、Elem
、UnsafeAddr
和 Len
,来处理切片。
- 首先, 使用
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中协程与通道