深入理解 Golang 中 New() 和 make() 的区别

在 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) // 输出创建的切片、映射和通道
}

在这个例子中,slicemapchan都是它们各自类型的实例,并且已经被初始化为特定的状态。 

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() 进行初始化是必不可少的,然后再对它们执行操作。

相关推荐

  1. 深入理解 Golang New() make() 区别

    2024-04-13 03:20:03       15 阅读
  2. golangmakenew有什么区别

    2024-04-13 03:20:03       31 阅读
  3. 深入理解 Golang 值类型引用类型

    2024-04-13 03:20:03       39 阅读
  4. make DESTDIR --prefix 区别

    2024-04-13 03:20:03       7 阅读
  5. Golang make vs new

    2024-04-13 03:20:03       40 阅读
  6. 深入理解Golanggoroutine 池

    2024-04-13 03:20:03       35 阅读
  7. 深入理解 golang 反射机制

    2024-04-13 03:20:03       35 阅读
  8. 深入理解GolangOptions模式

    2024-04-13 03:20:03       22 阅读
  9. Golangjsonjsoniter区别

    2024-04-13 03:20:03       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-13 03:20:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-13 03:20:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-13 03:20:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-13 03:20:03       18 阅读

热门阅读

  1. 构建高效运维平台与数据中心可视化方案

    2024-04-13 03:20:03       13 阅读
  2. <网络安全>《72 微课堂<什么是靶场?>》

    2024-04-13 03:20:03       16 阅读
  3. Anaconda的常用指令

    2024-04-13 03:20:03       14 阅读
  4. 桶排序:原理、实现与应用场景详解

    2024-04-13 03:20:03       14 阅读
  5. LeetCode 1. Two Sum

    2024-04-13 03:20:03       14 阅读
  6. 记录一次关于线程池任务编排和共享数据的尝试

    2024-04-13 03:20:03       13 阅读
  7. [AIGC] 分布式锁及其实现方式详解与Python代码示例

    2024-04-13 03:20:03       17 阅读
  8. Python学习入门(1)——基础语句

    2024-04-13 03:20:03       16 阅读
  9. ubuntu添加环境变量

    2024-04-13 03:20:03       17 阅读