Skip to content

2.10 关键字:make 和 new 的区别?

newmake 都是 Go 语言提供的内置函数,用于内存分配。它们是初学者最容易混淆的两个函数,但实际上它们的用途和行为有着本质的区别。

本节将详细讲解这两个函数的用法、区别以及最佳实践。

一、为什么需要 new 和 make?

在 Go 语言中,变量在使用之前必须先分配内存。对于简单类型(如 intstring),编译器会自动完成内存分配;但对于某些复杂类型,我们需要显式地进行内存分配和初始化。

go
// 简单类型:自动分配内存
var num int = 10

// 复杂类型:需要手动分配内存
var slice []int    // 此时 slice 是 nil,无法直接使用
// slice[0] = 1    // 会报错:panic: runtime error: index out of range

这就是 newmake 存在的意义:帮助我们分配和初始化内存

二、new 函数详解

1. 函数签名

go
// new 内置函数会分配内存。
// 第一个参数是类型(不是值)
// 返回值是指向该类型新分配的零值的指针
func new(Type) *Type

2. new 函数的特点

new 函数有以下三个特点:

  1. 分配内存:为指定类型分配一块内存空间
  2. 设置零值:将分配的内存初始化为该类型的零值
  3. 返回指针:返回指向该内存的指针

3. 适用范围

new 可以用于任何类型

  • 基本类型:intfloat64boolstring
  • 复合类型:structarray
  • 也可以用于 slicemapchan,但不推荐(后面会说明原因)

4. 使用示例

(1) 为基本类型分配内存

go
package main

import "fmt"

func main() {
    // 使用 new 为 int 类型分配内存
    num := new(int)
    
    fmt.Printf("类型: %T\n", num)    // 输出: 类型: *int (指针类型)
    fmt.Printf("值: %v\n", *num)     // 输出: 值: 0 (零值)
    fmt.Printf("地址: %p\n", num)    // 输出: 地址: 0xc000012028
    
    // 修改值
    *num = 100
    fmt.Printf("新值: %v\n", *num)   // 输出: 新值: 100
}

(2) 为结构体分配内存

go
package main

import "fmt"

type Student struct {
    Name string
    Age  int
}

func main() {
    // 使用 new 为结构体分配内存
    s := new(Student)
    
    fmt.Printf("类型: %T\n", s)                    // 输出: 类型: *Student
    fmt.Printf("零值: %+v\n", *s)                  // 输出: 零值: {Name: Age:0}
    
    // 可以直接通过指针访问字段(Go 的语法糖)
    s.Name = "Alice"
    s.Age = 20
    fmt.Printf("赋值后: %+v\n", *s)               // 输出: 赋值后: {Name:Alice Age:20}
}

(3) new 的等价写法

在实际开发中,new 并不常用,因为有更简洁的写法:

go
// 使用 new
p1 := new(int)
*p1 = 10

// 等价写法 1:使用 & 取地址
var v int
p2 := &v
*p2 = 10

// 等价写法 2:直接使用复合字面量
p3 := &Student{Name: "Bob", Age: 25}

三、make 函数详解

1. 函数签名

go
// make 内置函数用于分配并初始化 slice、map 或 chan 类型的对象
// 和 new 一样,第一个参数是类型(不是值)
// 但 make 返回的是类型本身(不是指针)
func make(t Type, size ...IntegerType) Type

2. make 函数的特点

make 函数有以下四个特点:

  1. 仅用于三种类型:只能为 slicemapchan 分配内存
  2. 分配并初始化:不仅分配内存,还会进行初始化(不是零值)
  3. 返回类型本身:返回的是类型本身,而不是指针
  4. 可传入参数:可以指定长度、容量等参数

3. 为什么只能用于这三种类型?

因为 slicemapchan引用类型,它们的底层数据结构比较复杂:

  • slice:底层包含指向数组的指针、长度和容量
  • map:底层是哈希表结构
  • chan:底层是带缓冲区的队列结构

这些类型不仅需要分配内存,还需要初始化内部结构,简单的零值无法让它们正常工作。

4. 使用示例

(1) 使用 make 创建 slice

go
package main

import "fmt"

func main() {
    // 创建一个长度为 3、容量为 5 的 int 切片
    s1 := make([]int, 3, 5)
    fmt.Printf("类型: %T\n", s1)           // 输出: 类型: []int (不是指针)
    fmt.Printf("长度: %d, 容量: %d\n", len(s1), cap(s1))  // 输出: 长度: 3, 容量: 5
    fmt.Printf("值: %v\n", s1)             // 输出: 值: [0 0 0] (零值元素)
    
    // 可以直接使用
    s1[0] = 10
    s1[1] = 20
    fmt.Printf("赋值后: %v\n", s1)         // 输出: 赋值后: [10 20 0]
    
    // 只指定长度(容量等于长度)
    s2 := make([]string, 2)
    fmt.Printf("s2: %v, 长度: %d, 容量: %d\n", s2, len(s2), cap(s2))
    // 输出: s2: [ ], 长度: 2, 容量: 2
}

(2) 使用 make 创建 map

go
package main

import "fmt"

func main() {
    // 创建一个 map
    m := make(map[string]int)
    
    fmt.Printf("类型: %T\n", m)           // 输出: 类型: map[string]int
    fmt.Printf("值: %v\n", m)             // 输出: 值: map[]
    fmt.Printf("长度: %d\n", len(m))      // 输出: 长度: 0
    
    // 可以直接使用
    m["Alice"] = 90
    m["Bob"] = 85
    fmt.Printf("赋值后: %v\n", m)         // 输出: 赋值后: map[Alice:90 Bob:85]
    
    // 也可以指定初始容量(可选,用于性能优化)
    m2 := make(map[string]int, 100)
    fmt.Printf("m2: %v\n", m2)           // 输出: m2: map[]
}

(3) 使用 make 创建 channel

go
package main

import "fmt"

func main() {
    // 创建一个无缓冲 channel
    ch1 := make(chan int)
    fmt.Printf("类型: %T\n", ch1)         // 输出: 类型: chan int
    
    // 创建一个有缓冲 channel(容量为 10)
    ch2 := make(chan string, 10)
    fmt.Printf("容量: %d\n", cap(ch2))    // 输出: 容量: 10
    
    // 可以直接使用
    ch2 <- "hello"
    ch2 <- "world"
    fmt.Println(<-ch2)                   // 输出: hello
}

(4) 对比:不使用 make 的后果

go
package main

func main() {
    // 错误示例:不使用 make 初始化
    var s []int      // s 是 nil
    // s[0] = 1      // panic: runtime error: index out of range
    
    var m map[string]int  // m 是 nil
    // m["key"] = 1  // panic: assignment to entry in nil map
    
    // 正确示例:使用 make 初始化
    s = make([]int, 1)
    s[0] = 1  // ✓ 正常工作
    
    m = make(map[string]int)
    m["key"] = 1  // ✓ 正常工作
}

四、new 和 make 的区别对比

特性newmake
适用类型任何类型只能用于 slicemapchan
返回值指针 (*Type)类型本身 (Type)
初始化方式设置为零值进行结构初始化
参数只接受类型可以接受额外的长度、容量参数
使用频率较少使用非常常用(引用类型必须)
内存分配分配内存分配内存 + 初始化内部结构

对比示例

go
package main

import "fmt"

func main() {
    // ========== new 的行为 ==========
    
    // new 一个 slice(不推荐)
    s1 := new([]int)
    fmt.Printf("new slice: 类型=%T, 值=%v, 是否nil=%v\n", s1, *s1, *s1 == nil)
    // 输出: new slice: 类型=*[]int, 值=[], 是否nil=true
    // 注意:s1 是指针,*s1 是 nil slice,无法直接使用
    
    // make 一个 slice(推荐)
    s2 := make([]int, 0)
    fmt.Printf("make slice: 类型=%T, 值=%v, 是否nil=%v\n", s2, s2, s2 == nil)
    // 输出: make slice: 类型=[]int, 值=[], 是否nil=false
    // 注意:s2 不是指针,是已初始化的 slice,可以直接使用
    
    // ========== new 的行为 ==========
    
    // new 一个 map(不推荐)
    m1 := new(map[string]int)
    fmt.Printf("new map: 类型=%T, 值=%v, 是否nil=%v\n", m1, *m1, *m1 == nil)
    // 输出: new map: 类型=*map[string]int, 值=map[], 是否nil=true
    // (*m1)["key"] = 1  // panic: 无法使用
    
    // make 一个 map(推荐)
    m2 := make(map[string]int)
    fmt.Printf("make map: 类型=%T, 值=%v, 是否nil=%v\n", m2, m2, m2 == nil)
    // 输出: make map: 类型=map[string]int, 值=map[], 是否nil=false
    m2["key"] = 1  // ✓ 可以直接使用
}

五、常见使用场景

1. 使用 new 的场景

在实际开发中,new 的使用场景较少,以下是一些可能用到的情况:

go
// 场景 1:需要获取基本类型的指针
func example1() *int {
    return new(int)  // 返回指向零值的指针
}

// 场景 2:需要明确表示"分配内存"的语义
type Config struct {
    Host string
    Port int
}

func NewConfig() *Config {
    return new(Config)  // 虽然可以用 &Config{},但 new 更直观
}

2. 使用 make 的场景(必须)

对于 slicemapchan必须使用 make 或字面量初始化才能使用:

go
// ✓ 正确:使用 make
s := make([]int, 0, 10)
m := make(map[string]int)
ch := make(chan int, 5)

// ✓ 正确:使用字面量
s := []int{1, 2, 3}
m := map[string]int{"a": 1}

// ✗ 错误:声明但不初始化
var s []int    // s 是 nil,无法使用
var m map[string]int  // m 是 nil,无法使用

六、常见误区与最佳实践

1. 误区:对 slice、map、chan 使用 new

go
// ✗ 错误示例:使用 new 创建 slice
s := new([]int)
// *s 是 nil,无法使用
// (*s)[0] = 1  // panic

// ✓ 正确做法:使用 make
s := make([]int, 0)

2. 误区:认为 make 返回指针

go
// make 返回的是类型本身,不是指针
s := make([]int, 3)
fmt.Printf("%T\n", s)  // 输出: []int (不是 *[]int)

// 如果需要指针,需要手动取地址
ps := &s
fmt.Printf("%T\n", ps)  // 输出: *[]int

3. 最佳实践

类型推荐方式原因
基本类型直接声明或字面量var n intn := 10
结构体字面量 + 取地址p := &Person{Name: "Alice"}
slicemake 或字面量make([]int, 0, 10)[]int{1,2,3}
mapmake 或字面量make(map[string]int)map[string]int{"a":1}
channelmakemake(chan int, 10)

七、总结

核心要点

  1. new

    • 任何类型分配内存
    • 返回指针
    • 初始化为零值
    • 实际使用较少
  2. make

    • 只能用于 slice、map、chan
    • 返回类型本身(不是指针)
    • 进行结构初始化(不是零值)
    • 地位无可替代

记忆技巧

go
// new:返回 Pointer(指针)
p := new(int)      // *int (指针)
s := new(Student)  // *Student (指针)

// make:返回 Value(值)
slice := make([]int, 0)       // []int (值)
m := make(map[string]int)     // map[string]int (值)
ch := make(chan int)          // chan int (值)

选择建议

  • 对于 slice、map、chan:必须使用 make 或字面量初始化
  • 对于其他类型:通常使用字面量或 &Type{} 的方式,很少使用 new