当前位置: 首页 > news >正文

【Go语言-Day 10】深入指针应用:解锁函数“引用传递”与内存分配的秘密

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

Go语言系列文章目录

01-【Go语言-Day 1】扬帆起航:从零到一,精通 Go 语言环境搭建与首个程序
02-【Go语言-Day 2】代码的基石:深入解析Go变量(var, :=)与常量(const, iota)
03-【Go语言-Day 3】从零掌握 Go 基本数据类型:string, runestrconv 的实战技巧
04-【Go语言-Day 4】掌握标准 I/O:fmt 包 Print, Scan, Printf 核心用法详解
05-【Go语言-Day 5】掌握Go的运算脉络:算术、逻辑到位的全方位指南
06-【Go语言-Day 6】掌控代码流:if-else 条件判断的四种核心用法
07-【Go语言-Day 7】循环控制全解析:从 for 基础到 for-range 遍历与高级控制
08-【Go语言-Day 8】告别冗长if-else:深入解析 switch-case 的优雅之道
09-【Go语言-Day 9】指针基础:深入理解内存地址与值传递
10-【Go语言-Day 10】深入指针应用:解锁函数“引用传递”与内存分配的秘密


文章目录

  • Langchain系列文章目录
  • Python系列文章目录
  • PyTorch系列文章目录
  • 机器学习系列文章目录
  • 深度学习系列文章目录
  • Java系列文章目录
  • JavaScript系列文章目录
  • Python系列文章目录
  • Go语言系列文章目录
  • 前言
  • 一、指针与函数:解锁“引用传递”的魔力
    • 1.1 值传递 vs 指针传递
      • 1.1.1 值传递(无法修改外部变量)
      • 1.1.2 指针传递(成功修改外部变量)
    • 1.2 使用指针作为函数参数的优势
  • 二、内存分配双雄:`new()` 与 `make()` 的终极对决
    • 2.1 `new(T)`:只分配内存,返回指针
      • 2.1.1 `new` 的使用示例
    • 2.2 `make(T, ...)`:为内建引用类型分配并初始化
      • 2.2.1 `make` 的使用示例
    • 2.3 `new` 与 `make` 的核心区别总结
  • 三、指针与复合类型:强强联合
    • 3.1 指针与数组
    • 3.2 指针与切片
        • (1) 何时需要切片的指针?
    • 3.3 指针与结构体
      • 3.3.1 为何要用结构体指针?
      • 3.3.2 结构体指针的使用
  • 四、总结


前言

在上一篇中,我们已经揭开了指针的神秘面纱,理解了其基本概念,并掌握了如何使用取地址符 & 和取值符 *。然而,指针的威力远不止于此。它真正的强大之处在于其应用场景。本篇文章将作为下篇,带你深入探索指针在 Go 语言中的三大核心应用领域:函数参数、内存分配以及与数组、切片、结构体等复合类型的结合。掌握了这些,你才算真正将指针这把“利器”收入囊中,为编写更高效、更灵活的 Go 代码打下坚实的基础。

一、指针与函数:解锁“引用传递”的魔力

在 Go 语言中,所有函数参数都是值传递 (Pass by Value)。这意味着当你将一个变量传递给函数时,函数接收到的是该变量的一个副本,函数内部对这个副本的任何修改都不会影响到原始变量。

但如果我们希望在函数内部修改外部变量的值呢?这正是指针大显身手的时刻。通过传递变量的内存地址(即指针),函数就可以根据这个地址找到原始变量,并对其进行修改。

1.1 值传递 vs 指针传递

让我们通过一个简单的例子来直观地感受两者的区别。

1.1.1 值传递(无法修改外部变量)

场景:假设我们想写一个函数,将一个整数变量的值加倍。

package mainimport "fmt"// 尝试通过值传递修改变量
func doubleValue(val int) {val = val * 2fmt.Printf("函数内部,val 的值为: %d, 地址为: %p\n", val, &val)
}func main() {num := 10fmt.Printf("调用前,num 的值为: %d, 地址为: %p\n", num, &num)doubleValue(num)fmt.Printf("调用后,num 的值为: %d, 地址为: %p\n", num, &num)
}

运行结果:

调用前,num 的值为: 10, 地址为: 0xc000018030
函数内部,val 的值为: 20, 地址为: 0xc000018038
调用后,num 的值为: 10, 地址为: 0xc000018030

分析
从输出可以看到,main 函数中的 numdoubleValue 函数中的 val 拥有不同的内存地址doubleValue 修改的只是 num 的一个副本 val,因此原始的 num 变量毫发无损。

1.1.2 指针传递(成功修改外部变量)

现在,我们将函数参数改为指针类型。

package mainimport "fmt"// 通过指针传递修改变量
func doubleValueByPtr(ptr *int) {// 通过 *ptr 取得指针指向地址的值,并将其加倍*ptr = *ptr * 2 fmt.Printf("函数内部,ptr 指向的值为: %d\n", *ptr)
}func main() {num := 10fmt.Printf("调用前,num 的值为: %d, 地址为: %p\n", num, &num)// 将 num 的内存地址 &num 传递给函数doubleValueByPtr(&num)fmt.Printf("调用后,num 的值为: %d, 地址为: %p\n", num, &num)
}

运行结果:

调用前,num 的值为: 10, 地址为: 0xc000018030
函数内部,ptr 指向的值为: 20
调用后,num 的值为: 20, 地址为: 0xc000018030

分析
这次,我们向函数传递了 num 的内存地址 &num。函数参数 ptr 接收了这个地址。在函数内部,*ptr 操作符让我们能够直接访问并修改 0xc000018030 这个地址上的数据。因此,调用结束后,main 函数中的 num 值被成功修改为 20。

1.2 使用指针作为函数参数的优势

  1. 修改外部状态:如上例所示,这是最直接的应用,允许函数对调用方的变量产生“副作用”。
  2. 提高性能:对于大型数据结构(如庞大的结构体),传递指针只需要复制一个内存地址(通常是 8 字节),而不是复制整个数据结构。这可以极大地减少内存拷贝带来的开销,提升程序性能。

二、内存分配双雄:new()make() 的终极对决

Go 语言提供了两个内置函数用于内存分配:newmake。初学者常常对它们的区别感到困惑。简而言之,它们服务于不同的目的和数据类型。

2.1 new(T):只分配内存,返回指针

new 是一个内置的泛型函数,用于分配内存。它的唯一参数是一个类型,它会为该类型的值分配一块足以容纳其所有字段的内存,并将这块内存初始化为该类型的零值。最重要的是,new(T) 返回一个指向新分配的零值的指针,即类型为 *T

2.1.1 new 的使用示例

package mainimport "fmt"func main() {// 为一个 int 类型分配内存,p1 的类型是 *int// p1 指向的内存被初始化为 int 的零值,即 0var p1 = new(int)fmt.Printf("p1 的类型: %T, p1 的值(地址): %v, p1 指向的值: %d\n", p1, p1, *p1)*p1 = 88 // 修改 p1 指向的内存中的值fmt.Printf("修改后, p1 指向的值: %d\n", *p1)// 为一个 bool 类型分配内存,p2 的类型是 *bool// p2 指向的内存被初始化为 bool 的零值,即 falsevar p2 = new(bool)fmt.Printf("p2 的类型: %T, p2 的值(地址): %v, p2 指向的值: %t\n", p2, p2, *p2)
}

运行结果:

p1 的类型: *int, p1 的值(地址): 0xc000018030, p1 指向的值: 0
修改后, p1 指向的值: 88
p2 的类型: *bool, p2 的值(地址): 0xc00000a087, p2 指向的值: false

2.2 make(T, ...):为内建引用类型分配并初始化

make 函数则专用于 slice、map 和 channel 这三种内建的引用类型的创建。make 不仅会分配内存,还会初始化这些复杂数据结构的内部组件(例如,对于 slice,它会分配底层数组并设置长度和容量)。make(T, ...) 返回的是初始化后的类型 T 的实例,而不是指针 *T

2.2.1 make 的使用示例

package mainimport "fmt"func main() {// 创建一个切片,类型 []int, 长度为 5, 容量为 10s := make([]int, 5, 10)fmt.Printf("s 的类型: %T, s 的值: %v, 长度: %d, 容量: %d\n", s, s, len(s), cap(s))// 创建一个 map,类型 map[string]intm := make(map[string]int)m["age"] = 30fmt.Printf("m 的类型: %T, m 的值: %v\n", m, m)// 创建一个 channel,类型 chan string,无缓冲ch := make(chan string)fmt.Printf("ch 的类型: %T, ch 的值(地址): %v\n", ch, ch)
}

运行结果:

s 的类型: []int, s 的值: [0 0 0 0 0], 长度: 5, 容量: 10
m 的类型: map[string]int, m 的值: map[age:30]
ch 的类型: chan string, ch 的值(地址): 0xc00007e060

2.3 newmake 的核心区别总结

特性new(T)make(T, ...)
适用类型所有类型仅用于 slice, map, channel
返回类型指针 (*T)类型本身 (T)
核心作用分配内存,并将其初始化为类型的零值分配内存,并初始化数据结构的内部状态(如 slice 的长度、容量,map 的哈希表等)。
类比好比“申请一块空地”,告诉你地在哪里(地址)。好比“建好一座房子”,可以直接入住(使用)。
示例p := new(int) 等价于 var i int; p := &is := make([]int, 5) 是创建切片的标准方式,没有简单的等价写法。

选择困难症?记住这一点:当你需要为 slice、map 或 channel 分配内存时,总是使用 make。对于其他所有类型,如果你需要一个指向其零值的指针,就使用 new

三、指针与复合类型:强强联合

指针与 Go 的复合数据类型(数组、切片、结构体)结合使用,是构建复杂数据结构和实现高效算法的关键。

3.1 指针与数组

Go 中的数组是值类型。这意味着将数组传递给函数时,会完整地复制一份数组,开销较大。

// 接收一个包含 10000 个整数的数组
// 调用时会复制 10000 * 8 = 80KB 的数据
func processArray(arr [10000]int) {// ...
}

为了避免这种开销,我们可以传递一个指向数组的指针。

package mainimport "fmt"// 接收一个指向数组的指针
func processArrayByPtr(arr *[3]int) {// 通过指针修改原始数组的元素(*arr)[0] = 100 // 注意 (*arr) 的括号是必须的,因为 . 的优先级高于 *
}func main() {a := [3]int{1, 2, 3}fmt.Println("修改前:", a)processArrayByPtr(&a)fmt.Println("修改后:", a)
}

运行结果:

修改前: [1 2 3]
修改后: [100 2 3]

注意:尽管可以这么做,但在 Go 中,我们更倾向于使用切片 (slice),因为它更灵活且本身就是引用语义。

3.2 指针与切片

切片本身就是一个包含指向底层数组的指针长度 (len)容量 (cap) 的结构体。因此,切片是引用类型。将切片传递给函数时,虽然切片头结构本身是值传递,但复制的成本极低,并且复制后的切片头仍然指向同一个底层数组。

package mainimport "fmt"func modifySlice(s []int) {// 这个修改会影响到原始切片指向的底层数组s[0] = 999
}func main() {mySlice := []int{10, 20, 30}fmt.Println("修改前:", mySlice) // 输出: 修改前: [10 20 30]modifySlice(mySlice)fmt.Println("修改后:", mySlice) // 输出: 修改后: [999 20 30]
}
(1) 何时需要切片的指针?

通常我们不需要传递切片的指针 (*[]int)。但有一个特殊情况:当你希望一个函数能够修改切片头本身(即它的长度、容量或指向的底层数组)时。最典型的例子就是 append。如果 append 操作导致底层数组重新分配内存,那么原始的切片变量需要被更新为指向新数组的新切片头。

package mainimport "fmt"// 这个函数无法改变 main 函数中 s 的长度
func wrongAppend(s []int) {s = append(s, 100) // s 在这里被重新赋值,但这个新值只在函数内部有效
}// 正确的做法是返回新的切片
func correctAppend(s []int) []int {s = append(s, 100)return s
}func main() {s1 := make([]int, 0)wrongAppend(s1)fmt.Println("wrongAppend 后 s1 的长度:", len(s1)) // 输出: 0s2 := make([]int, 0)s2 = correctAppend(s2)fmt.Println("correctAppend 后 s2 的长度:", len(s2)) // 输出: 1
}

核心要点:对于切片,直接传递切片值即可修改其元素。如果函数涉及 append 等可能改变切片头信息的操作,应该让函数返回修改后的新切片,并由调用方接收。

3.3 指针与结构体

这是指针最常见和最重要的应用场景之一。与数组类似,结构体也是值类型

3.3.1 为何要用结构体指针?

  1. 性能:避免在函数调用时复制整个结构体,特别是当结构体很大时。
  2. 可修改性:允许函数或方法修改结构体实例的状态。这是定义带有指针接收者 (Pointer Receiver) 方法的基础。

3.3.2 结构体指针的使用

package mainimport "fmt"type Employee struct {ID   intName stringAge  int
}// celebrateBirthday 接收一个 Employee 指针
// 这样可以修改原始 Employee 实例的 Age 字段
func celebrateBirthday(e *Employee) {e.Age += 1
}func main() {// 创建一个 Employee 实例emp := Employee{ID: 101, Name: "Alice", Age: 28}fmt.Printf("原始 Employee: %+v\n", emp)// 传递 emp 的地址给函数celebrateBirthday(&emp)fmt.Printf("生日后 Employee: %+v\n", emp)// Go 提供了一个语法糖:// 当我们有一个结构体指针 p 时,可以直接使用 p.Field 来访问字段,// 而不需要写成繁琐的 (*p).Field。Go 会自动为我们解引用。pEmp := &empfmt.Println("通过指针访问 Name:", pEmp.Name) // 等价于 (*pEmp).Name
}

运行结果:

原始 Employee: {ID:101 Name:Alice Age:28}
生日后 Employee: {ID:101 Name:Alice Age:30}
通过指针访问 Name: Alice

最佳实践:在实践中,当结构体较大或需要在函数/方法中修改其内容时,应优先使用结构体指针。

四、总结

恭喜你,完成了对 Go 指针高级应用的探索!现在,让我们回顾一下本篇的核心知识点:

  1. 指针与函数:Go 函数参数为值传递。通过传递指针(变量的内存地址),函数可以“间接”地修改外部变量的值,实现类似“引用传递”的效果。这对于修改状态和避免大数据拷贝至关重要。

  2. new vs make

    • new(T) 用于为任意类型分配内存,初始化为零值,并返回一个指针 (*T)
    • make(T, ...) 专用于 slice、map、channel 的创建,它不仅分配内存,还进行必要的内部结构初始化,并返回类型实例本身 (T)
  3. 指针与复合类型

    • 数组/结构体:是值类型。为了提高效率和实现函数内修改,通常传递它们的指针。Go 提供了 p.Field 语法糖,方便通过指针访问结构体字段。
    • 切片:是引用类型,其内部已包含指针。通常直接传递切片值即可。若函数需改变切片长度/容量(如 append),标准做法是返回新的切片。

指针是 Go 语言中一把锋利的双刃剑。深刻理解其工作原理和应用场景,将使你能够编写出更优雅、高效和地道的 Go 代码。从这里开始,你已经具备了构建更复杂、更强大程序的坚实基础。


相关文章:

  • 进阶向:Flask框架详解,从零开始理解Web开发利器
  • 什么是哈希函数(SHA-256)
  • 华为云Flexus+DeepSeek征文|利用华为云一键部署的Dify平台构建高效智能电商客服系统实战
  • 【数据挖掘】贝叶斯分类学习—NaiveBayes
  • Spring Boot 3 多数据源改造全流程:Druid、HikariCP 与 dynamic-datasource 实战总结
  • 23种设计模式——策略模式:像换口红一样切换你的算法
  • Solidity学习 - 函数修改器(modifier)
  • 大事件项目记录10-文章分类接口开发-更新文章分类
  • 太速科技-670-3U VPX PCIe桥扩展3路M.2高速存储模块
  • 莫队算法 —— 将暴力玩出花
  • 配置文件application.yml使用指南
  • Conformal LEC:官方学习教程
  • 大语言模型推理速度优化之模型量化实践
  • 【Elasticsearch】全文检索 组合检索
  • 正交视图三维重建 笔记 2d线到3d线
  • red-black-tree
  • 《Go语言高级编程》玩转RPC
  • axure基础操作
  • Rust高效编程实战指南
  • c++学习(五、函数高级)