Go 语言中的 make 与 new 使用区别与技巧

337 阅读5分钟

Go 语言中的 make 与 new 使用区别与技巧

Go 语言的内存分配有两种常用方式:make 和 new。虽然它们都用于分配内存,但它们的作用和使用场景有所不同。理解这两者的区别对于编写高效和可维护的 Go 代码至关重要。本文将详细解析 make 和 new 的区别、适用场景,并提供一些使用技巧。

1. make 和 new 的基本区别

1.1 new:创建指针类型的零值

new 是 Go 中用于分配内存的关键字,它的作用是为一个类型分配一块内存,并返回一个指向这块内存的指针。使用 new 分配的内存被初始化为类型的零值。

示例:使用 new
package main

import "fmt"

func main() {
    var p *int = new(int)
    fmt.Println(*p) // 输出: 0,`new` 为 int 分配的内存被初始化为零值
}
  • 返回类型new 返回的是指向类型的指针。
  • 零值初始化new 会将分配的内存初始化为类型的零值。例如,对于 int 类型,零值是 0;对于 string 类型,零值是空字符串 ""

1.2 make:初始化切片、映射和通道

make 是 Go 特有的一个内建函数,专门用于初始化三种内建数据类型:切片(slice)、映射(map)和通道(channel)。与 new 不同,make 不返回一个指向内存的指针,而是返回初始化后的对象本身。

示例:使用 make
package main

import "fmt"

func main() {
    // 初始化切片
    s := make([]int, 5)
    fmt.Println(s) // 输出: [0 0 0 0 0]
    
    // 初始化映射
    m := make(map[string]int)
    m["age"] = 30
    fmt.Println(m) // 输出: map[age:30]

    // 初始化通道
    ch := make(chan int, 2)
    ch <- 1
    fmt.Println(<-ch) // 输出: 1
}
  • 返回类型make 返回的是切片、映射或通道类型的对象,而不是指针。
  • 内存分配与初始化make 不仅分配内存,还会初始化数据结构本身。例如,在初始化切片时,make 会为切片分配底层数组并设置其长度和容量。

1.3 主要区别总结

特性newmake
用途分配内存并返回指向类型的指针初始化并返回切片、映射、通道对象
返回值返回类型的指针返回初始化后的对象本身
适用类型所有类型切片、映射、通道
初始化返回零值根据数据结构的类型进行初始化

2. make 和 new 的使用技巧

2.1 new 的使用技巧

  • 适用于结构体类型new 常用于为结构体分配内存并返回指针。需要注意的是,结构体指针的初始化值是结构体的零值。
示例:使用 new 为结构体分配内存
type Person struct {
    Name string
    Age  int
}

func main() {
    p := new(Person)
    fmt.Println(p)      // 输出: &{ 0}
    fmt.Println(p.Name) // 输出: 空字符串
    fmt.Println(p.Age)  // 输出: 0
}
  • 通过 new 创建的指针:由于 new 返回的是指向结构体的指针,可以直接使用 p.Name 或 p.Age 修改其字段值。

2.2 make 的使用技巧

  • 初始化带容量的切片make 可用于初始化一个具有指定长度和容量的切片。通过 make,可以高效地分配底层数组并初始化切片。
示例:使用 make 初始化带容量的切片
// 初始化一个长度为 5,容量为 10 的切片
s := make([]int, 5, 10)
fmt.Println(len(s), cap(s)) // 输出: 5 10
  • 初始化映射时指定容量:在使用 make 创建映射时,可以指定初始的容量,这有助于优化性能,避免在插入元素时进行多次内存扩展。
示例:使用 make 初始化映射
m := make(map[string]int, 10) // 指定容量为 10
m["age"] = 30
m["height"] = 175
fmt.Println(m) // 输出: map[age:30 height:175]
  • 初始化带缓冲区的通道:使用 make 创建带缓冲区的通道,指定通道的缓冲区大小,这在并发程序中非常有用。
示例:使用 make 创建带缓冲区的通道
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch) // 输出: 1

2.3 选择合适的内存分配方式

  • 结构体的使用场景:如果你只是需要一个结构体的指针,并且初始化时没有特别的需求,使用 new 是一种简单且常见的方式。
  • 切片、映射和通道的使用场景:如果你需要初始化一个切片、映射或通道并且可能会修改它们的内容,make 是更合适的选择,特别是在你需要提前指定容量时。

3. make 和 new 的性能考虑

  • 内存分配开销make 在初始化切片、映射和通道时不仅分配内存,还执行类型初始化,这可能会带来额外的开销。而 new 仅分配内存并初始化为零值,因此其开销较小。
  • 避免不必要的内存分配:对于切片、映射或通道等类型,尽量使用 make 时指定合适的容量,以减少内存重分配的次数。

4. 常见的错误用法

  • 误用 new 创建切片或映射new 用于切片、映射和通道时,只会返回其类型的零值,而不进行初始化,因此如果使用 new 创建切片、映射或通道并尝试直接访问其内容,会导致运行时错误。
错误示例:误用 new 创建映射
m := new(map[string]int) // 错误:返回的是一个指针而不是已初始化的映射
m["age"] = 30 // 运行时错误:m 是 nil
  • 正确示例:应该使用 make 来初始化映射。
m := make(map[string]int)
m["age"] = 30

5. 总结

Go 语言中的 make 和 new 作为内存分配的关键字,虽然功能相似,但有着明确的区别。new 用于为类型分配内存并返回指针,适用于大多数类型;而 make 主要用于初始化切片、映射和通道,提供了更强的初始化功能。

  • new:适用于创建结构体类型和其他基础类型的指针,并将内存初始化为零值。
  • make:用于初始化切片、映射和通道,支持指定容量并完成内部初始化。

理解这两者的不同使用场景和性能影响,有助于编写更高效和可维护的 Go 代码。