2.10 关键字:make 和 new 的区别?
new 和 make 都是 Go 语言提供的内置函数,用于内存分配。它们是初学者最容易混淆的两个函数,但实际上它们的用途和行为有着本质的区别。
本节将详细讲解这两个函数的用法、区别以及最佳实践。
一、为什么需要 new 和 make?
在 Go 语言中,变量在使用之前必须先分配内存。对于简单类型(如 int、string),编译器会自动完成内存分配;但对于某些复杂类型,我们需要显式地进行内存分配和初始化。
go
// 简单类型:自动分配内存
var num int = 10
// 复杂类型:需要手动分配内存
var slice []int // 此时 slice 是 nil,无法直接使用
// slice[0] = 1 // 会报错:panic: runtime error: index out of range这就是 new 和 make 存在的意义:帮助我们分配和初始化内存。
二、new 函数详解
1. 函数签名
go
// new 内置函数会分配内存。
// 第一个参数是类型(不是值)
// 返回值是指向该类型新分配的零值的指针
func new(Type) *Type2. new 函数的特点
new 函数有以下三个特点:
- 分配内存:为指定类型分配一块内存空间
- 设置零值:将分配的内存初始化为该类型的零值
- 返回指针:返回指向该内存的指针
3. 适用范围
new 可以用于任何类型:
- 基本类型:
int、float64、bool、string等 - 复合类型:
struct、array等 - 也可以用于
slice、map、chan,但不推荐(后面会说明原因)
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) Type2. make 函数的特点
make 函数有以下四个特点:
- 仅用于三种类型:只能为
slice、map、chan分配内存 - 分配并初始化:不仅分配内存,还会进行初始化(不是零值)
- 返回类型本身:返回的是类型本身,而不是指针
- 可传入参数:可以指定长度、容量等参数
3. 为什么只能用于这三种类型?
因为 slice、map、chan 是引用类型,它们的底层数据结构比较复杂:
- 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 的区别对比
| 特性 | new | make |
|---|---|---|
| 适用类型 | 任何类型 | 只能用于 slice、map、chan |
| 返回值 | 指针 (*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 的场景(必须)
对于 slice、map、chan,必须使用 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) // 输出: *[]int3. 最佳实践
| 类型 | 推荐方式 | 原因 |
|---|---|---|
| 基本类型 | 直接声明或字面量 | var n int 或 n := 10 |
| 结构体 | 字面量 + 取地址 | p := &Person{Name: "Alice"} |
| slice | make 或字面量 | make([]int, 0, 10) 或 []int{1,2,3} |
| map | make 或字面量 | make(map[string]int) 或 map[string]int{"a":1} |
| channel | make | make(chan int, 10) |
七、总结
核心要点
new:
- 为任何类型分配内存
- 返回指针
- 初始化为零值
- 实际使用较少
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