目录

Go 指针

指针是编程语言中用于存储变量内存地址的特殊类型。Go 提供了一定程度的指针支持,既保留了直接操作内存地址的能力,又通过安全机制(如没有指针运算、nil 检查)避免了 C 语言中指针的常见陷阱。本篇将详细讲解 Go 指针的概念和使用方法。

https://img.zhaojq.top/20260729161534062.png
指针与内存

什么是指针

在计算机内存中,每个变量都存储在一个特定的地址上。指针就是存储这个地址的变量。

可以把内存想象成一排带编号的储物柜:

  • 变量是柜子里存放的物品。
  • 地址是柜子的编号。
  • 指针就是一张写着柜子编号的纸条,通过它可以找到对应的物品。
package main

import "fmt"

func main() {
    x := 42
    fmt.Println("x 的值:", x)          // 42
    fmt.Println("x 的地址:", &x)       // 0xc0000b4008(具体地址每次运行不同)
}

& 取地址运算符

& 运算符用于获取变量的内存地址,结果是一个指针:

package main

import "fmt"

func main() {
    name := "Go语言"
    age := 15
    pi := 3.14

    fmt.Printf("name 的地址: %p\n", &name)
    fmt.Printf("age 的地址:  %p\n", &age)
    fmt.Printf("pi 的地址:   %p\n", &pi)

    // 将地址保存到指针变量中
    ptr := &age
    fmt.Printf("ptr 的值(即 age 的地址): %p\n", ptr)
    fmt.Printf("ptr 的类型: %T\n", ptr) // *int
}

* 解引用运算符

* 运算符用于通过指针访问指针所指向的值,称为"解引用"或"间接引用":

package main

import "fmt"

func main() {
    x := 100
    ptr := &x // ptr 指向 x

    // 通过指针读取值
    fmt.Println("ptr 指向的值:", *ptr) // 100

    // 通过指针修改值
    *ptr = 200
    fmt.Println("修改后 x 的值:", x) // 200
}

指针类型

Go 中指针的类型用 *T 表示,其中 T 是指针指向的类型:

package main

import "fmt"

func main() {
    var a int = 10
    var b float64 = 3.14
    var c string = "Hello"

    var pa *int = &a
    var pb *float64 = &b
    var pc *string = &c

    fmt.Printf("pa 类型: %T, 值: %v\n", pa, *pa) // *int, 10
    fmt.Printf("pb 类型: %T, 值: %v\n", pb, *pb) // *float64, 3.14
    fmt.Printf("pc 类型: %T, 值: %v\n", pc, *pc) // *string, Hello
}

注意:不同类型的指针之间不能互相赋值,*int*float64 是完全不同的类型。

值传递 vs 引用传递

值传递(默认行为)

Go 中函数参数默认是值传递,即传递的是值的副本,修改参数不会影响原始变量:

package main

import "fmt"

func increment(n int) {
    n++ // 修改的是副本
    fmt.Println("函数内 n:", n)
}

func main() {
    x := 10
    increment(x)
    fmt.Println("main 中 x:", x) // 仍然是 10
}

使用指针实现"引用传递"

通过传递指针,函数可以修改原始变量:

package main

import "fmt"

func increment(n *int) {
    *n++ // 修改指针指向的值
    fmt.Println("函数内 *n:", *n)
}

func main() {
    x := 10
    increment(&x)          // 传递 x 的地址
    fmt.Println("main 中 x:", x) // 11
}

对比示例

package main

import "fmt"

// 值传递:交换失败
func swapByValue(a, b int) {
    a, b = b, a
    fmt.Printf("swapByValue 内: a=%d, b=%d\n", a, b)
}

// 指针传递:交换成功
func swapByPointer(a, b *int) {
    *a, *b = *b, *a
    fmt.Printf("swapByPointer 内: *a=%d, *b=%d\n", *a, *b)
}

func main() {
    x, y := 1, 2

    swapByValue(x, y)
    fmt.Printf("swapByValue 后: x=%d, y=%d\n", x, y) // 1, 2(未变)

    swapByPointer(&x, &y)
    fmt.Printf("swapByPointer 后: x=%d, y=%d\n", x, y) // 2, 1(已交换)
}

指针作为函数参数

指针参数在实际开发中非常常见,主要用于以下场景:

避免大结构体的拷贝

package main

import "fmt"

type User struct {
    Name  string
    Age   int
    Email string
}

// 使用指针避免拷贝整个结构体
func printUser(u *User) {
    fmt.Printf("姓名: %s, 年龄: %d, 邮箱: %s\n", u.Name, u.Age, u.Email)
}

func main() {
    user := User{
        Name:  "张三",
        Age:   25,
        Email: "zhangsan@example.com",
    }
    printUser(&user)
}

修改函数外部的数据

package main

import "fmt"

func setName(u *User, name string) {
    u.Name = name // 等价于 (*u).Name = name
}

func main() {
    user := User{Name: "张三", Age: 25}
    setName(&user, "李四")
    fmt.Println("修改后的名字:", user.Name) // 李四
}

new 函数

new(T) 分配一个 T 类型的零值内存,并返回指向它的指针:

package main

import "fmt"

func main() {
    // new(int) 分配一个 int 的内存,初始化为 0,返回 *int
    p := new(int)
    fmt.Printf("类型: %T, 值: %d\n", p, *p) // *int, 0

    *p = 42
    fmt.Println("赋值后:", *p) // 42

    // 等价于:
    // var temp int  // 零值 0
    // p := &temp
}

提示:从 Go 1.18 开始,可以使用泛型辅助函数 func Ptr[T any](v T) *T { return &v } 来创建任意类型的指针。

nil 指针

零值指针的值为 nil,对 nil 指针解引用会导致程序崩溃(panic):

package main

import "fmt"

func main() {
    var p *int // 零值为 nil
    fmt.Println("p == nil:", p == nil) // true

    // fmt.Println(*p) // panic: runtime error: invalid memory address

    // 正确做法:解引用前先检查
    if p != nil {
        fmt.Println(*p)
    } else {
        fmt.Println("指针为空,无法解引用")
    }
}

指针的常见使用场景

场景一:结构体方法接收者

当方法需要修改结构体的字段时,使用指针接收者:

package main

import "fmt"

type Counter struct {
    count int
}

// 值接收者:不能修改原始结构体
func (c Counter) GetCount() int {
    return c.count
}

// 指针接收者:可以修改原始结构体
func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) Reset() {
    c.count = 0
}

func main() {
    counter := Counter{}

    counter.Increment()
    counter.Increment()
    counter.Increment()

    fmt.Println("计数:", counter.GetCount()) // 3

    counter.Reset()
    fmt.Println("重置后:", counter.GetCount()) // 0
}

场景二:链表节点

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func main() {
    // 创建链表: 1 -> 2 -> 3
    head := &Node{Value: 1}
    head.Next = &Node{Value: 2}
    head.Next.Next = &Node{Value: 3}

    // 遍历链表
    current := head
    for current != nil {
        fmt.Printf("%d -> ", current.Value)
        current = current.Next
    }
    fmt.Println("nil")
}

场景三:可选字段

在结构体中,指针类型字段可以表示"有值"或"无值"(nil):

package main

import "fmt"

type Profile struct {
    Name    string
    Age     int
    Website *string // 可选字段,nil 表示未填写
}

func main() {
    // 没有网站
    p1 := Profile{Name: "张三", Age: 25}
    printProfile(p1)

    // 有网站
    site := "https://example.com"
    p2 := Profile{Name: "李四", Age: 30, Website: &site}
    printProfile(p2)
}

func printProfile(p Profile) {
    fmt.Printf("姓名: %s, 年龄: %d", p.Name, p.Age)
    if p.Website != nil {
        fmt.Printf(", 网站: %s", *p.Website)
    }
    fmt.Println()
}

总结

本篇详细讲解了 Go 指针的核心概念和使用方法。指针存储变量的内存地址,& 取地址、* 解引用是两个基本操作。Go 的值传递默认传递副本,通过指针可以实现"引用传递"效果。new 函数用于分配零值内存,nil 指针需要在解引用前检查。在实际开发中,指针常用于:避免大结构体拷贝、修改函数外部数据、结构体方法接收者、链表等数据结构、以及表示可选字段。Go 的指针设计在灵活性和安全性之间取得了良好的平衡。