Golang学习历程【第八篇 指针(pointer)】

1. 什么是指针

指针是编程中一个重要的概念,它就像一个"地址标签",存储着内存中某个变量的具体位置信息。通过指针,我们可以直接访问和操作内存中的数据。

1.1 指针的基本概念

想象一下现实生活中的场景:

  • 你的家有一个具体地址(比如:北京市朝阳区xxx街道xxx号)
  • 别人想要找到你家,只需要知道这个地址就可以了
  • 指针就相当于这个"地址",它存储的是变量在内存中的位置

在计算机中:

  • 每个变量都会被分配一块内存空间来存储数据
  • 这块内存空间有一个唯一的地址
  • 指针变量存储的就是这个内存地址

1.2 为什么需要指针

指针主要有以下几个重要作用:

  1. 节省内存:传递大型数据结构时,传递指针比传递整个数据更高效
  2. 直接修改:可以在函数中直接修改原始数据
  3. 动态内存管理:可以动态地分配和释放内存
  4. 数据结构实现:链表、树等复杂数据结构的实现基础

2. 指针的声明和使用

2.1 指针的声明

go 复制代码
// 指针的声明语法:var 指针变量名 *数据类型
var ptr *int     // 声明一个指向int类型的指针
var ptr2 *string // 声明一个指向string类型的指针
var ptr3 *float64 // 声明一个指向float64类型的指针

2.2 获取变量地址(&操作符)

go 复制代码
package main

import "fmt"

func main() {
    // 声明普通变量
    var num int = 100
    var name string = "张三"
    var score float64 = 95.5
    
    // 使用&操作符获取变量的内存地址
    var ptrNum *int = &num
    var ptrName *string = &name
    var ptrScore *float64 = &score
    
    fmt.Printf("变量num的值:%d,地址:%p\n", num, &num)
    fmt.Printf("变量name的值:%s,地址:%p\n", name, &name)
    fmt.Printf("变量score的值:%f,地址:%p\n", score, &score)
    
    fmt.Printf("指针ptrNum存储的地址:%p,指向的值:%d\n", ptrNum, *ptrNum)
    fmt.Printf("指针ptrName存储的地址:%p,指向的值:%s\n", ptrName, *ptrName)
    fmt.Printf("指针ptrScore存储的地址:%p,指向的值:%f\n", ptrScore, *ptrScore)
}

运行结果:

bash 复制代码
变量num的值:100,地址:0xc0000140a8
变量name的值:张三,地址:0xc000010230
变量score的值:95.500000,地址:0xc0000140b8
指针ptrNum存储的地址:0xc0000140a8,指向的值:100
指针ptrName存储的地址:0xc000010230,指向的值:张三
指针ptrScore存储的地址:0xc0000140b8,指向的值:95.500000

2.3 访问指针指向的值(*操作符)

go 复制代码
package main

import "fmt"

func main() {
    var num int = 50
    var ptr *int = &num  // ptr指向num的地址
    
    fmt.Printf("num的值:%d\n", num)
    fmt.Printf("ptr存储的地址:%p\n", ptr)
    fmt.Printf("通过指针访问的值:%d\n", *ptr)  // *ptr表示获取ptr指向地址处的值
    
    // 通过指针修改值
    *ptr = 100
    fmt.Printf("修改后num的值:%d\n", num)  // num的值也被修改了
    fmt.Printf("通过指针访问修改后的值:%d\n", *ptr)
}

运行结果:

bash 复制代码
num的值:50
ptr存储的地址:0xc0000140a8
通过指针访问的值:50
修改后num的值:100
通过指针访问修改后的值:100

3. 指针的零值和nil

3.1 指针的零值

go 复制代码
package main

import "fmt"

func main() {
    // 声明指针变量但不初始化
    var ptr *int
    var ptr2 *string
    
    fmt.Printf("未初始化的int指针:%v,是否为nil:%t\n", ptr, ptr == nil)
    fmt.Printf("未初始化的string指针:%v,是否为nil:%t\n", ptr2, ptr2 == nil)
    
    // 尝试访问未初始化指针的值会导致panic
    // fmt.Println(*ptr)  // 这行代码会报错!
}

运行结果:

bash 复制代码
未初始化的int指针:<nil>,是否为nil:true
未初始化的string指针:<nil>,是否为nil:true

3.2 nil指针的安全检查

go 复制代码
package main

import "fmt"

func main() {
    var ptr *int
    
    // 安全检查
    if ptr != nil {
        fmt.Println("指针不为nil,可以安全访问")
        fmt.Println(*ptr)
    } else {
        fmt.Println("指针为nil,不能访问")
    }
    
    // 初始化指针后
    num := 42
    ptr = &num
    
    if ptr != nil {
        fmt.Println("指针不为nil,可以安全访问")
        fmt.Println("指针指向的值:", *ptr)
    }
}

运行结果:

bash 复制代码
指针为nil,不能访问
指针不为nil,可以安全访问
指针指向的值: 42

4. 指针与函数

4.1 指针作为函数参数

go 复制代码
package main

import "fmt"

// 传值方式 - 不会修改原始值
func changeValueByValue(x int) {
    x = 100
    fmt.Printf("函数内x的值:%d\n", x)
}

// 传指针方式 - 可以修改原始值
func changeValueByPointer(ptr *int) {
    *ptr = 200
    fmt.Printf("函数内通过指针修改的值:%d\n", *ptr)
}

func main() {
    num := 50
    fmt.Printf("调用函数前num的值:%d\n", num)
    
    // 传值调用
    changeValueByValue(num)
    fmt.Printf("传值调用后num的值:%d\n", num)
    
    // 传指针调用
    changeValueByPointer(&num)
    fmt.Printf("传指针调用后num的值:%d\n", num)
}

运行结果:

bash 复制代码
调用函数前num的值:50
函数内x的值:100
传值调用后num的值:50
函数内通过指针修改的值:200
传指针调用后num的值:200

4.2 返回指针的函数

go 复制代码
package main

import "fmt"

// 返回指向int的指针
func createIntPointer(value int) *int {
    return &value
}

// 返回指向结构体的指针
func createStudentPointer(name string, age int) *struct {
    Name string
    Age  int
} {
    student := struct {
        Name string
        Age  int
    }{name, age}
    return &student
}

func main() {
    // 获取指针
    ptr := createIntPointer(999)
    fmt.Printf("指针地址:%p,指向的值:%d\n", ptr, *ptr)
    
    // 修改通过指针访问的值
    *ptr = 888
    fmt.Printf("修改后的值:%d\n", *ptr)
    
    // 结构体指针
    studentPtr := createStudentPointer("李四", 20)
    fmt.Printf("学生信息:%s,%d岁\n", (*studentPtr).Name, (*studentPtr).Age)
    
    // Go语言提供了简化语法
    fmt.Printf("学生信息简化写法:%s,%d岁\n", studentPtr.Name, studentPtr.Age)
}

运行结果:

bash 复制代码
指针地址:0xc0000140a8,指向的值:999
修改后的值:888
学生信息:李四,20岁
学生信息简化写法:李四,20岁

5. 指针数组和数组指针

5.1 指针数组

go 复制代码
package main

import "fmt"

func main() {
    // 创建几个变量
    a, b, c := 1, 2, 3
    
    // 指针数组 - 数组的每个元素都是指针
    ptrArray := [3]*int{&a, &b, &c}
    
    fmt.Println("指针数组的内容:")
    for i, ptr := range ptrArray {
        fmt.Printf("索引%d:地址%p,值%d\n", i, ptr, *ptr)
    }
    
    // 通过指针数组修改原变量
    *ptrArray[0] = 10
    *ptrArray[1] = 20
    *ptrArray[2] = 30
    
    fmt.Printf("修改后:a=%d, b=%d, c=%d\n", a, b, c)
}

运行结果:

bash 复制代码
指针数组的内容:
索引0:地址0xc0000140a8,值1
索引1:地址0xc0000140b0,值2
索引2:地址0xc0000140b8,值3
修改后:a=10, b=20, c=30

5.2 数组指针

go 复制代码
package main

import "fmt"

func main() {
    // 普通数组
    arr := [3]int{10, 20, 30}
    
    // 数组指针 - 指向整个数组的指针
    arrPtr := &arr
    
    fmt.Printf("数组地址:%p\n", &arr)
    fmt.Printf("数组指针存储的地址:%p\n", arrPtr)
    fmt.Printf("通过数组指针访问元素:%d, %d, %d\n", 
               (*arrPtr)[0], (*arrPtr)[1], (*arrPtr)[2])
    
    // 修改数组元素
    (*arrPtr)[0] = 100
    (*arrPtr)[1] = 200
    (*arrPtr)[2] = 300
    
    fmt.Printf("修改后数组:%v\n", arr)
    
    // Go语言的简化写法
    arrPtr[0] = 1000  // 等价于 (*arrPtr)[0] = 1000
    fmt.Printf("简化写法修改后:%v\n", arr)
}

运行结果:

bash 复制代码
数组地址:0xc000016060
数组指针存储的地址:0xc000016060
通过数组指针访问元素:10, 20, 30
修改后数组:[100 200 300]
简化写法修改后:[1000 200 300]

6. 指针的指针(多级指针)

go 复制代码
package main

import "fmt"

func main() {
    num := 42
    
    // 一级指针
    ptr1 := &num
    fmt.Printf("num值:%d,地址:%p\n", num, &num)
    fmt.Printf("一级指针ptr1:%p,指向的值:%d\n", ptr1, *ptr1)
    
    // 二级指针(指向指针的指针)
    ptr2 := &ptr1
    fmt.Printf("二级指针ptr2:%p,指向的地址:%p,最终值:%d\n", 
               ptr2, *ptr2, **ptr2)
    
    // 三级指针
    ptr3 := &ptr2
    fmt.Printf("三级指针ptr3:%p,最终值:%d\n", ptr3, ***ptr3)
    
    // 通过多级指针修改值
    ***ptr3 = 999
    fmt.Printf("通过三级指针修改后num的值:%d\n", num)
}

运行结果:

bash 复制代码
num值:42,地址:0xc0000140a8
一级指针ptr1:0xc0000140a8,指向的值:42
二级指针ptr2:0xc000006028,指向的地址:0xc0000140a8,最终值:42
三级指针ptr3:0xc000006038,最终值:42
通过三级指针修改后num的值:999

7. 指针的实际应用示例

7.1 交换两个数的值

go 复制代码
package main

import "fmt"

// 使用指针交换两个数
func swap(a, b *int) {
    temp := *a
    *a = *b
    *b = temp
}

func main() {
    x, y := 10, 20
    fmt.Printf("交换前:x=%d, y=%d\n", x, y)
    
    swap(&x, &y)
    fmt.Printf("交换后:x=%d, y=%d\n", x, y)
}

运行结果:

bash 复制代码
交换前:x=10, y=20
交换后:x=20, y=10

7.2 链表节点示例

go 复制代码
package main

import "fmt"

// 定义链表节点结构
type ListNode struct {
    Value int
    Next  *ListNode
}

// 创建新节点
func NewNode(value int) *ListNode {
    return &ListNode{Value: value, Next: nil}
}

// 在链表末尾添加节点
func AppendNode(head **ListNode, value int) {
    newNode := NewNode(value)
    
    if *head == nil {
        *head = newNode
        return
    }
    
    current := *head
    for current.Next != nil {
        current = current.Next
    }
    current.Next = newNode
}

// 打印链表
func PrintList(head *ListNode) {
    current := head
    for current != nil {
        fmt.Printf("%d -> ", current.Value)
        current = current.Next
    }
    fmt.Println("nil")
}

func main() {
    var head *ListNode = nil
    
    // 添加节点
    AppendNode(&head, 1)
    AppendNode(&head, 2)
    AppendNode(&head, 3)
    AppendNode(&head, 4)
    
    fmt.Println("链表内容:")
    PrintList(head)
}

运行结果:

bash 复制代码
链表内容:
1 -> 2 -> 3 -> 4 -> nil

8. 指针注意事项和最佳实践

8.1 注意事项

  1. 空指针访问危险:访问nil指针会导致程序崩溃
  2. 野指针问题:指向已释放内存的指针
  3. 内存泄漏:忘记释放动态分配的内存

8.2 最佳实践

go 复制代码
package main

import "fmt"

func main() {
    // 1. 始终检查指针是否为nil
    var ptr *int
    if ptr != nil {
        fmt.Println(*ptr) // 安全访问
    }
    
    // 2. 初始化指针
    num := 100
    ptr = &num
    fmt.Printf("安全访问:%d\n", *ptr)
    
    // 3. 使用指针时要考虑生命周期
    data := make([]int, 5)
    ptrToSlice := &data
    fmt.Printf("切片指针:%v\n", *ptrToSlice)
    
    // 4. 函数返回指针时要注意
    func returnPointer() *int {
        value := 42  // 局部变量
        return &value // 危险!返回局部变量的地址
    }
    
    // 正确的做法
    func returnPointerSafe() *int {
        value := new(int) // 在堆上分配内存
        *value = 42
        return value
    }
}

9. new函数和make函数的区别

9.1 new函数

go 复制代码
package main

import "fmt"

func main() {
    // new函数为指定类型分配零值内存并返回指针
    ptr1 := new(int)        // *int类型,值为0
    ptr2 := new(string)     // *string类型,值为""
    ptr3 := new([3]int)     // *[3]int类型,值为[0,0,0]
    
    fmt.Printf("int指针:%p,值:%d\n", ptr1, *ptr1)
    fmt.Printf("string指针:%p,值:%s\n", ptr2, *ptr2)
    fmt.Printf("数组指针:%p,值:%v\n", ptr3, *ptr3)
    
    // 修改值
    *ptr1 = 100
    *ptr2 = "Hello"
    (*ptr3)[0] = 1
    (*ptr3)[1] = 2
    (*ptr3)[2] = 3
    
    fmt.Printf("修改后int值:%d\n", *ptr1)
    fmt.Printf("修改后string值:%s\n", *ptr2)
    fmt.Printf("修改后数组值:%v\n", *ptr3)
}

运行结果:

bash 复制代码
int指针:0xc0000140a8,值:0
string指针:0xc000010230,值:
数组指针:0xc000016060,值:[0 0 0]
修改后int值:100
修改后string值:Hello
修改后数组值:[1 2 3]

9.2 make函数

go 复制代码
package main

import "fmt"

func main() {
    // make函数用于创建切片、map、channel
    slice := make([]int, 5)        // 创建长度为5的切片
    mapData := make(map[string]int) // 创建map
    ch := make(chan int)           // 创建channel
    
    fmt.Printf("切片:%v,长度:%d\n", slice, len(slice))
    fmt.Printf("map:%v\n", mapData)
    fmt.Printf("channel:%v\n", ch)
    
    // 初始化数据
    slice[0] = 10
    slice[1] = 20
    mapData["one"] = 1
    mapData["two"] = 2
    
    fmt.Printf("初始化后切片:%v\n", slice)
    fmt.Printf("初始化后map:%v\n", mapData)
}

10. 总结

指针是Golang中一个强大但需要谨慎使用的特性:

核心概念

  • 指针存储的是内存地址,而不是具体的值
  • 使用&获取变量地址,使用*访问指针指向的值
  • 指针的零值是nil

主要用途

  1. 函数间共享和修改数据
  2. 避免大数据结构的拷贝开销
  3. 实现复杂数据结构(链表、树等)
  4. 动态内存管理

注意事项

  • 始终检查指针是否为nil
  • 注意变量的生命周期
  • 避免悬空指针和内存泄漏

掌握指针对于深入理解Golang和编写高效的程序非常重要,虽然初学时可能会觉得复杂,但通过大量练习就能熟练运用。


上一篇:Golang学习历程【第七篇 闭包&type defer panic recover了解&time包】

下一篇:Golang学习历程【第九篇 结构体(struct)】

相关推荐
老毛肚6 小时前
jeecg-boot-base-core 02 day
javascript·python
袁小皮皮不皮10 小时前
1.HCIP BFD 学习笔记(优化版)
服务器·网络·笔记·网络协议·学习·智能路由器·ip
装不满的克莱因瓶10 小时前
【自动驾驶领域】学习 Cityscapes 数据集——城市街景语义理解的标准基准
人工智能·pytorch·python·深度学习·学习·机器学习·自动驾驶
清辞85311 小时前
产品经理需求推进流程
大数据·深度学习·学习·产品经理
烬羽11 小时前
后端返回的 JSON 字符串,浏览器怎么"看懂"的?——Ajax 全链路拆解
javascript
YM52e12 小时前
鸿蒙PC ArkTS 声明合并问题深度解析与最佳实践
学习·华为·harmonyos·鸿蒙·鸿蒙系统
半个落月12 小时前
一个新手用 Bun + Axios 调通 DeepSeek API 的实践记录
javascript
不好听61312 小时前
深入理解链表:线性数据结构的另一面
javascript·数据结构
林希_Rachel_傻希希12 小时前
学React治好了我的焦虑症,1小时速通React 前20分钟。
前端·javascript·面试
小林ixn12 小时前
从 Ajax 到异步编程:JSON 序列化、Event Loop 与 XHR 请求完全解析
javascript