深入理解Go语言中的new与make:内存分配与类型初始化

深入理解Go语言中的new与make:内存分配与类型初始化

Go语言提供了new和make两种内建函数用于内存分配和初始化,它们各自服务于不同的场景。new用于为任何类型分配零值内存并返回其指针,而make则专为切片、映射和通道这三种引用类型设计,用于分配并初始化其内部数据结构,返回的是已准备好使用的类型实例本身。理解两者的区别对于编写高效且正确的Go代码至关重要。

Go语言的内存分配机制概览

go语言中,进行内存分配和值初始化有多种方式,包括:

复合字面量(Composite Literals): 如 Point{2, 3} 或 []int{1, 2, 3},通常在栈上或根据逃逸分析在堆上分配并初始化。局部变量地址: &someLocalVar,获取已声明局部变量的地址。new 函数: 通用内存分配器,返回指向零值内存的指针。make 函数: 专用于切片、映射和通道的初始化。

理解这些机制有助于我们选择最合适的内存管理方式。

new 关键字:通用内存分配

new 函数是Go语言中一个通用的内存分配器。它的主要功能是:

分配内存: 为指定类型的值分配足够的内存。零值初始化: 将分配的内存初始化为该类型的零值(例如,整型为0,布尔型为false,字符串为空字符串,指针为nil)。返回指针: 返回一个指向新分配内存的指针。

语法: new(Type)

示例:

立即学习“go语言免费学习笔记(深入)”;

package mainimport "fmt"type Point struct {    X, Y int}func main() {    // 为Point类型分配内存,并返回*Point类型的指针    p1 := new(Point)    fmt.Printf("p1 类型: %T, 值: %+vn", p1, p1) // p1 类型: *main.Point, 值: &{X:0 Y:0}    // 为int类型分配内存,并返回*int类型的指针    i1 := new(int)    fmt.Printf("i1 类型: %T, 值: %vn", i1, *i1) // i1 类型: *int, 值: 0    // 对比复合字面量:&Point{} 结合了分配和初始化    p2 := &Point{}    fmt.Printf("p2 类型: %T, 值: %+vn", p2, p2) // p2 类型: *main.Point, 值: &{X:0 Y:0}    p3 := &Point{X: 2, Y: 3}    fmt.Printf("p3 类型: %T, 值: %+vn", p3, p3) // p3 类型: *main.Point, 值: &{X:2 Y:3}    // 注意:&int 是非法的,因为int是一个值类型,不能直接取其类型地址。    // 但 new(int) 是合法的,它分配了一个int的内存并返回其指针。    // var i int    // i4 := &i // 合法,获取已存在变量i的地址}

从示例中可以看出,new(Point) 和 &Point{} 都能得到 *Point 类型的值,但后者允许在分配的同时进行字段初始化。对于基本类型如 int,new(int) 是分配零值 int 并返回其指针的常用方式,因为直接 &int 是语法错误的。

make 关键字:引用类型的专属初始化

make 函数与 new 不同,它不是通用的内存分配器。make 专用于分配并初始化三种内建的引用类型:切片(slice)映射(map)通道(channel)。这三种类型在Go语言中是特殊的存在,它们不仅仅是内存块,还需要内部数据结构(如切片头、哈希表、缓冲区等)进行初始化才能正常使用。

语法:

切片: make([]Type, length, capacity) 或 make([]Type, length)映射: make(map[KeyType]ValueType, initialCapacity) 或 make(map[KeyType]ValueType)通道: make(chan Type, bufferCapacity) 或 make(chan Type)

make 函数会返回一个已初始化且可用的类型实例本身,而不是指针。

示例:

立即学习“go语言免费学习笔记(深入)”;

package mainimport "fmt"func main() {    // 切片:分配一个长度为5,容量为10的int切片    s := make([]int, 5, 10)    fmt.Printf("s 类型: %T, 值: %v, 长度: %d, 容量: %dn", s, s, len(s), cap(s))    // s 类型: []int, 值: [0 0 0 0 0], 长度: 5, 容量: 10    // 映射:分配一个string到int的映射,并初始化其内部哈希表    m := make(map[string]int)    fmt.Printf("m 类型: %T, 值: %vn", m, m)    // m 类型: map[string]int, 值: map[]    m["key"] = 10    fmt.Println("m['key']:", m["key"])    // 通道:分配一个int类型的通道,带有一个缓冲区    c := make(chan int, 1)    fmt.Printf("c 类型: %T, 值: %vn", c, c)    // c 类型: chan int, 值: 0xc000060060 (通道的内部表示)    c <- 1    val := <-c    fmt.Println("从通道接收到的值:", val)    // 注意:make(Point) 或 make(int) 是非法的,因为make不能用于非引用类型    // make(Point) // 编译错误    // make(int)   // 编译错误}

可以看到,make 返回的是 []int、map[string]int、chan int 这些类型本身,而不是它们的指针。这是因为这些类型在使用前需要进行特定的初始化步骤,而make正是负责完成这些步骤。

new 与 make 的关键区别

理解 new 和 make 的核心差异是掌握Go内存管理的关键:

返回类型:

new(T) 返回 *T,即一个指向零值 T 的指针。make(T, args) 返回 T,即一个已初始化且可用的 T 类型实例。

p := new(chan int)   // p 的类型是 *chan intc := make(chan int)  // c 的类型是 chan int

这里 p 是一个指向 nil 通道的指针,需要进一步解引用并赋值一个 make 创建的通道才能使用。而 c 已经是可以直接使用的通道。

适用类型:

new 可以用于任何类型(包括结构体、基本类型、切片、映射、通道等),它只是分配内存并零值化。make 只能用于切片 ([]T)、映射 (map[K]V) 和通道 (chan T) 这三种引用类型。

初始化行为:

new 只是将内存清零,使其达到该类型的零值状态。对于引用类型,如 new([]int),它会返回一个指向 nil 切片头的指针。这个切片仍然是 nil,不能直接使用(例如,不能 append)。make 不仅分配内存,还会初始化这些引用类型的内部数据结构,使它们处于可用状态。例如,make([]int, 0, 5) 会创建一个长度为0,容量为5的切片头,并指向一个底层的数组。

设计考量:为何需要两个函数?

Go语言的设计者选择保留 new 和 make 两个独立的函数,而非合并为一个,主要是出于清晰性避免混淆的考虑。

试想如果只有一个名为 NEW 的函数:

NEW(*int) 对应 new(int)NEW(*Point) 对应 new(Point)NEW(*chan int) 对应 new(chan int) (返回 *chan int)NEW(chan int) 对应 make(chan int) (返回 chan int)NEW([]int, 10) 对应 make([]int, 10)

这种统一的 NEW 函数在处理引用类型时,需要根据参数是否带有 * 来区分是分配指针还是初始化实例,这无疑会增加学习曲线和使用时的心智负担。例如,NEW(chan int) 返回 chan int,而 NEW(*chan int) 返回 *chan int,这种细微的语法差异可能会导致开发者混淆。

通过将功能明确地划分为 new(通用零值内存分配,返回指针)和 make(引用类型初始化,返回实例),Go语言使得这两种操作的目的和结果更加直观,降低了新Go程序员的理解难度。

总结与实践建议

使用 new:

当你需要为任何类型分配内存并获取一个指向其零值的指针时。当你想明确地表示你只是在分配内存,而初始化将在后续步骤中进行时。例如:ptr := new(MyStruct),counter := new(int)。

使用 make:

当你需要创建和初始化切片、映射或通道时。这些类型在创建时需要特定的内部结构设置,make 确保它们在返回时是完全可用的。例如:s := make([]int, 10), m := make(map[string]string), ch := make(chan int, 5)。

结构体初始化: 对于结构体,通常推荐使用复合字面量 &MyStruct{Field: value} 的形式,它结合了分配和初始化,代码更简洁易读。

理解 new 和 make 的区别,并根据具体场景选择合适的函数,是编写高效、健壮Go程序的关键一步。

以上就是深入理解Go语言中的new与make:内存分配与类型初始化的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1400658.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月15日 17:11:36
下一篇 2025年12月15日 17:11:47

相关推荐

  • Go WebSockets 长连接管理:解决 EOF 错误与实现持久化通信

    本文旨在解决Go语言WebSocket连接在首次请求后出现EOF错误并导致连接中断的问题。通过详细阐述WebSocket持久化连接的核心原理,即在独立的Goroutine中维护持续的读写循环,确保连接的生命周期与应用需求一致,从而实现稳定的双向通信,避免频繁重连。 理解WebSocket连接的生命周…

    2025年12月15日
    000
  • Go语言内存分配与初始化:深入解析new()、make()及复合字面量

    本文深入探讨Go语言中内存分配与初始化的多种机制,包括new()、make()、复合字面量&T{}以及取址操作&localVar。文章将详细阐述new()和make()各自的独特用途、返回类型差异,并解释为何Go语言设计者选择保留这两个独立的内置函数,旨在帮助开发者清晰理解并正确选择…

    2025年12月15日
    000
  • Go 语言内存分配:new 与 make 的选择

    Go 语言提供了多种内存分配和值初始化的方式,包括 &T{…}、&someLocalVar、new 和 make。此外,创建复合字面量时也会发生内存分配。理解 new 和 make 的区别对于编写高效的 Go 代码至关重要。 正如上述摘要所概括的,new 和 make 是…

    2025年12月15日
    000
  • Go语言内存分配:深入解析new与make的异同与应用场景

    在Go语言中,new和make是两种核心的内存分配与初始化机制。new用于为任意类型分配零值内存并返回其指针,而make则专为切片、映射和通道这三种引用类型设计,用于分配并初始化其内部数据结构,返回的是已初始化的值而非指针。理解两者的差异及其适用场景,对于编写高效且符合Go惯例的代码至关重要。 Go…

    2025年12月15日
    000
  • Go语言运行时内省:获取调用方包名与函数信息

    本文探讨在Go语言中如何通过运行时(runtime)机制,程序化地获取调用方(caller)的包名、函数名及其源文件位置。我们将重点介绍runtime.Caller和runtime.FuncForPC这两个核心函数,并提供示例代码,帮助开发者在构建如日志、配置管理等库时,实现基于调用上下文的灵活功能…

    2025年12月15日
    000
  • 使用部分字符串在 Go GAE Datastore 中搜索条目

    本文介绍了如何在 Google App Engine (GAE) 的 Datastore 中使用 Go 语言进行部分字符串匹配查询。由于 Datastore 本身不支持 LIKE 操作,我们将利用其提供的范围查询功能(> 和 在 Google App Engine (GAE) 的 Datast…

    2025年12月15日
    000
  • Go语言运行时自省:获取调用者包名与函数信息

    本文深入探讨了Go语言中通过runtime.Caller和runtime.FuncForPC进行运行时自省,以程序化方式获取调用者包名、文件路径、行号及函数名称的方法。文章提供了详细的代码示例,并分析了不同调用场景下的输出结果。同时,着重阐述了这些API在实际使用中可能遇到的局限性,如编译器内联的影…

    2025年12月15日
    000
  • 获取 Go 程序入口包名:使用 runtime 包进行自省

    在 Go 语言中,有时我们需要在运行时获取关于当前程序的一些信息,例如当前执行函数的包名。这在编写通用库或框架时尤其有用,可以根据调用者的上下文来执行不同的操作。runtime 包提供了一些函数,可以帮助我们实现这个目标。 runtime.Caller 函数可以获取调用栈的信息,包括程序计数器(PC…

    2025年12月15日
    000
  • 获取 Go 程序主包名:运行时自省技巧

    在 Go 语言中,有时我们需要在运行时获取关于当前代码执行环境的信息,例如调用者的包名、函数名等。这种自省能力在编写通用库或框架时尤其有用,它可以帮助我们根据调用者的上下文做出不同的处理。虽然 Go 语言不像 Python 那样拥有强大的 inspect 模块,但 runtime 包提供了一些函数,…

    2025年12月15日
    000
  • 获取 Go 程序主包名的方法

    在 Go 语言中,有时我们需要在运行时获取关于调用者的信息,例如调用者的文件名、行号以及函数名。这在编写库或者框架时尤为有用,可以帮助我们实现一些高级功能,比如自动化的配置加载、日志记录等。本文将介绍如何利用 runtime 包中的 runtime.Caller 和 runtime.FuncForP…

    2025年12月15日
    000
  • Go语言集成SQLite3数据库:使用go-sqlite3库的实践指南

    本文旨在为Go语言开发者提供一套完整的SQLite3数据库集成指南。我们将重点介绍如何使用广受欢迎的github.com/mattn/go-sqlite3库,涵盖其安装、数据库连接、表创建、数据插入、查询、更新及删除等核心操作,并提供实用的代码示例和注意事项,助您高效地在Go应用中实现SQLite3…

    2025年12月15日
    000
  • Go 语言中使用 SQLite3:库选择与实践指南

    本文旨在为 Go 语言开发者提供一份关于 SQLite3 数据库连接与操作的实用指南。我们将介绍如何选择合适的 SQLite3 驱动库,并提供简单的 INSERT 和 SELECT 操作示例代码,帮助你快速上手在 Go 项目中使用 SQLite3。 选择合适的 SQLite3 驱动库 在 Go 语言…

    2025年12月15日
    000
  • Golang指针与CGO交互 混合编程中的应用

    掌握指针转换、内存生命周期和结构体对齐是Go与C混合编程的核心,通过CGO实现高效数据交互,需注意内存归属,避免跨语言GC问题。 在Go语言与C语言混合编程中,指针和CGO是实现高效数据交互的核心机制。Go通过CGO调用C代码时,由于两种语言内存模型和类型系统的差异,正确使用指针转换和内存管理尤为关…

    2025年12月15日
    000
  • 将二进制字符串转换为整数:Go语言高效指南

    本文将介绍如何使用Go语言将表示二进制数的字符串转换为整数。正如摘要所述,我们将使用Go标准库strconv中的ParseInt函数,避免手动编写转换逻辑。 使用 strconv.ParseInt 函数 strconv.ParseInt 函数可以将给定基数的字符串转换为指定位数的整数。其函数签名如下…

    2025年12月15日
    000
  • Go语言包的可见性:子包与根包的成员访问

    Go语言的包管理机制与传统的面向对象编程语言存在显著差异。虽然目录结构上存在类似父子关系的组织形式,例如foo和foo/utils,但在Go语言中,它们被视为完全独立的包。这意味着,foo/utils并非foo的子包,它们之间的关系仅仅体现在导入路径上。 包的独立性 在Go语言中,每个目录对应一个独…

    2025年12月15日
    000
  • Go语言包可见性深度解析:理解“子包”的真相

    Go语言中,包的可见性规则严格遵循首字母大小写,而非文件系统路径层级。一个包(如foo)无法访问其子目录中其他包(如foo/utils)的私有成员。foo/utils仅是导入路径,不代表继承或特殊访问权限,所有包都是独立的可见性单元。深入理解这一机制对于编写清晰、可维护的Go代码至关重要。 Go语言…

    2025年12月15日
    000
  • Go语言单例结构体的简写技巧与替代方案

    在Go语言中创建单例结构体时,常常会遇到需要重复编写字段签名的情况。例如,以下代码: foo := struct{ bar func(string, int, bool) error}{ bar: func(a string, b int, c bool) error { // …}} 这段代码…

    2025年12月15日
    000
  • Go语言匿名结构体中函数字段定义的优化与替代方案

    本文探讨了Go语言中为匿名结构体定义单一函数字段时,函数签名需要重复声明的问题。虽然没有语法糖能直接缩短这种双重声明,但若该结构体仅用于封装一个函数,更简洁的实践是直接将函数赋值给变量,从而避免不必要的结构体定义,提升代码的清晰度与简洁性。 匿名结构体中函数字段的声明冗余问题 在go语言中,有时我们…

    2025年12月15日
    000
  • Go语言中单字段匿名结构体函数签名的优化与替代策略

    本文探讨Go语言中定义含单个函数字段的匿名结构体时,函数签名重复的问题。我们将分析这种重复的必然性,并提供一种更简洁的替代方案:当匿名结构体仅作为单个函数的包装时,可直接使用函数字面量,从而避免不必要的结构体定义和签名冗余,简化代码结构。 Go语言中单字段匿名结构体与函数签名重复问题 在go语言中,…

    2025年12月15日
    000
  • 深入理解Go语言包可见性:无“子包”概念与模块化设计

    Go语言中,包的可见性遵循严格的规则,不存在传统意义上的“子包”概念。每个目录对应一个独立的包,包之间通过导入路径关联,但彼此的私有成员是不可见的。即使在文件系统上存在层级关系,如foo和foo/utils,foo包也无法直接访问foo/utils包的私有成员。理解这一机制对于构建清晰、可维护的Go…

    2025年12月15日
    000

发表回复

登录后才能评论
关注微信