四、go语言指针

一,指针基础概念

  • Go编程语言中的指针是一个变量 ,用于存储另一个变量的内存地址

  • Golang中的指针也称为特殊变量

  • 变量被用于一些数据在系统中的一个特定存储器地址存储 。始终以十六进制格式找到内存地址(以0x开头,如0xFFAAF等)。

二,基本操作

2.1 基本声明与初始化

go 复制代码
var a int = 42
var p *int = &a  // p 是指向 a 的指针
  • & 操作符:取变量地址(&a 返回 a 的内存地址)。
  • * 操作符:声明指针类型(*int)或解引用指针(*p 访问指向的值)。

2.2 指针的解引用

go 复制代码
fmt.Println(*p) // 输出 42(通过指针访问值)
*p = 100       // 通过指针修改值
fmt.Println(a) // 输出 100(原变量被修改)
  • 解引用指针(*p)直接操作其指向的内存数据。

2.3 指针的零值

go 复制代码
var p *int
fmt.Println(p) // 输出 <nil>(指针零值)
  • 未初始化的指针值为 nil,解引用 nil 指针会导致运行时 panic

2.4 new 函数动态分配内存

go 复制代码
ptr := new(int) // 分配 int 类型零值内存,返回指针
*ptr = 30       // 安全赋值
  • new(T) 分配类型 T 的零值内存,并返回 *T 类型指针。

三、指针与结构体

3.1 隐式解引用

go 复制代码
type User struct {
    Name string
    Age  int
}

u := User{"Alice", 25}
p := &u
fmt.Println(p.Name)    // 隐式解引用:输出 "Alice"
fmt.Println((*p).Age) // 显式解引用:输出 25
  • 结构体指针可直接访问字段(语法糖),无需显式写 (*p).Field

3.2 方法接收者

指针接收者允许方法修改原结构体:

go 复制代码
func (p *Point) Move(dx, dy int) {
    p.X += dx
    p.Y += dy
}

四,指针与函数

4.1 修改外部变量

go 复制代码
func increment(n *int) {
    *n++ // 修改外部变量
}

num := 10
increment(&num)
fmt.Println(num) // 输出 11

4.2 避免大结构体复制

go 复制代码
type LargeStruct struct { data [1024]byte }

func process(s *LargeStruct) { /* 避免复制开销 */ }

4.3 函数返回指针

go 复制代码
//Go 函数返回指针
package main

import "fmt"

func main() {
    //调用函数
    n := rpf()
    //显示值
    fmt.Println("n的值: ", *n)
}

//定义具有整数的函数
//指针作为返回类型
func rpf() *int {
    //局部变量
    //函数内部使用简短运算符声明
    lv := 100
    // 返回lv的地址
    return &lv
}

五,二级指针

5.1 基本概念

在Go中:

  • 一级指针 :存储变量的内存地址(如 var p *int
  • 二级指针 :存储指针变量的内存地址(如 var pp **int
go 复制代码
package main

import "fmt"

func main() {
    a := 10
    p := &a       // 一级指针,指向a
    pp := &p      // 二级指针,指向p
    
    fmt.Println("a:", a)        // 10
    fmt.Println("*p:", *p)      // 10
    fmt.Println("**pp:", **pp)  // 10
}

内存关系:

css 复制代码
pp → p → a

5.2 应用场景

5.2.1 在函数中修改指针指向

当需要修改指针本身的指向时(而不仅仅是修改指针指向的值),需要使用二级指针:

go 复制代码
func modifyPointer(pp **int) {
    b := 20
    *pp = &b // 修改外部指针的指向
    
    // 注意:b是局部变量,返回后会失效
    // 实际应用中应使用堆分配
}

func safeModifyPointer(pp **int) {
    // 更安全的做法:使用堆分配
    b := new(int)
    *b = 30
    *pp = b
}

func main() {
    a := 10
    p := &a
    
    // 错误示例:无法修改指针指向
    // unsafeModifyPointer(&p)
    // fmt.Println(*p) // 结果不确定,可能崩溃
    
    safeModifyPointer(&p)
    fmt.Println(*p) // 30
}

5.2.2 动态数据结构操作

在链表、树等数据结构中,二级指针可以简化头节点/根节点的操作:

go 复制代码
type Node struct {
    Value int
    Next  *Node
}

// 使用二级指针在链表头部插入节点
func insertAtBeginning(pp **Node, value int) {
    newNode := &Node{Value: value}
    newNode.Next = *pp
    *pp = newNode
}

func main() {
    var head *Node // 空链表
    
    insertAtBeginning(&head, 3)
    insertAtBeginning(&head, 2)
    insertAtBeginning(&head, 1)
    
    // 链表:1 -> 2 -> 3
}

5.2.3 接口指针的修改

当需要修改接口变量时(接口本身包含类型和值指针),可以使用指向接口的指针:

go 复制代码
type Writer interface {
    Write([]byte) (int, error)
}

func replaceWriter(wp *Writer) {
    *wp = new(bytes.Buffer)
}

func main() {
    var w Writer
    replaceWriter(&w)
    // w现在指向bytes.Buffer
}

5.3 使用注意事项

  1. 避免返回局部变量地址

    go 复制代码
    func badExample() **int {
        var p *int
        pp := &p
        return pp // 安全,但p是nil
    }
  2. nil指针处理

    go 复制代码
    var pp **int
    // **pp // 会导致panic
    
    if pp != nil && *pp != nil {
        fmt.Println(**pp)
    }
  3. 在反射中的使用

    go 复制代码
    func setValue(ptr interface{}) {
        v := reflect.ValueOf(ptr).Elem()
        v.SetInt(42)
    }
    
    func main() {
        var x int
        setValue(&x) // 实际上使用了"指针的指针"概念
        fmt.Println(x) // 42
    }

六,指针与数组

在Go中,数组是值类型,当数组作为参数传递时会发生拷贝。使用指针可以避免这种拷贝。

go 复制代码
package main

import "fmt"

func main() {
    // 定义数组
    arr := [3]int{1, 2, 3}
    
    // 获取数组指针
    pArr := &arr
    
    // 通过指针访问数组元素
    fmt.Println((*pArr)[0]) // 1
    fmt.Println(pArr[0])    // 1 (Go语法糖,等价于上一行)
    
    // 通过指针修改数组元素
    pArr[1] = 20
    fmt.Println(arr) // [1 20 3]
    
    // 数组指针作为函数参数
    modifyArray(pArr)
    fmt.Println(arr) // [100 20 3]
}

func modifyArray(p *[3]int) {
    (*p)[0] = 100
    // 或者 p[0] = 100 (语法糖)
}

注意事项

  • 数组指针的类型是 *[n]T,其中n是数组长度
  • Go提供了语法糖,可以直接用指针变量名访问元素,无需显式解引用
  • 数组长度是指针类型的一部分,*[3]int*[4]int 是不同的类型

七,指针的比较

7.1 基本指针比较

相等性比较

在Go中,可以使用 ==!= 运算符来比较两个指针是否指向同一个内存地址:

go 复制代码
package main

import "fmt"

func main() {
    a := 10
    b := 10
    p1 := &a
    p2 := &a
    p3 := &b
    
    fmt.Println(p1 == p2) // true,指向同一个变量
    fmt.Println(p1 == p3) // false,指向不同变量
    fmt.Println(p1 != p3) // true
}

nil比较

指针可以与nil比较,检查是否为空指针:

go 复制代码
var p *int
fmt.Println(p == nil) // true

p = new(int)
fmt.Println(p == nil) // false

7.2 不同类型指针的比较

Go是强类型语言,不同类型的指针不能直接比较:

go 复制代码
var x int = 10
var y float64 = 10.0
px := &x
py := &y

// fmt.Println(px == py) // 编译错误:类型不匹配

特殊情况:unsafe.Pointer

使用unsafe.Pointer可以绕过类型系统进行比较:

go 复制代码
import "unsafe"

pxUnsafe := unsafe.Pointer(px)
pyUnsafe := unsafe.Pointer(py)
fmt.Println(pxUnsafe == pyUnsafe) // 可以比较,但通常为false

7.3 切片、映射和函数的指针比较

这些引用类型的比较有特殊规则:

7.3.1 切片比较

切片只能与nil比较,不能直接比较两个切片:

go 复制代码
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}

fmt.Println(s1 == nil) // false
// fmt.Println(s1 == s2) // 编译错误:切片只能与nil比较

7.3.2 映射比较

映射同样只能与nil比较:

go 复制代码
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}

fmt.Println(m1 == nil) // false
// fmt.Println(m1 == m2) // 编译错误:映射只能与nil比较

7.3.3 函数比较

函数也只能与nil比较:

go 复制代码
f1 := func() {}
f2 := func() {}

fmt.Println(f1 == nil) // false
// fmt.Println(f1 == f2) // 编译错误:函数只能与nil比较

7.4 结构体指针的比较

结构体指针的比较遵循基本指针比较规则:

go 复制代码
type Person struct {
    Name string
    Age  int
}

p1 := &Person{"Alice", 30}
p2 := p1
p3 := &Person{"Alice", 30}

fmt.Println(p1 == p2) // true,相同指针
fmt.Println(p1 == p3) // false,不同指针(即使内容相同)

八,其他

指针的限制

  • 不支持指针算术 (如 ptr++): Go 禁止直接操作内存地址,避免越界错误。
  • 内存安全: 垃圾回收机制自动管理内存,避免野指针。

指针 vs 值

场景 值传递 指针传递
修改变量 无法修改原值 可修改原值
大对象性能 复制开销大 避免复制(仅传地址)
并发安全 天然安全(副本隔离) 需同步机制(如互斥锁)
零值可用性 直接使用 需检查 nil

使用事项

  1. 必要场景使用指针
    • 需要修改接收者状态时(方法接收者)
    • 传递大结构体/数组
    • 实现接口时动态修改对象
  2. 避免过度使用
    • 小对象(如基本类型)直接传值更高效
    • 减少指针链(如 **int)以保持代码清晰
  3. 始终检查 nil 指针: 避免运行时 panic。
相关推荐
码小凡5 小时前
优雅!用了这两款插件,我成了整个公司代码写得最规范的码农
java·后端
星星电灯猴5 小时前
Charles抓包工具深度解析:如何高效调试HTTPHTTPS请求与API接口
后端
isfox5 小时前
Hadoop 版本进化论:从 1.0 到 2.0,架构革命全解析
大数据·后端
yeyong6 小时前
用springboot开发一个snmp采集程序,并最终生成拓扑图 (二)
后端
掉鱼的猫7 小时前
Solon AI 五步构建 RAG 服务:2025 最新 AI + 向量数据库实战
java·redis·后端
HyggeBest7 小时前
Mysql之undo log、redo log、binlog日志篇
后端·mysql
java金融7 小时前
FactoryBean 和BeanFactory的傻傻的总是分不清?
java·后端
独立开阀者_FwtCoder7 小时前
Nginx 部署负载均衡服务全解析
前端·javascript·后端