在 Go 中,new() 和 make() 是两个常用的函数,用于创建和初始化不同类型的变量。
在 Go 中,new() 和 make() 函数是创建和初始化变量的重要工具,首先来简要说明下二者的区别。
new() 用于创建指定类型的变量,并返回该变量的指针,其值为零。
make() 用于创建和初始化引用类型变量,如 slice、map 和 channel。
下面结合代码展开详细解释。
变量声明
var i int
var s string
使用var关键字可以进行变量声明,并且这些变量可以在程序中使用。
当我们没有为变量指定初始值时,它们的默认值是它们的零值。
例如,int类型的零值为 0,字符串的零值是空字符串(""),而对于切片、映射和通道等引用类型,零值是 nil。
在示例中的两种声明类型中,我们可以直接使用它们并为输出分配值。但如果我们转而使用引用类型呢?
package main
import (
"fmt"
)
func main() {
var i *int
*i=10
fmt.Println(*i)
}
这个示例会打印0还是10呢?
以上所有都是不正确的。当你运行时,这段代码将因为以下原因导致 panic:
panic: runtime error: invalid memory address or nil pointer dereference
从这个提示可以明显看出,对于引用类型,我们不仅需要声明它们,还需要为它们的内容分配内存;不然我们的值会存放在哪里呢?这就是上面提到的错误消息的原因。
对于值类型,不需要分配内存,因为默认已经分配了内存。
要分配内存,需要使用 new() 和 make()。
什么是 new()
?
new
函数用于分配内存并返回一个指向新分配类型零值的指针。这意味着,当你使用new(T)
时,你会得到一个类型为*T
的指针,其中T
是任何可以被分配的类型(如结构体、数组、切片等)。new
函数对类型T
的每个字段都会调用其类型的零值初始化函数。
例如:
type MyStruct struct {
Field1 int
Field2 string
}
func main() {
p := new(MyStruct)
fmt.Println(p) // 输出指向MyStruct零值的指针
}
在这个例子中,p
是一个指向MyStruct
类型零值的指针。
new() 用于创建除了引用类型以外的其他类型的变量。
package main
import "fmt"
func main() {
numPtr := new(int)
// Result: 0
fmt.Println(*numPtr)
}
new() 函数在底层利用了 Go 的 runtime.newobject 函数。
runtime.newobject 函数分配了一个大小等于指定类型大小的内存块,并将该内存初始化为零。
然后,runtime.newobject 函数返回指向该内存块的指针。
以下是 new() 函数的底层实现的简化版本:
package main
import (
"fmt"
"unsafe"
)
func main() {
// To create a pointer to a zero-value int using new()
numPtr := new(int)
// To retrieve the value of the pointer
ptrValue := uintptr(unsafe.Pointer(numPtr))
fmt.Println(ptrValue)
}
在提供的示例代码中,我们使用了unsafe包中的类型,即Pointer和uintptr,来处理指针。以下是代码的解析:
我们首先使用new(int)创建一个指向int变量的指针(numPtr);
我们使用 unsafe.Pointer 将 numPtr(一个指向int的指针)转换为 unsafe.Pointer类型;
然后,我们使用 uintptr 将 unsafe.Pointer 值转换为 uintptr 类型;
最后,我们打印指针的值,这表示我们创建的变量的内存地址。
什么是make()
?
与new
不同,make
函数仅用于创建切片、映射和通道,并且它总是返回一个有初始值的实例,而不是零值。make
函数的返回类型是类型T
本身,而不是指向T
的指针。make
函数接受额外的参数来指定切片的长度(以及可选的容量)和映射的初始容量。
例如:
func main() {
slice := make([]int, 0, 10) // 创建一个长度为0,容量为10的整数切片
map := make(map[string]int) // 创建一个空的字符串到整数的映射
chan := make(chan int) // 创建一个整数通道
fmt.Println(slice, map, chan) // 输出创建的切片、映射和通道
}
在这个例子中,slice
、map
和chan
都是它们各自类型的实例,并且已经被初始化为特定的状态。
make() 用于创建和初始化引用类型的变量,例如 slice、map 和 channel。
它适用于创建这些变量,因为它们不是设置为零值,而是根据它们的类型进行内存初始化。
package main
import "fmt"
func main() {
slice := make([]int, 3)
// Result: [0 0 0]
fmt.Println(slice)
}
make() 函数在底层利用了 Go 运行时的 runtime.makeslice、runtime.makemap 和 runtime.makechan 函数。
runtime.makeslice 函数用于创建切片。它分配一个连续的内存块,并返回一个 slice 结构。
runtime.makemap 函数用于创建映射。它为哈希表分配内存,并返回一个 map 结构。
runtime.makechan 函数用于创建通道。它为通道分配内存,并返回一个 channel 结构。
以下是 make() 函数底层实现的简化版本:
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()的底层实现。在实际开发中,通常不需要频繁使用这些包。
现在让我们重新来看博客开头的示例,并看看如何解决错误。
现在我们知道错误是因为没有为它分配内存,让我们使用 new() 来分配内存。
func main() {
var i *int
i=new(int)
*i=10
fmt.Println(*i)
}
现在可以正确运行并打印出 10
。
总结
new(T)
用于分配内存并返回指向类型T
零值的指针。它适用于所有可以被分配的类型。make(T, args...)
用于创建切片、映射和通道,并返回初始化后的实例。它不适用于所有类型,只适用于它支持的三种引用类型。
new() 用于创建任意类型的变量,而 make() 专门用于创建引用类型的变量。 new() 返回指向指定类型的零值的指针,而 make() 返回指定引用类型的初始化值。 使用 new() 创建的变量设置为它们的零值,而使用 make() 创建的变量根据其类型进行初始化。 最后,需要注意的是,new() 和 make() 都在堆上分配内存。
在实际编码中,很少使用 new(),因为我们通常依赖于短变量声明和结构体字面量,这样更简洁、方便,并且避免了与指针相关的复杂性。
另一方面,make() 函数是不可或缺的。在处理 slice、map和 channel 时,使用 make() 进行初始化是必不可少的,然后再对它们执行操作。