-
摘要
该视频主要讲述了指针的本质和用法。指针是用于间接控制其他变量的变量,通过存储其他变量的内存地址实现。指针类型在定义时需要在前面加上星号,表示它是一个指针类型。指针类型本质上是声明一个与变量类型相同大小的内存空间,并将这个空间的地址赋值给指针。指针类型的大小与它所指向的变量类型的大小相同,因为每个变量在内存中都有一个唯一的地址。通过指针可以间接地控制和操作它所指向的变量,例如修改其值或调用其方法。此外,视频还提到了函数传递和值传递的本质,以及指针在函数传递中的作用。总之,该视频详细解释了指针的本质、定义、使用方法和在函数传递中的作用,对于理解指针概念非常有帮助。
-
分段总结
折叠
00:30指针的定义和本质
1.指针是一个变量,存储的是另一个变量的地址 2.指针变量定义时前面有一个星号 * 3.指针类型可以指向不同类型的数据,如 int、float、struct 等
03:49指针的内存表示
1.指针变量存储的是一个地址,而不是直接存储数据值 2.内存中的每个空间都有唯一的地址 3.指针变量通过存储的地址来访问目标数据
09:12指针的定义和赋值
1.指针变量的定义:var pointer_name *type 2.指针变量的赋值:pointer_name = &variable 3.指针变量可以指向不同类型的数据,如 int、float、struct 等
11:22Go语言指针的特性
1.Go语言保留了指针,但限制了指针的运算 2.Go语言中的指针变量可以直接访问其指向的数据,无需显式解引用 3.Go语言提供了unsafe包,允许进行不安全的指针运算,但需要谨慎使用
-
重点
本视频暂不支持提取重点
一、指针 00:00
1. 指针的本质 00:06
1)需求讲解 00:16
-
-
结构体修改需求:在函数中修改结构体字段值并反映到原变量中,需要通过指针传递而非值传递
-
值传递问题:当结构体作为值传递时,函数内修改的是副本,不会影响原变量
-
指针解决方案:使用指针传递可以修改原结构体字段值,如func changeName(p *Person)
2)结构体定义 01:07
-
-
基本定义:type Person struct { name string }
-
方法定义:func (p Person) print()中Person表示指针接收器
-
指针优势:当结构体较大时使用指针可避免复制开销,且能修改原结构体
3)指针变量定义 02:40
-
-
定义语法:var pi Person = &p,其中表示指针类型,&取地址
-
打印指针:fmt.Printf("%p",pi)打印地址,fmt.Println(pi)打印指向的值
-
访问方式:通过(*pi).name或直接pi.name访问字段(Go特有语法糖)
4)指针类型的机理 03:41
-
-
存储本质:指针变量存储的是目标变量的内存地址
-
访问过程:先获取地址值,再通过地址访问实际数据
-
复制特性:复制指针变量时复制的是地址值,多个指针可指向同一内存
2. 指针的定义 09:18
-
-
基本语法:var po *float64定义指向float64的指针
-
初始化方式:可直接po := &Person{name:"bobby2"}一步完成
-
赋值限制:不能直接将结构体赋给指针,必须使用地址符&
3. go语言的指针 11:22
1)通过指针访问值 12:05
-
-
简化语法:Go允许直接通过指针访问字段(po.name),无需显式解引用((*po).name)
-
兼容特性:既支持传统指针语法,也支持类似非指针语言的简化访问
-
打印优化:fmt.Println会自动打印指针指向的值而非地址
2)go语言的阉割版指针 14:36
-
-
运算限制:Go指针不能像C语言那样进行算术运算(如p++)
-
安全设计:通过限制指针运算避免内存安全问题
-
unsafe包:特殊需求可使用unsafe包进行指针运算,但会失去安全性保障
-
设计理念:在灵活性和安全性之间取得平衡,日常开发推荐使用安全指针
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
指针的本质 | 指针是存储变量内存地址的变量,通过地址间接访问数据。修改指针指向的值会影响原变量。 | 值传递 vs 指针传递:值传递复制副本,指针传递直接修改原数据。 | ⭐⭐ |
指针的定义与初始化 | 使用 var p *int 定义指针,通过 & 取地址符获取变量地址(如 &a)。 | 星号(*)的两种含义:定义时表示指针类型(*int),使用时表示解引用(*p)。 | ⭐⭐ |
Go指针特性 | 1. 简化访问:可直接用 p.name 代替 (*p).name2. 禁止运算:不支持指针算术(需通过 unsafe 包实现)。 | 与C/C++区别:Go指针更安全,但功能受限(如无指针运算)。 | ⭐⭐⭐ |
指针的应用场景 | 结构体传参时需修改原数据(如 changeName(p *Person)),避免值传递的副本问题。 | 易错点:未取地址导致传值(changeName(p) 无效)。 | ⭐⭐ |
unsafe包的作用 | 提供指针运算能力,但标记为"不安全",需谨慎使用。 | 风险提示:直接操作内存可能引发不可控错误。 | ⭐⭐⭐⭐ |
-
摘要
该视频主要讲述了在Go语言中初始化的几种方式。对于结构体类型,会自动初始化;对于指针类型,需要使用new函数进行初始化;对于slice、map等类型,需要使用make函数进行初始化;对于channel类型,也需要使用make函数进行初始化。此外,视频还提到了在使用指针时需要注意的一些问题,如访问结构体成员时可能会出现的运行时错误,以及定义指针时需要初始化的重要性。
-
分段总结
折叠
00:01指针的定义和赋值
1.定义指针:通过定义一个指针变量来引用其他变量的值。 2.赋值方式:不能直接对指针进行赋值操作,需要通过指针变量来间接赋值。 3.取值方式:通过指针变量来获取其指向的值。
01:31指针的初始化
1.初始化重要性:指针在使用前必须进行初始化,以避免访问无效内存地址。 2.初始化方法:可以通过直接实例化、定义结构体指针并进行赋值、使用new方法等方式进行初始化。 3.new方法:用于创建并初始化结构体指针,返回指向新分配内存的指针。
07:22map的初始化
1.map初始化:map必须使用make方法进行初始化,不能自定义。 2.slice和person:slice和person可以不进行初始化,除非定义为指针。
-
重点
本视频暂不支持提取重点
一、指针的初始化 00:00
1. 指针的定义 00:17
-
-
基本定义:通过var po *Person声明指向Person结构体的指针变量
-
初始化方式:使用&取地址运算符,如po := &Person{name: "bobby2"}
2. 指针的特性
-
运算限制:与C语言不同,Go指针不能进行算术运算(如+1操作)
-
安全机制:标准指针是"阉割版",如需底层操作需使用unsafe包
-
访问方式
:支持两种等效语法:
-
(*po).name = "bobby3"
-
po.name = "bobby4"(语法糖)
-
3. 结构体指针的初始化 02:02
1)未初始化问题
-
-
常见错误:访问未初始化指针会触发invalid memory address异常
-
默认值:未初始化的指针变量值为nil,打印显示为<nil>
-
对比说明:普通结构体变量会自动初始化,但指针类型不会
2)第一种初始化方式 03:40
-
直接实例化:ps := &Person{}创建结构体并立即取地址
-
特点:最直观的初始化方式,适合已知初始值的情况
3)第二种初始化方式 03:56
-
引用现有变量:先定义结构体变量,再取其地址
-
适用场景:当需要基于已有变量创建指针时使用
4)第三种初始化方式 04:54
-
-
new函数:var pp = new(Person)自动分配内存并返回指针
-
底层机制
:
-
分配零值内存空间
-
返回该内存地址
-
等效于先创建后取地址的复合操作
-
4. 初始化总结 06:32
-
关键原则:所有指针必须显式初始化后才能使用
-
初始化方法对比:
-
语言规范
:
-
指针初始化推荐使用new函数
-
与make的区别:make用于slice/map/channel,new用于指针
-
未初始化指针访问会导致nil pointer dereference错误(类似Java的NullPointerException)
-
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
指针定义与赋值 | 通过int p = &a方式定义指针并赋值,不能直接对未初始化的指针赋值(如p = 5非法) | 指针必须指向有效内存地址才能操作 | ⭐⭐⭐ |
指针初始化 | 三种方式:1. 直接实例化(&a)2. 从已存在变量取地址(p = &person{})3. new函数(p = new(person)) | 未初始化的指针访问会触发nil pointer错误(类似NullPointerException) | ⭐⭐⭐⭐ |
结构体与指针差异 | 结构体变量自动初始化(默认值),指针需显式初始化 | 直接访问结构体字段安全,指针需确保非nil | ⭐⭐ |
new函数作用 | 分配内存并返回地址,推荐用于指针初始化 | 与make区别:new返回指针,make用于slice/map/channel | ⭐⭐⭐ |
强制初始化类型 | map必须用make初始化,slice可延迟初始化,结构体指针需显式处理 | 未初始化的map操作会引发panic | ⭐⭐⭐⭐ |
-
摘要
该视频主要讲述了如何通过指针交换两个值。视频中强调了规范命名的重要性,以提高代码的可读性。对于包含两个单词的名称,接收者命名一般遵循两个单词首字母全部小写的规范,例如pn。此外,视频还介绍了如何使用指针交换两个值。在示例中,传入的两个参数a和b分别等于1和2,通过指针交换它们的值。然而,输出结果中a仍然等于1,这表明交换操作没有成功。这是因为指针的使用不当导致的。要成功交换两个值,需要正确使用指针。
-
分段总结
折叠
00:01结构体与方法接收者命名规范
1.结构体与方法接收者命名规范:接收者命名一般为p加上结构体名称,如person命名为p。 2.命名规范:使用首字母小写命名,如person命名为p。 3.随意命名:虽然可以随意命名,但遵循规范更佳。
01:45通过指针交换两个值
1.无指针情况:通过函数返回值交换两个值。 2.有指针情况:通过指针直接交换两个值,无需返回值。 3.示例代码:通过指针交换两个变量的值。
04:48指针传递与值传递
1.函数调用本质:值传递,包括指针传递。 2.指针传递:传递的是地址,可以修改底层数据。 3.值传递:传递的是值的副本,不能修改底层数据。
09:13正确交换两个变量的值
1.错误示例:直接交换两个篮子中的值,不影响外部变量。 2.正确方法:使用临时变量,交换临时变量和外部变量的值。 3.示例代码:通过临时变量正确交换两个变量的值。
-
重点
本视频暂不支持提取重点
一、通过swap交换指针的值 00:02
1. 接收者命名规范 00:22
-
-
单单词命名:当接收者为单单词结构体(如Person)时,使用首字母小写(如p)作为接收者名称
-
多单词命名:当结构体名由多个单词组成(如PersonName)时,使用各单词首字母小写组合(如pn)作为接收者名称
-
规范本质:命名规范没有特殊原因,主要是为了保持代码风格统一,便于团队协作开发
2. 通过指针交换两个值的方法 01:45
1)无指针的情况 02:03
-
-
实现方式:需要定义返回值的函数,通过返回值重新赋值给原变量
-
示例代码:
-
缺点:需要额外的返回值处理,代码不够简洁
2)有指针的情况 02:22
-
-
直接交换误区:
-
正确实现:
-
调用示例:
3)原理分析 04:48
-
-
内存模型
:
-
每个变量都有自己的内存地址(如a=0x001指向值1,b=0x002指向值2)
-
函数参数传递时创建新的指针变量(值传递)
-
-
错误原因:直接交换指针变量只是交换了函数内部局部变量的指向,不影响外部变量
-
正确逻辑:需要通过指针解引用(*操作符)修改指针指向的实际值
4)值传递分析 09:08
-
-
本质特性:Go语言中所有参数传递都是值传递,包括指针传递
-
指针传递本质:传递的是指针值的副本(内存地址的拷贝)
-
关键区别
:
-
修改指针本身(改变指向):只影响函数内部
-
修改指针指向的值:会影响外部变量
-
-
初始化注意
:
-
指针必须初始化(使用new或取地址&)
-
未初始化指针会导致nil pointer错误
-
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
结构体方法接收者 | 接收者可以是值类型或指针类型,指针类型能修改内部值且避免复制 | 指针接收者与值接收者的本质区别 | ⭐⭐ |
命名规范 | 单单词接收者首字母小写(p),多单词首字母全小写(pn) | 规范虽非强制但建议统一遵循 | ⭐ |
指针交换值案例 | 错误示范:仅交换指针地址;正确做法:通过*操作符修改指针指向的值 | 值传递本质导致地址交换无效,必须操作指针指向的内存 | ⭐⭐⭐ |
指针底层原理 | 函数调用时参数是值传递,修改指针地址不影响原变量,需通过*操作符修改目标值 | 临时变量在值交换中的必要性 | ⭐⭐⭐⭐ |
-
摘要
该视频主要讲述了在Go语言中,不同类型的默认值以及在结构体中的使用,以及slice和map的特性和使用注意事项。首先介绍了不同类型的默认值,包括布尔类型、字符串类型、切片类型等。接着讲述了自定义的结构体默认值是其字段的默认值,而不是整个结构体的默认值。此外还提到了在判断结构体中的属性是否相等时,需要每一个属性都相等才最终相等。对于slice和map的使用注意事项,强调了slice底层对应一个数组,其值由指针指向数组中的元素,而map分为near map和mmap两种类型,操作上有所不同。在进行map赋值时,需要进行mac初始化,以避免潜在的问题。
-
分段总结
折叠
00:01nil在Go中的含义
1.nil在Go语言中代表某些数据类型的零值。 2.不同数据类型的零值不同,如布尔型的零值为false,数值型的零值为0,字符串的零值为空字符串。 3.指针和slice的默认值也是nil。 4.struct的默认值是其每个字段的默认值。
04:39nil在判断中的应用
1.判断一个值是否为nil,可以直接使用等号。 2.判断error是否为nil的常用手段。 3.slice的默认值问题,未初始化的slice与使用make初始化的slice的区别。 4.map的默认值也是nil,但nil map和非nil map在赋值操作上有区别。
-
重点
本视频暂不支持提取重点
一、nil 00:09
1. 数据零值 00:56
-
-
布尔类型: 默认值为false
-
数值类型: 包括float和int等,默认值为0
-
字符串类型: 默认值为空字符串""
-
指针类型: 默认值为nil
-
slice类型: 默认值为nil
-
map类型: 默认值为nil
-
channel类型: 默认值为nil
-
interface类型: 默认值为nil
-
function类型: 默认值为nil
2. struct默认值 02:15
-
-
非nil特性: struct的默认值不是nil,而是其字段的默认值组合
-
字段继承: 每个字段继承各自类型的零值
-
判等特性: 可以直接用==比较两个struct实例
-
示例说明
:
-
name默认为空字符串
-
age默认为0
-
f默认为nil指针
-
3. error判断 04:46
-
本质: error是interface类型,默认值为nil
-
判断方法: 直接使用== nil判断是否有错误
-
常见用法:
4. slice默认值 05:11
-
-
nil slice
:
-
通过var声明:var ps []Person
-
底层结构体各字段为零值
-
ptr为nil,len为0,cap为0
-
可以与nil直接比较
-
-
empty slice
:
-
通过make创建:make([]Person, 0)
-
ptr指向空数组,len为0,cap为0
-
不与nil相等
-
-
底层结构:
5. map默认值 10:26
-
-
nil map
:
-
通过var声明:var m map[string]string
-
可以进行range操作(无效果)
-
可以取值(返回零值)
-
不能赋值(会panic)
-
-
empty map
:
-
通过make创建:make(map[string]string, 0)
-
可以进行所有map操作
-
-
安全建议
:
-
通常使用make初始化map
-
只有在需要区分nil和empty的特殊场景才使用var声明
-
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
nil关键字 | Go语言中代表数据类型的零值,不同数据类型零值不同 | 布尔类型零值为false,数值类型为零,字符串为空串,指针/slice/map/channel/interface/function为零值nil | ⭐⭐⭐⭐ |
struct零值 | 自定义struct的零值是各字段的零值组合,不是nil | struct可以直接用==判等,需所有字段相等才返回true | ⭐⭐⭐ |
slice的nil判断 | 未初始化的slice为nil,make创建的slice即使长度为0也不等于nil | nil slice和empty slice底层结构不同,判等结果不同 | ⭐⭐⭐⭐⭐ |
map的nil特性 | nil map可以安全进行读取操作,但写入会panic | nil map和empty map在range遍历时表现相同,但赋值操作不同 | ⭐⭐⭐⭐ |
指针与interface | 指针和interface类型的零值为nil | error本质是interface,常用err == nil判断错误 | ⭐⭐⭐ |
零值设计哲学 | Go通过零值机制确保变量总有默认值,减少未初始化错误 | 需特别注意slice和map在nil状态下的操作差异 | ⭐⭐⭐⭐ |
以下是关于 Go 指针的 20 道八股文题(难度递增)和 25 道场景题(涵盖基础到高级应用),系统覆盖指针的核心概念、底层原理及实践技巧。
一、八股文题(20 题)
基础篇(1-8 题)
-
问题 :Go 中指针的定义格式是什么?指针变量存储的是什么? 答案 : 定义格式:
var 指针变量名 *类型
(如var p *int
)。 指针变量存储的是另一个变量的内存地址。 -
问题 :如何获取一个变量的地址?如何通过指针访问目标变量的值? 答案:
-
获取地址:使用
&
运算符(如p := &x
,p
是x
的地址)。 -
访问值:使用
*
运算符(如*p
表示p
指向的变量值)。
-
-
问题 :指针的零值是什么?对
nil
指针解引用会发生什么? 答案:-
指针的零值是
nil
(表示不指向任何变量)。 -
对
nil
指针解引用(*nilPtr
)会触发panic: runtime error: invalid memory address or nil pointer dereference
。
-
-
问题 :
*
运算符在指针定义和指针解引用时有什么区别? 答案:-
定义时(如
*int
):表示 "指向 int 类型的指针",*
是类型的一部分。 -
解引用时(如
*p
):表示 "获取指针指向的变量值",*
是运算符。
-
-
问题 :值类型变量和指针类型变量作为函数参数的根本区别是什么? 答案:
-
值类型参数:函数接收变量副本,修改副本不影响原变量。
-
指针类型参数:函数接收指针副本(仍指向原变量地址),修改
*指针
会影响原变量。
-
-
问题 :数组指针和指针数组的区别是什么?分别如何定义? 答案:
-
数组指针:指向数组的指针(
*[n]T
),如var p *[3]int
。 -
指针数组:元素为指针的数组(
[n]*T
),如var arr [3]*int
。 核心区别:数组指针是单个指针,指针数组是多个指针的集合。
-
-
问题 :Go 支持指针运算吗?例如
p++
或p+1
这样的操作是否合法? 答案 : 不支持。Go 刻意简化了指针功能,禁止指针运算(如增减、比较地址大小),仅允许通过&
取地址和*
解引用。 -
问题 :结构体指针如何访问结构体的字段?是否需要先解引用? 答案 : 结构体指针访问字段时可直接使用
.
运算符(无需显式解引用),如p.Name
等价于(*p).Name
(编译器自动优化)。
中级篇(9-15 题)
-
问题 :什么是 "指针的指针"(双重指针)?在什么场景下会用到? 答案 : 指针的指针是指向指针变量的指针(如
**int
),存储的是指针变量的地址。 场景:函数中修改外部指针变量本身(如为指针分配内存),例如:go
运行
func initPtr(p **int) { *p = new(int) // 修改外部指针变量,使其指向新内存 ** p = 10 }
-
问题 :
new(T)
和&T{}
都能创建指针,它们的区别是什么? 答案:-
new(T)
:分配 T 类型的零值内存,返回*T
(仅初始化零值)。 -
&T{}
:通过结构体字面量初始化,可指定字段值,返回*T
(适用于结构体)。 对于非结构体类型(如int
),new(int)
与&int{}
等价。
-
-
问题 :指针作为函数返回值时,需要注意什么?为什么不能返回函数内局部变量的指针? 答案:
-
注意:返回的指针指向的变量生命周期必须长于函数调用(否则可能指向无效内存)。
-
可以返回局部变量的指针:Go 的逃逸分析会将被外部引用的局部变量分配到堆上,避免悬垂指针。
-
-
问题 :接口类型变量存储指针类型时,接口的动态类型和动态值分别是什么? 答案:
-
动态类型:指针指向的类型(如
*int
)。 -
动态值:指针本身(存储的地址)。 例如:
var i int; var iface interface{} = &i
中,动态类型是*int
,动态值是&i
。
-
-
问题 :切片的底层结构包含指针,传递切片时是否需要传递切片指针?为什么? 答案 : 通常不需要。切片本身包含指向底层数组的指针,值传递切片时,副本的指针仍指向原数组,修改元素会影响原切片。仅当需要修改切片本身(如
len
、cap
或重新分配底层数组)时,才需传递切片指针。 -
问题 :
map
是引用类型,传递map
指针和直接传递map
有区别吗? 答案 : 区别不大。map
本身是指针包装,传递map
和map
指针都是传递地址(前者是*hmap
,后者是**hmap
)。修改map
内容时两者效果相同,但修改map
指针(如重新分配*m = make(map...)
)时,只有传递指针才能影响外部。 -
问题 :指针类型如何实现接口?与值类型实现接口有何区别? 答案:
-
指针类型实现接口:需为指针类型定义接口方法(
func (p *T) Method()
),仅指针实例可赋值给接口变量。 -
值类型实现接口:需为值类型定义接口方法(
func (t T) Method()
),值实例和指针实例均可赋值给接口变量(指针会自动解引用)。
-
高级篇(16-20 题)
-
问题 :什么是 "悬垂指针"(dangling pointer)?Go 如何避免悬垂指针? 答案 : 悬垂指针是指向已释放内存的指针。 Go 通过垃圾回收(GC) 和逃逸分析避免:
-
逃逸分析将被指针引用的局部变量分配到堆上,延长生命周期。
-
GC 自动回收不再被引用的内存,避免指针指向无效内存。
-
-
问题 :指针的内存布局是什么?32 位和 64 位系统中指针占用多少字节? 答案: 指针存储的是内存地址,布局为一个纯地址值。
-
32 位系统:指针占用 4 字节(可寻址 2³² 字节内存)。
-
64 位系统:指针占用 8 字节(可寻址 2⁶⁴字节内存)。
-
-
问题 :
unsafe.Pointer
的作用是什么?使用时需要注意哪些风险? 答案:-
作用:
unsafe.Pointer
是 "通用指针",可转换为任意类型指针,用于绕过 Go 的类型安全检查(如*int
与*float64
互转)。 -
风险:破坏类型安全,可能导致内存访问错误;依赖具体内存布局,可移植性差;可能被 GC 错误处理。
-
-
问题 :如何通过指针实现两个变量的值交换?与值传递方式相比有何优势? 答案: 指针实现:
go
运行
func swap(a, b *int) { *a, *b = *b, *a }
优势:无需复制变量值(尤其适合大结构体),直接操作原变量,性能更优。
-
问题 :Go 的指针与 C 的指针最主要的区别是什么? 答案:
-
安全性:Go 禁止指针运算,无悬垂指针(GC 管理);C 允许指针运算,需手动管理内存,易产生悬垂指针。
-
功能:Go 指针仅支持取地址(
&
)和解引用(*
);C 指针支持算术运算、转换为整数等。 -
类型严格性:Go 指针类型严格(
*int
与*float64
不可隐式转换);C 指针转换更灵活(需显式类型转换)。
-
二、场景题(25 题)
基础应用(1-8 题)
-
场景 :编写函数,通过指针交换两个整数的值。 答案:
go
func swap(a, b *int) { *a, *b = *b, *a } // 使用: // x, y := 10, 20 // swap(&x, &y) // x=20, y=10
-
场景 :定义一个结构体
Person
,通过结构体指针修改其字段值,并比较直接修改与指针修改的区别。 答案:go
type Person struct { Name string } // 直接修改(值传递,不影响原结构体) func modifyValue(p Person, newName string) { p.Name = newName } // 指针修改(影响原结构体) func modifyPointer(p *Person, newName string) { p.Name = newName } // 使用: // p := Person{Name: "Alice"} // modifyValue(p, "Bob") // p.Name仍为"Alice" // modifyPointer(&p, "Bob") // p.Name变为"Bob"
-
场景 :创建一个指针数组,存储多个整数的地址,遍历数组并修改每个整数的值。 答案:
go
func main() { a, b, c := 1, 2, 3 // 指针数组:元素是*int ptrs := []*int{&a, &b, &c} // 遍历并修改值 for _, p := range ptrs { *p *= 2 // 每个整数翻倍 } // 输出:2 4 6 fmt.Println(a, b, c) }
-
场景 :编写函数,接收
*int
类型参数,若指针为nil
则初始化它,否则将其值加 1。 答案:go
func initOrIncrement(p *int) { if p == nil { return // 无法修改nil指针(需双重指针) } if *p == 0 { // 假设0为未初始化 *p = 1 } else { *p++ } } // 使用: // var x int // initOrIncrement(&x) // x=1 // initOrIncrement(&x) // x=2
-
场景 :通过指针实现一个函数,将字符串反转(原地修改,不创建新字符串)。 答案:
go
// 字符串不可变,需转换为[]byte func reverseString(s *[]byte) { for i, j := 0, len(*s)-1; i < j; i, j = i+1, j-1 { (*s)[i], (*s)[j] = (*s)[j], (*s)[i] } } // 使用: // s := []byte("hello") // reverseString(&s) // fmt.Println(string(s)) // "olleh"
-
场景 :创建一个数组指针,通过指针修改数组中的元素,并比较数组指针与指针数组的使用差异。 答案:
go
func main() { // 数组指针:指向数组的指针 arr := [3]int{1, 2, 3} pArr := &arr pArr[0] = 10 // 等价于(*pArr)[0] = 10 // 指针数组:元素为指针 a, b, c := 1, 2, 3 arrPtrs := [3]*int{&a, &b, &c} *arrPtrs[0] = 10 fmt.Println(arr) // [10 2 3] fmt.Println(a, b, c) // 10 2 3 }
-
场景 :编写函数,接收指针类型参数,判断指针是否为
nil
,并处理nil
情况避免 panic。 答案:go
func safeDereference(p *int) int { if p == nil { return 0 // 处理nil指针,返回默认值 } return *p } // 使用: // var x *int // fmt.Println(safeDereference(x)) // 0(无panic) // y := 5 // fmt.Println(safeDereference(&y)) // 5
-
场景 :通过指针传递大结构体,比较其与值传递的性能差异(使用
time
包)。 答案:go
import "time" // 大结构体 type BigStruct struct { Data [1024 * 1024]int // 4MB(每个int4字节) } // 值传递 func passByValue(b BigStruct) {} // 指针传递 func passByPointer(b *BigStruct) {} func main() { b := BigStruct{} // 测试值传递耗时 start := time.Now() for i := 0; i < 100; i++ { passByValue(b) } fmt.Println("值传递耗时:", time.Since(start)) // 测试指针传递耗时 start = time.Now() for i := 0; i < 100; i++ { passByPointer(&b) } fmt.Println("指针传递耗时:", time.Since(start)) // 明显快于值传递 }
中级应用(9-18 题)
-
场景 :使用双重指针(
**int
)在函数内部为外部指针分配内存并赋值。 答案:go
func allocate(p **int, value int) { *p = new(int) // 为外部指针分配内存 **p = value // 赋值 } // 使用: // var x *int // allocate(&x, 10) // fmt.Println(*x) // 10(x已被正确初始化)
-
场景 :实现一个函数,接收切片指针,向切片中添加元素并修改切片的长度(对比直接传递切片的区别)。 答案:
go
// 直接传递切片:修改len不影响外部 func appendValue(s []int, val int) { s = append(s, val) } // 传递切片指针:修改影响外部 func appendPointer(s *[]int, val int) { *s = append(*s, val) } // 使用: // s := []int{1,2} // appendValue(s, 3) // s仍为[1,2] // appendPointer(&s, 3) // s变为[1,2,3]
-
场景 :定义接口
Logger
,分别用结构体和结构体指针实现该接口,观察赋值差异。 答案:go
type Logger interface { Log(msg string) } type ConsoleLogger struct{} // 值类型实现接口 func (c ConsoleLogger) Log(msg string) { fmt.Println("Log:", msg) } type FileLogger struct{} // 指针类型实现接口 func (f *FileLogger) Log(msg string) { fmt.Println("FileLog:", msg) } func main() { var l Logger // 结构体实现:值和指针均可赋值 c := ConsoleLogger{} l = c // 合法 l = &c // 合法 // 指针实现:仅指针可赋值 f := FileLogger{} // l = f // 编译错误(值类型未实现接口) l = &f // 合法 }
-
场景 :通过指针实现链表的插入操作(在指定节点后插入新节点)。 答案:
go
type ListNode struct { Val int Next *ListNode } // 在node后插入新节点 func insertAfter(node *ListNode, val int) { if node == nil { return } newNode := &ListNode{Val: val} newNode.Next = node.Next node.Next = newNode } // 使用: // head := &ListNode{Val: 1} // insertAfter(head, 2) // head.Next.Val=2
-
场景 :编写函数,接收
map
指针,在函数内重新初始化map
(对比直接传递map
的区别)。 答案:go
// 直接传递map:重新初始化不影响外部 func resetMap(m map[int]string) { m = make(map[int]string) } // 传递map指针:重新初始化影响外部 func resetMapPointer(m *map[int]string) { *m = make(map[int]string) } // 使用: // m := map[int]string{1: "a"} // resetMap(m) // m仍为{1: "a"} // resetMapPointer(&m) // m变为空map
-
场景 :使用指针实现结构体的深拷贝(对比浅拷贝)。 答案:
go
type Data struct { Num int List []int // 引用类型字段 } // 浅拷贝:仅复制指针,共享底层切片 func shallowCopy(d Data) Data { return d } // 深拷贝:通过指针手动复制引用类型 func deepCopy(d *Data) *Data { newList := make([]int, len(d.List)) copy(newList, d.List) return &Data{ Num: d.Num, List: newList, } } // 使用: // d1 := Data{Num: 1, List: []int{1,2}} // d2 := shallowCopy(d1) // d2.List[0] = 100 // d1.List[0]也变为100 // d3 := deepCopy(&d1) // d3.List[0] = 200 // d1.List不受影响
-
场景 :通过指针传递实现并发安全的计数器(使用
sync.Mutex
)。 答案:go
import "sync" type Counter struct { mu sync.Mutex value int } // 指针接收者确保锁和值被正确共享 func (c *Counter) Incr() { c.mu.Lock() defer c.mu.Unlock() c.value++ } func (c *Counter) Get() int { c.mu.Lock() defer c.mu.Unlock() return c.value } // 使用: // c := &Counter{} // for i := 0; i < 1000; i++ { // go c.Incr() // } // time.Sleep(time.Second) // fmt.Println(c.Get()) // 1000(并发安全)
-
场景 :使用
unsafe.Pointer
将int
指针转换为float64
指针,修改其值(演示类型转换)。 答案:go
import "unsafe" func main() { var x int = 0x4048000000000000 // float64(3.5)的二进制表示 pInt := &x // 转换为unsafe.Pointer,再转换为*float64 pFloat := (*float64)(unsafe.Pointer(pInt)) fmt.Println(*pFloat) // 输出3.5 *pFloat = 2.5 fmt.Println(x) // 输出0x4000000000000000(float64(2.5)的整数表示) }
-
场景 :编写函数,通过指针判断两个变量是否存储在同一块内存地址。 答案:
go
func isSameAddress(a, b *int) bool { return a == b // 比较指针地址 } // 使用: // x := 5 // y := 5 // fmt.Println(isSameAddress(&x, &y)) // false(不同地址) // z := &x // fmt.Println(isSameAddress(&x, z)) // true(相同地址)
-
场景 :实现一个函数,接收指针类型的参数,返回该指针指向的值的副本(适用于任意类型)。 答案:
go
import "reflect" // 使用反射实现通用副本函数 func copyValue(p interface{}) interface{} { val := reflect.ValueOf(p).Elem() // 解引用指针 copyVal := reflect.New(val.Type()).Elem() copyVal.Set(val) return copyVal.Interface() } // 使用: // x := 10 // y := copyValue(&x).(int) // y是x的副本 // s := "hello" // t := copyValue(&s).(string) // t是s的副本
高级应用(19-25 题)
-
场景 :使用指针实现二叉树的前序遍历(递归和非递归方式)。 答案:
go
type TreeNode struct { Val int Left *TreeNode Right *TreeNode } // 递归前序遍历 func preorderRecursive(root *TreeNode, result *[]int) { if root == nil { return } *result = append(*result, root.Val) preorderRecursive(root.Left, result) preorderRecursive(root.Right, result) } // 非递归前序遍历 func preorderIterative(root *TreeNode) []int { var result []int if root == nil { return result } stack := []*TreeNode{root} for len(stack) > 0 { node := stack[len(stack)-1] stack = stack[:len(stack)-1] result = append(result, node.Val) // 右子树先入栈(栈是LIFO) if node.Right != nil { stack = append(stack, node.Right) } if node.Left != nil { stack = append(stack, node.Left) } } return result }
-
场景 :通过指针实现一个简单的内存池(复用对象,减少 GC 压力)。 答案:
go
import "sync" type Object struct { Data int // 其他字段... } type Pool struct { mu sync.Mutex items []*Object // 存储可用对象指针 } func NewPool() *Pool { return &Pool{ items: make([]*Object, 0), } } // 获取对象(从池或新建) func (p *Pool) Get() *Object { p.mu.Lock() defer p.mu.Unlock() if len(p.items) > 0 { // 从池尾取对象 obj := p.items[len(p.items)-1] p.items = p.items[:len(p.items)-1] return obj } // 新建对象 return &Object{} } // 归还对象(重置并放入池) func (p *Pool) Put(obj *Object) { p.mu.Lock() defer p.mu.Unlock() // 重置对象状态 obj.Data = 0 p.items = append(p.items, obj) }
-
场景 :使用指针和接口实现策略模式(根据不同策略动态切换算法)。 答案:
go
// 策略接口 type PaymentStrategy interface { Pay(amount float64) string } // 具体策略:信用卡支付 type CreditCard struct { CardNo string } func (c *CreditCard) Pay(amount float64) string { return fmt.Sprintf("用信用卡%s支付%.2f元", c.CardNo, amount) } // 具体策略:支付宝支付 type Alipay struct { UserID string } func (a *Alipay) Pay(amount float64) string { return fmt.Sprintf("支付宝用户%s支付%.2f元", a.UserID, amount) } // 上下文:订单 type Order struct { strategy PaymentStrategy // 策略指针 } func (o *Order) SetStrategy(s PaymentStrategy) { o.strategy = s } func (o *Order) Pay(amount float64) string { return o.strategy.Pay(amount) } // 使用: // order := &Order{} // order.SetStrategy(&CreditCard{CardNo: "1234"}) // fmt.Println(order.Pay(100)) // 信用卡支付 // order.SetStrategy(&Alipay{UserID: "user1"}) // fmt.Println(order.Pay(200)) // 支付宝支付
-
场景 :通过指针操作二进制数据(使用
encoding/binary
包解析字节流)。 答案:go
import ( "encoding/binary" "fmt" ) // 解析字节流为结构体(小端序) func parseBytes(data []byte) (*struct { ID uint16 Len uint32 Flag bool }, error) { if len(data) < 2+4+1 { // ID(2) + Len(4) + Flag(1) return nil, fmt.Errorf("数据长度不足") } result := &struct { ID uint16 Len uint32 Flag bool }{} // 通过指针解析 result.ID = binary.LittleEndian.Uint16(data[0:2]) result.Len = binary.LittleEndian.Uint32(data[2:6]) result.Flag = data[6] != 0 return result, nil }
-
场景 :实现一个泛型函数,接收任意类型的指针,将其值设置为该类型的零值。 答案:
go
import "reflect" func setZero[T any](p *T) { // 通过反射获取零值并设置 zero := reflect.Zero(reflect.TypeOf(*p)).Interface().(T) *p = zero } // 使用: // x := 5 // setZero(&x) // x=0 // s := "hello" // setZero(&s) // s="" // m := map[int]int{1: 2} // setZero(&m) // m=nil
-
场景 :使用指针实现一个单例模式(确保全局只有一个实例)。 答案:
go
import "sync" type Singleton struct { // 单例字段 } var ( instance *Singleton once sync.Once // 确保只初始化一次 ) // 获取单例实例 func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} // 只执行一次 }) return instance } // 使用: // a := GetInstance() // b := GetInstance() // fmt.Println(a == b) // true(同一实例)
-
场景 :通过指针和
sync.Cond
实现多个 goroutine 的同步(等待某个条件满足)。 答案:go
import ( "sync" "time" ) type Queue struct { mu sync.Mutex cond *sync.Cond items []int closed bool } func NewQueue() *Queue { q := &Queue{} q.cond = sync.NewCond(&q.mu) // 基于队列的锁创建条件变量 return q } // 入队 func (q *Queue) Push(item int) { q.mu.Lock() defer q.mu.Unlock() if q.closed { return } q.items = append(q.items, item) q.cond.Signal() // 唤醒一个等待的goroutine } // 出队(阻塞直到有元素或关闭) func (q *Queue) Pop() (int, bool) { q.mu.Lock() defer q.mu.Unlock() // 等待条件:队列非空或已关闭 for len(q.items) == 0 && !q.closed { q.cond.Wait() // 释放锁并等待 } if q.closed { return 0, false } item := q.items[0] q.items = q.items[1:] return item, true } // 关闭队列 func (q *Queue) Close() { q.mu.Lock() defer q.mu.Unlock() q.closed = true q.cond.Broadcast() // 唤醒所有等待的goroutine }
总结
以上题目从基础语法到高级应用,全面覆盖了 Go 指针的核心知识点:
-
八股文题聚焦指针的定义、特性、安全机制及与其他类型的区别。
-
场景题结合实际开发需求,涵盖数据结构(链表、树)、并发控制、设计模式等,体现指针在性能优化和内存管理中的关键作用。
通过练习这些题目,可深入理解指针在 Go 中的设计哲学(安全与简洁并重)及最佳实践。