Skip to content

1.8 数据类型:指针

指针(Pointer)是 Go 语言中的重要概念,它存储变量的内存地址而非变量值本身。指针使得程序能够间接访问和修改内存中的数据,是实现高效内存管理、避免数据拷贝、实现复杂数据结构的基础。

理解指针对于掌握 Go 语言至关重要,它不仅影响程序性能,还是理解方法接收者、接口实现等高级特性的前提。

一、指针的基础概念

1. 什么是指针?

  • 指针 (Pointer) 是一种特殊的变量,它存储了另一个变量在内存中的地址
  • 这个“另一个变量”就是指针所指向的目标。

2. Go 语言中的指针操作符

Go 语言主要使用两个操作符来操作指针:

操作符名称作用示例
&取地址符 (Address-of)获取一个变量的内存地址。p := &v
*取值符 (Dereference)根据地址获取该地址存储的v := *p

二、指针的定义与使用

1. 指针变量的定义

指针变量的类型由它所指向的变量类型决定,语法为 *T,其中 T 是目标变量的类型。

go
var a int = 10 
var p *int // 定义一个指向 int 类型的指针变量 p

2. 基本操作步骤

步骤代码示例说明
获取地址p = &a将变量 a 的内存地址赋给指针 pp 的值就是 a 的地址。
通过地址取值b := *p通过指针 p 存储的地址,取出该地址处存储的值(即 a 的值)。此时 b 等于 10。
修改指向的值*p = 20通过指针 p 修改它所指向的变量 a 的值。此时 a 变为 20。

通过下面这段代码,你可以熟悉这两个符号的用法

go
package main

import "fmt"

func main() {
	aint := 1     // 定义普通变量
	ptr := &aint  // 定义指针变量
	fmt.Println("普通变量存储的是:", aint)
	fmt.Println("普通变量存储的是:", *ptr)
	fmt.Println("指针变量存储的是:", &aint)
	fmt.Println("指针变量存储的是:", ptr)
}

输出如下

go
普通变量存储的是: 1
普通变量存储的是: 1
指针变量存储的是: 0xc0000100a0
指针变量存储的是: 0xc0000100a0

要想打印指针指向的内存地址,方法有两种

go
// 第一种
fmt.Printf("%p", ptr)
// 第二种
fmt.Println(ptr)

3. 指针的类型

指针也是有类型的,一个指针只能指向特定类型的变量。指针类型的格式为 *T,其中 T 是指针所指向的数据类型。

go
package main

import "fmt"

func main() {
    // 不同类型的指针
    var intPtr *int        // 指向 int 类型的指针
    var strPtr *string     // 指向 string 类型的指针
    var boolPtr *bool      // 指向 bool 类型的指针
    var floatPtr *float64  // 指向 float64 类型的指针
    
    // 为指针赋值
    num := 42
    text := "Hello"
    flag := true
    price := 99.99
    
    intPtr = &num
    strPtr = &text
    boolPtr = &flag
    floatPtr = &price
    
    // 打印指针类型和值
    fmt.Printf("intPtr 类型: %T, 值: %p, 指向的值: %d\n", intPtr, intPtr, *intPtr)
    fmt.Printf("strPtr 类型: %T, 值: %p, 指向的值: %s\n", strPtr, strPtr, *strPtr)
    fmt.Printf("boolPtr 类型: %T, 值: %p, 指向的值: %t\n", boolPtr, boolPtr, *boolPtr)
    fmt.Printf("floatPtr 类型: %T, 值: %p, 指向的值: %.2f\n", floatPtr, floatPtr, *floatPtr)
}

输出结果:

intPtr 类型: *int, 值: 0xc000010098, 指向的值: 42
strPtr 类型: *string, 值: 0xc000010110, 指向的值: Hello
boolPtr 类型: *bool, 值: 0xc000010118, 指向的值: true
floatPtr 类型: *float64, 值: 0xc000010120, 指向的值: 99.99

指针类型的重要特性:

  1. 类型安全:不同类型的指针不能相互赋值

    go
    var intPtr *int
    var strPtr *string
    
    // intPtr = strPtr  // 编译错误:类型不匹配
  2. 自定义类型的指针

    go
    type Person struct {
        Name string
        Age  int
    }
    
    var personPtr *Person  // 指向 Person 结构体的指针
    person := Person{Name: "Alice", Age: 30}
    personPtr = &person
    
    fmt.Printf("Person指针类型: %T\n", personPtr)  // *main.Person
    fmt.Printf("Person信息: %+v\n", *personPtr)   // {Name:Alice Age:30}
  3. 指向指针的指针(多级指针):

    go
    num := 100
    ptr := &num      // *int 类型
    ptrPtr := &ptr   // **int 类型(指向指针的指针)
    
    fmt.Printf("num = %d\n", num)           // 100
    fmt.Printf("*ptr = %d\n", *ptr)         // 100
    fmt.Printf("**ptrPtr = %d\n", **ptrPtr) // 100
    
    fmt.Printf("ptr类型: %T\n", ptr)         // *int
    fmt.Printf("ptrPtr类型: %T\n", ptrPtr)   // **int

4. 指针的零值

指针的零值是 nil

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 当一个指针为 nil 时,表示它不指向任何有效的内存地址
  • nil 指针进行取值操作(解引用 *p)会引发程序运行时错误(panic)。
go
package main

import "fmt"

func main() {
    var p *string
    fmt.Println(p)
    fmt.Printf("p的值是%s/n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

三、new() 函数的使用

Go 语言提供了一个内置函数 new(),用于为 基本类型或复合类型(如结构体) 分配内存。

语法new(T)
作用为类型 T 分配内存,将其初始化为该类型对应的零值,并返回该内存的地址(即 *T 类型的指针)。
示例p := new(int)

& 操作符的区别:

  • & 操作符是获取一个已存在变量的地址。
  • new() 函数是新分配一块内存空间,并返回其地址。
go
// 方式一:使用 & (常用)
var v int = 10
p1 := &v 

// 方式二:使用 new()
p2 := new(int) // *p2 初始值为 0
*p2 = 5        // 通过指针赋值

四、指针在函数中的应用

指针在函数调用中尤其重要,用于实现对外部变量的 “传引用” 效果,从而实现对外部状态的修改,避免大型数据结构的值拷贝。

1. 传值 vs 传指针

方式函数参数效果目的
传值 (Value)func add(x int)函数内部操作的是参数的副本,不会影响外部变量。保护外部变量不被修改。
传指针 (Pointer)func modify(p *int)函数内部通过指针可以直接修改外部变量的值修改外部状态;避免大型数据拷贝。

2. 传指针示例

go
func modifyValue(x *int) {
    *x = *x * 2 // 解引用并修改传入地址的值
}

func main() {
    num := 5
    // 传入 num 的地址
    modifyValue(&num) 
    
    // 此时 num 的值已经被修改为 10
    fmt.Println(num) 
}

3. 指针与切片

切片与指针一样,都是引用类型。如果我们想通过一个函数改变一个数组的值,有两种方法:

  1. 将这个数组的切片做为参数传给函数
  2. 将这个数组的指针做为参数传给函数

尽管二者都可以实现我们的目的,但是按照 Go 语言的使用习惯,建议使用第一种方法,因为第一种方法,写出来的代码会更加简洁,易读。

使用切片

go
func modify(sls []int) {  
	sls[0] = 90
}
	
func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

使用指针

go
func modify(arr *[3]int) {  
	(*arr)[0] = 90
}
	
func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

五、结构体与指针的特殊性

1. 结构体指针访问字段

如果有一个结构体指针,Go 语言允许使用 点号 (.) 直接访问结构体字段,而无需手动解引用。这是 Go 提供的语法糖

go
type Person struct {
    Age int
}

p := new(Person) // p 是 *Person 指针
p.Age = 30       // 语法糖:等价于 (*p).Age = 30

2. 方法中的指针接收者

如结构体教程中所述,方法中使用指针接收者 (p *T) 是 Go 语言实现状态修改和高效编程的核心机制。

go
package main

import "fmt"

type BankAccount struct {
    owner   string
    balance float64
}

// 值接收者:尝试修改(但实际不会生效)
func (acc BankAccount) DepositWrong(amount float64) {
    acc.balance += amount // 只修改副本,原始数据不变
}

// 指针接收者:可以修改原始结构体
func (acc *BankAccount) Deposit(amount float64) {
    acc.balance += amount // 修改原始数据
}

// 指针接收者:返回账户信息
func (acc *BankAccount) String() string {
    return fmt.Sprintf("账户所有者: %s, 余额: %.2f", acc.owner, acc.balance)
}

func main() {
    // 创建银行账户
    account := BankAccount{
        owner:   "张三",
        balance: 1000.0,
    }
    
    fmt.Printf("初始状态: %s\n", account.String())
    
    // 使用值接收者的错误存款方法
    account.DepositWrong(500.0)
    fmt.Printf("错误存款后: %s\n", account.String()) // 余额不变
    
    // 使用指针接收者的正确存款方法
    account.Deposit(500.0)
    fmt.Printf("正确存款后: %s\n", account.String()) // 余额增加
    
    // Go 语言的自动转换特性
    accountPtr := &account
    accountPtr.Deposit(100.0)                              // 指针调用指针方法
    fmt.Printf("最终状态: %s\n", accountPtr.String())
}

输出结果:

初始状态: 账户所有者: 张三, 余额: 1000.00
错误存款后: 账户所有者: 张三, 余额: 1000.00
正确存款后: 账户所有者: 张三, 余额: 1500.00
取款成功: 账户所有者: 张三, 余额: 1300.00
余额不足,取款失败: 账户所有者: 张三, 余额: 1300.00
当前余额: 1300.00
最终状态: 账户所有者: 张三, 余额: 1400.00

关键要点:

  1. 值接收者 (BankAccount):接收结构体的副本,无法修改原始数据
  2. 指针接收者 (*BankAccount):接收结构体的指针,可以修改原始数据
  3. 自动转换:Go 会自动在值和指针之间进行转换,使调用更加便捷

六、使用指针的注意事项

  1. 避免悬空指针 (Dangling Pointer): 确保指针指向的内存区域仍然有效。
  2. nil 检查: 在对指针进行解引用操作前,务必检查它是否为 nil,以防程序 panic。
  3. 内存管理: Go 语言有 垃圾回收(GC) 机制,您不需要手动释放内存,这大大降低了 C/C++ 中指针导致的内存泄漏风险。