MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Gomake与new的区别

2022-09-292.9k 阅读

内存分配的基础概念

在深入探讨 Go 语言中 makenew 的区别之前,我们先来回顾一下内存分配的基本概念。在程序运行过程中,内存被分为不同的区域,主要包括堆(heap)和栈(stack)。

栈是一种后进先出(LIFO)的数据结构,用于存储函数的局部变量、参数以及函数调用的上下文信息。栈上的内存分配和释放非常高效,因为它遵循简单的规则,随着函数的进入和退出进行操作。

堆则是一个更加复杂的内存区域,用于存储程序运行过程中动态分配的对象。堆上的内存分配相对灵活,但管理起来也更加复杂,需要专门的垃圾回收(Garbage Collection,GC)机制来回收不再使用的内存,以避免内存泄漏。

在 Go 语言中,newmake 这两个关键字都与内存分配相关,但它们在分配内存的方式、适用的数据类型以及初始化方式等方面存在显著差异。

new 关键字详解

new 的基本用法

new 是 Go 语言中的一个内置函数,其主要作用是为类型分配内存空间,并返回指向该内存空间的指针。new 函数的签名如下:

func new(Type) *Type

其中,Type 是要分配内存的类型,函数返回一个指向新分配内存的指针,该内存被零值初始化。

下面通过一个简单的示例来展示 new 的用法:

package main

import "fmt"

func main() {
    var numPtr *int
    numPtr = new(int)
    *numPtr = 10
    fmt.Println(*numPtr)
}

在上述代码中,首先声明了一个 numPtr 变量,其类型为 *int,即指向 int 类型的指针。然后使用 new(int)int 类型分配内存,并将返回的指针赋值给 numPtr。接着通过指针 numPtr 对分配的内存进行赋值操作,最后打印出 numPtr 所指向的值。

new 适用的数据类型

new 适用于所有的内置类型、用户自定义类型(包括结构体)以及指针类型。对于结构体类型,new 同样会为结构体分配内存,并将结构体的所有字段初始化为零值。

以下是一个针对结构体使用 new 的示例:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    var personPtr *Person
    personPtr = new(Person)
    personPtr.Name = "Alice"
    personPtr.Age = 30
    fmt.Printf("Name: %s, Age: %d\n", personPtr.Name, personPtr.Age)
}

在这个例子中,定义了一个 Person 结构体,包含 NameAge 两个字段。通过 new(Person)Person 结构体分配内存,并通过指针 personPtr 对结构体的字段进行赋值和访问。

new 的内存分配与初始化

new 分配的内存位于堆上,这是因为栈上的内存生命周期与函数调用紧密相关,而 new 分配的对象可能在函数调用结束后仍然需要存在,所以必须在堆上分配。

new 分配的内存会被零值初始化。对于数值类型,零值为 0;对于布尔类型,零值为 false;对于字符串类型,零值为空字符串 "";对于指针、切片、映射、通道等引用类型,零值为 nil。这种零值初始化确保了新分配的内存处于一个已知的、可预测的状态,避免了未初始化变量带来的潜在问题。

make 关键字详解

make 的基本用法

make 也是 Go 语言的内置函数,但它与 new 有明显的不同。make 主要用于创建和初始化引用类型,如切片(slice)、映射(map)和通道(channel)。make 函数的签名如下:

func make(Type, size IntegerType) Type
func make(Type, size IntegerType, cap IntegerType) Type

其中,Type 是要创建的引用类型,size 是类型的初始长度,cap(可选)是类型的初始容量。

下面通过示例展示 make 用于创建切片的用法:

package main

import "fmt"

func main() {
    // 创建一个长度为 5,容量为 10 的切片
    slice := make([]int, 5, 10)
    for i := 0; i < len(slice); i++ {
        slice[i] = i * 2
    }
    fmt.Println(slice)
}

在上述代码中,使用 make([]int, 5, 10) 创建了一个 int 类型的切片,其长度为 5,容量为 10。然后通过循环对切片的元素进行赋值,并最后打印出切片。

make 适用的数据类型

如前所述,make 仅适用于切片、映射和通道这三种引用类型。这是因为这些类型在使用前需要进行特殊的初始化操作,而 make 正是为此而设计的。

以映射为例,下面展示 make 创建映射的用法:

package main

import "fmt"

func main() {
    // 创建一个初始容量为 10 的字符串到整数的映射
    myMap := make(map[string]int, 10)
    myMap["one"] = 1
    myMap["two"] = 2
    fmt.Println(myMap)
}

在这个例子中,通过 make(map[string]int, 10) 创建了一个字符串到整数的映射,并向映射中添加了两个键值对,最后打印出整个映射。

make 的内存分配与初始化

make 同样在堆上分配内存,但它不仅仅是分配内存,还会对分配的内存进行特定类型的初始化。对于切片,make 会根据指定的长度和容量分配内存,并将切片的长度和容量字段进行初始化。对于映射,make 会初始化映射的内部数据结构,以便能够存储键值对。对于通道,make 会初始化通道的缓冲区和相关的同步机制。

例如,对于切片 make([]int, 5, 10),会分配足够容纳 10 个 int 类型元素的内存空间,并将切片的长度设置为 5,容量设置为 10。而对于映射 make(map[string]int, 10),会初始化一个能够容纳大约 10 个键值对的映射结构。

makenew 的区别对比

适用数据类型的区别

new 适用于所有类型,包括基本类型、结构体、指针等,而 make 仅适用于切片、映射和通道这三种引用类型。这是两者最明显的区别之一。

例如,尝试使用 make 创建一个 int 类型变量是不合法的:

package main

func main() {
    // 编译错误:invalid operation: make(int) (make on non-slice, non-map, non-channel type int)
    num := make(int)
}

同样,使用 new 创建切片、映射或通道虽然语法上可行,但得到的是一个指向零值的指针,而不是经过初始化可直接使用的对象:

package main

import "fmt"

func main() {
    slicePtr := new([]int)
    // 此时 slicePtr 指向的切片为 nil,不能直接使用
    // 以下操作会导致运行时错误:invalid memory address or nil pointer dereference
    (*slicePtr)[0] = 10
    fmt.Println(*slicePtr)
}

内存分配与初始化方式的区别

new 只负责分配内存,并将内存初始化为零值,不涉及特定类型的初始化逻辑。而 make 不仅分配内存,还会根据类型的特点进行特定的初始化。

以切片为例,new([]int) 分配的是一个指向 []int 类型的零值(即 nil)的指针,并没有分配实际存储元素的内存空间。而 make([]int, 5) 会分配能够存储 5 个 int 类型元素的内存空间,并将切片的长度设置为 5,容量也为 5(如果未指定容量)。

对于映射,new(map[string]int) 得到的是一个指向 map[string]int 类型的零值(即 nil)的指针,不能直接用于存储键值对。而 make(map[string]int, 10) 会初始化一个可以存储大约 10 个键值对的映射结构。

返回值类型的区别

new 返回的是指向新分配内存的指针,即 *Type。而 make 返回的是类型本身,而不是指针。例如,make([]int, 5) 返回的是 []int 类型的切片,make(map[string]int, 10) 返回的是 map[string]int 类型的映射。

这种返回值类型的差异也影响了它们的使用方式。对于 new 返回的指针,在使用时需要通过解引用操作来访问所指向的值。而对于 make 返回的对象,可以直接使用其方法和操作。

实际应用场景分析

使用 new 的场景

  1. 创建简单变量并延迟初始化:当需要创建一个变量,但暂时不需要对其进行非零值初始化时,可以使用 new。例如,在某些情况下,先创建一个指针,然后在后续的逻辑中根据条件来决定如何初始化该指针所指向的值。
package main

import "fmt"

func initValue(numPtr *int) {
    if someCondition() {
        *numPtr = 10
    } else {
        *numPtr = 20
    }
}

func someCondition() bool {
    // 这里返回一个实际的条件判断结果
    return true
}

func main() {
    numPtr := new(int)
    initValue(numPtr)
    fmt.Println(*numPtr)
}
  1. 创建结构体对象并逐步初始化字段:对于结构体类型,new 可以创建一个结构体实例,并通过指针逐步初始化各个字段。这种方式在结构体字段较多,且初始化过程较为复杂时比较有用。
package main

import "fmt"

type ComplexStruct struct {
    Field1 int
    Field2 string
    Field3 []byte
}

func initComplexStruct(structPtr *ComplexStruct) {
    structPtr.Field1 = 100
    structPtr.Field2 = "Hello"
    structPtr.Field3 = []byte("World")
}

func main() {
    structPtr := new(ComplexStruct)
    initComplexStruct(structPtr)
    fmt.Printf("Field1: %d, Field2: %s, Field3: %s\n", structPtr.Field1, structPtr.Field2, structPtr.Field3)
}

使用 make 的场景

  1. 创建切片并进行元素操作:在需要创建一个切片,并立即对其元素进行填充或操作时,make 是首选。例如,创建一个用于存储计算结果的切片,然后通过循环计算并填充切片。
package main

import "fmt"

func calculateSquares(n int) []int {
    squares := make([]int, n)
    for i := 0; i < n; i++ {
        squares[i] = i * i
    }
    return squares
}

func main() {
    result := calculateSquares(5)
    fmt.Println(result)
}
  1. 创建映射并存储键值对:当需要创建一个映射,并向其中添加键值对时,make 是必不可少的。例如,创建一个用于统计单词出现次数的映射。
package main

import "fmt"

func countWords(words []string) map[string]int {
    wordCount := make(map[string]int)
    for _, word := range words {
        wordCount[word]++
    }
    return wordCount
}

func main() {
    words := []string{"apple", "banana", "apple", "cherry"}
    result := countWords(words)
    fmt.Println(result)
}
  1. 创建通道并进行数据通信:在并发编程中,当需要创建一个通道用于 goroutine 之间的数据通信时,使用 make。例如,创建一个无缓冲通道,用于两个 goroutine 之间传递数据。
package main

import (
    "fmt"
)

func sender(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func receiver(ch chan int) {
    for num := range ch {
        fmt.Println("Received:", num)
    }
}

func main() {
    ch := make(chan int)
    go sender(ch)
    receiver(ch)
}

容易混淆的情况及避免方法

  1. 对引用类型使用 new:如前面提到的,对切片、映射和通道使用 new 可能会导致意外的结果,因为得到的是指向零值的指针,而不是可直接使用的对象。为避免这种情况,应始终使用 make 来创建切片、映射和通道。
  2. 对基本类型使用 make:试图对基本类型(如 intfloatbool 等)使用 make 会导致编译错误。在这种情况下,应使用 new 或直接声明并初始化变量。
  3. 初始化逻辑混淆:要清楚 new 只是分配内存并零值初始化,而 make 除了分配内存还会进行特定类型的初始化。在编写代码时,根据实际需求选择正确的函数,确保对象得到正确的初始化。

通过理解这些容易混淆的情况,并在编写代码时仔细检查和选择合适的内存分配与初始化方式,可以避免许多潜在的错误和问题。

总结两者的区别与联系

综上所述,makenew 在 Go 语言中虽然都与内存分配相关,但有着显著的区别。new 适用于所有类型,主要负责内存的分配并零值初始化,返回指针;而 make 仅适用于切片、映射和通道这三种引用类型,不仅分配内存还进行特定类型的初始化,返回类型本身。

在实际编程中,根据不同的数据类型和需求,合理选择 makenew 是编写高效、正确的 Go 代码的关键。理解它们的区别,有助于开发者更好地控制内存使用,避免常见的编程错误,提高程序的性能和稳定性。无论是简单的变量创建,还是复杂的数据结构初始化,准确运用这两个关键字都能使代码更加简洁和可读。