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)】

相关推荐
极客小云2 小时前
【基于AI的自动商品试用系统:不仅仅是虚拟试衣!】
javascript·python·django·flask·github·pyqt·fastapi
丝斯20112 小时前
AI学习笔记整理(69)——物理AI中世界模型
人工智能·笔记·学习
一位搞嵌入式的 genius2 小时前
深入理解浏览器中的 JavaScript:BOM、DOM、网络与性能优化
前端·javascript·网络·性能优化
li星野2 小时前
若依初体验
学习
David凉宸2 小时前
Vue 3生态系统深度解析与最佳实践
前端·javascript·vue.js
凯尔萨厮2 小时前
软件23种设计模式(学习笔记)
笔记·学习·设计模式
近津薪荼2 小时前
递归专题5——快速幂
c++·学习·算法
Highcharts.js2 小时前
用 Highcharts如何创建一个音频图表
javascript·信息可视化·音视频·highcharts·音频图表
小二·2 小时前
Go 语言系统编程与云原生开发实战(第11篇)微服务治理实战:服务注册发现 × 负载均衡 × 全链路压测(生产级落地)
微服务·云原生·golang