golang 10指针

  • 摘要

    该视频主要讲述了指针的本质和用法。指针是用于间接控制其他变量的变量,通过存储其他变量的内存地址实现。指针类型在定义时需要在前面加上星号,表示它是一个指针类型。指针类型本质上是声明一个与变量类型相同大小的内存空间,并将这个空间的地址赋值给指针。指针类型的大小与它所指向的变量类型的大小相同,因为每个变量在内存中都有一个唯一的地址。通过指针可以间接地控制和操作它所指向的变量,例如修改其值或调用其方法。此外,视频还提到了函数传递和值传递的本质,以及指针在函数传递中的作用。总之,该视频详细解释了指针的本质、定义、使用方法和在函数传递中的作用,对于理解指针概念非常有帮助。

  • 分段总结

    折叠

    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 题)
  1. 问题 :Go 中指针的定义格式是什么?指针变量存储的是什么? 答案 : 定义格式:var 指针变量名 *类型(如var p *int)。 指针变量存储的是另一个变量的内存地址

  2. 问题 :如何获取一个变量的地址?如何通过指针访问目标变量的值? 答案

    • 获取地址:使用&运算符(如p := &xpx的地址)。

    • 访问值:使用*运算符(如*p表示p指向的变量值)。

  3. 问题 :指针的零值是什么?对nil指针解引用会发生什么? 答案

    • 指针的零值是nil(表示不指向任何变量)。

    • nil指针解引用(*nilPtr)会触发panic: runtime error: invalid memory address or nil pointer dereference

  4. 问题*运算符在指针定义和指针解引用时有什么区别? 答案

    • 定义时(如*int):表示 "指向 int 类型的指针",*是类型的一部分。

    • 解引用时(如*p):表示 "获取指针指向的变量值",*是运算符。

  5. 问题 :值类型变量和指针类型变量作为函数参数的根本区别是什么? 答案

    • 值类型参数:函数接收变量副本,修改副本不影响原变量。

    • 指针类型参数:函数接收指针副本(仍指向原变量地址),修改*指针会影响原变量。

  6. 问题 :数组指针和指针数组的区别是什么?分别如何定义? 答案

    • 数组指针:指向数组的指针(*[n]T),如var p *[3]int

    • 指针数组:元素为指针的数组([n]*T),如var arr [3]*int。 核心区别:数组指针是单个指针,指针数组是多个指针的集合。

  7. 问题 :Go 支持指针运算吗?例如p++p+1这样的操作是否合法? 答案 : 不支持。Go 刻意简化了指针功能,禁止指针运算(如增减、比较地址大小),仅允许通过&取地址和*解引用。

  8. 问题 :结构体指针如何访问结构体的字段?是否需要先解引用? 答案 : 结构体指针访问字段时可直接使用.运算符(无需显式解引用),如p.Name等价于(*p).Name(编译器自动优化)。

中级篇(9-15 题)
  1. 问题 :什么是 "指针的指针"(双重指针)?在什么场景下会用到? 答案 : 指针的指针是指向指针变量的指针(如**int),存储的是指针变量的地址。 场景:函数中修改外部指针变量本身(如为指针分配内存),例如:

    go

    运行

    复制代码
    func initPtr(p **int) {
        *p = new(int) // 修改外部指针变量,使其指向新内存
        ** p = 10
    }
  2. 问题new(T)&T{}都能创建指针,它们的区别是什么? 答案

    • new(T):分配 T 类型的零值内存,返回*T(仅初始化零值)。

    • &T{}:通过结构体字面量初始化,可指定字段值,返回*T(适用于结构体)。 对于非结构体类型(如int),new(int)&int{}等价。

  3. 问题 :指针作为函数返回值时,需要注意什么?为什么不能返回函数内局部变量的指针? 答案

    • 注意:返回的指针指向的变量生命周期必须长于函数调用(否则可能指向无效内存)。

    • 可以返回局部变量的指针:Go 的逃逸分析会将被外部引用的局部变量分配到堆上,避免悬垂指针。

  4. 问题 :接口类型变量存储指针类型时,接口的动态类型和动态值分别是什么? 答案

    • 动态类型:指针指向的类型(如*int)。

    • 动态值:指针本身(存储的地址)。 例如:var i int; var iface interface{} = &i中,动态类型是*int,动态值是&i

  5. 问题 :切片的底层结构包含指针,传递切片时是否需要传递切片指针?为什么? 答案 : 通常不需要。切片本身包含指向底层数组的指针,值传递切片时,副本的指针仍指向原数组,修改元素会影响原切片。仅当需要修改切片本身(如lencap或重新分配底层数组)时,才需传递切片指针。

  6. 问题map是引用类型,传递map指针和直接传递map有区别吗? 答案 : 区别不大。map本身是指针包装,传递mapmap指针都是传递地址(前者是*hmap,后者是**hmap)。修改map内容时两者效果相同,但修改map指针(如重新分配*m = make(map...))时,只有传递指针才能影响外部。

  7. 问题 :指针类型如何实现接口?与值类型实现接口有何区别? 答案

    • 指针类型实现接口:需为指针类型定义接口方法(func (p *T) Method()),仅指针实例可赋值给接口变量。

    • 值类型实现接口:需为值类型定义接口方法(func (t T) Method()),值实例和指针实例均可赋值给接口变量(指针会自动解引用)。

高级篇(16-20 题)
  1. 问题 :什么是 "悬垂指针"(dangling pointer)?Go 如何避免悬垂指针? 答案 : 悬垂指针是指向已释放内存的指针。 Go 通过垃圾回收(GC)逃逸分析避免:

    • 逃逸分析将被指针引用的局部变量分配到堆上,延长生命周期。

    • GC 自动回收不再被引用的内存,避免指针指向无效内存。

  2. 问题 :指针的内存布局是什么?32 位和 64 位系统中指针占用多少字节? 答案: 指针存储的是内存地址,布局为一个纯地址值。

    • 32 位系统:指针占用 4 字节(可寻址 2³² 字节内存)。

    • 64 位系统:指针占用 8 字节(可寻址 2⁶⁴字节内存)。

  3. 问题unsafe.Pointer的作用是什么?使用时需要注意哪些风险? 答案

    • 作用:unsafe.Pointer是 "通用指针",可转换为任意类型指针,用于绕过 Go 的类型安全检查(如*int*float64互转)。

    • 风险:破坏类型安全,可能导致内存访问错误;依赖具体内存布局,可移植性差;可能被 GC 错误处理。

  4. 问题 :如何通过指针实现两个变量的值交换?与值传递方式相比有何优势? 答案: 指针实现:

    go

    运行

    复制代码
    func swap(a, b *int) {
        *a, *b = *b, *a
    }

    优势:无需复制变量值(尤其适合大结构体),直接操作原变量,性能更优。

  5. 问题 :Go 的指针与 C 的指针最主要的区别是什么? 答案

    • 安全性:Go 禁止指针运算,无悬垂指针(GC 管理);C 允许指针运算,需手动管理内存,易产生悬垂指针。

    • 功能:Go 指针仅支持取地址(&)和解引用(*);C 指针支持算术运算、转换为整数等。

    • 类型严格性:Go 指针类型严格(*int*float64不可隐式转换);C 指针转换更灵活(需显式类型转换)。

二、场景题(25 题)

基础应用(1-8 题)
  1. 场景 :编写函数,通过指针交换两个整数的值。 答案

    go

    复制代码
    func swap(a, b *int) {
        *a, *b = *b, *a
    }
    ​
    // 使用:
    // x, y := 10, 20
    // swap(&x, &y) // x=20, y=10
  2. 场景 :定义一个结构体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"
  3. 场景 :创建一个指针数组,存储多个整数的地址,遍历数组并修改每个整数的值。 答案

    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)
    }
  4. 场景 :编写函数,接收*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
  5. 场景 :通过指针实现一个函数,将字符串反转(原地修改,不创建新字符串)。 答案

    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"
  6. 场景 :创建一个数组指针,通过指针修改数组中的元素,并比较数组指针与指针数组的使用差异。 答案

    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
    }
  7. 场景 :编写函数,接收指针类型参数,判断指针是否为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
  8. 场景 :通过指针传递大结构体,比较其与值传递的性能差异(使用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 题)
  1. 场景 :使用双重指针(**int)在函数内部为外部指针分配内存并赋值。 答案

    go

    复制代码
    func allocate(p **int, value int) {
        *p = new(int) // 为外部指针分配内存
        **p = value   // 赋值
    }
    ​
    // 使用:
    // var x *int
    // allocate(&x, 10)
    // fmt.Println(*x) // 10(x已被正确初始化)
  2. 场景 :实现一个函数,接收切片指针,向切片中添加元素并修改切片的长度(对比直接传递切片的区别)。 答案

    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]
  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     // 合法
    }
  4. 场景 :通过指针实现链表的插入操作(在指定节点后插入新节点)。 答案

    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
  5. 场景 :编写函数,接收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
  6. 场景 :使用指针实现结构体的深拷贝(对比浅拷贝)。 答案

    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不受影响
  7. 场景 :通过指针传递实现并发安全的计数器(使用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(并发安全)
  8. 场景 :使用unsafe.Pointerint指针转换为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)的整数表示)
    }
  9. 场景 :编写函数,通过指针判断两个变量是否存储在同一块内存地址。 答案

    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(相同地址)
  10. 场景 :实现一个函数,接收指针类型的参数,返回该指针指向的值的副本(适用于任意类型)。 答案

    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 题)
  1. 场景 :使用指针实现二叉树的前序遍历(递归和非递归方式)。 答案

    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
    }
  2. 场景 :通过指针实现一个简单的内存池(复用对象,减少 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)
    }
  3. 场景 :使用指针和接口实现策略模式(根据不同策略动态切换算法)。 答案

    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)) // 支付宝支付
  4. 场景 :通过指针操作二进制数据(使用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
    }
  5. 场景 :实现一个泛型函数,接收任意类型的指针,将其值设置为该类型的零值。 答案

    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
  6. 场景 :使用指针实现一个单例模式(确保全局只有一个实例)。 答案

    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(同一实例)
  7. 场景 :通过指针和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 中的设计哲学(安全与简洁并重)及最佳实践。

相关推荐
IAR Systems1 小时前
在IAR Embedded Workbench for Arm中实现Infineon TRAVEO™ T2G安全调试
开发语言·arm开发·安全·嵌入式软件开发·iar
jayzhang_1 小时前
SPARK入门
大数据·开发语言
蹦极的考拉1 小时前
网站日志里面老是出现{pboot:if((\x22file_put_co\x22.\x22ntents\x22)(\x22temp.php\x22.....
android·开发语言·php
fured2 小时前
[调试][实现][原理]用Golang实现建议断点调试器
开发语言·后端·golang
大翻哥哥2 小时前
Python地理空间数据分析:从地图绘制到智能城市应用
开发语言·python·数据分析
NPE~2 小时前
[手写系列]Go手写db — — 第二版
开发语言·数据库·golang·教程·db·手写系列
M_Reus_112 小时前
Groovy集合常用简洁语法
java·开发语言·windows
爬虫程序猿3 小时前
利用 Python 爬虫获取 1688 商品详情 API 返回值说明(代码示例)实战指南
开发语言·爬虫·python
明月看潮生4 小时前
编程与数学 02-017 Python 面向对象编程 23课题、测试面向对象的程序
开发语言·python·青少年编程·面向对象·编程与数学