Go 保留了指针的核心能力(间接寻址、传地址),但砍掉了指针算术和引用类型 &T,换来内存安全和 GC 兼容。


C++ 对照速查表

概念C++Go差异说明
声明指针int* p / int *pvar p *intGo 把 * 放在类型前
取地址&x&x完全相同
解引用*p*p完全相同
指针算术p++, p + n不支持Go 禁止,用切片代替
空指针NULL / nullptrnilGo 没有 NULL 关键字
引用类型int& ref = x不存在Go 没有引用,统一用指针
newnew int (调用构造)new(int) → *intGo 的 new 只分配零值内存,不调用构造函数
struct 访问ptr->fieldptr.fieldGo 编译器自动解引用,不需要 ->
make无对应物make([]int, 5)仅用于 slice/map/channel,返回初始化后的值(不是指针)

1. 指针基础:&、*、*T

package main

import "fmt"

func main() {
    x := 42
    var p *int = &x // p 是指向 x 的指针,*int 表示"指向 int 的指针"

    fmt.Println("x 的值:", *p)  // 解引用:通过指针读取值 → 42
    fmt.Println("x 的地址:", p)  // 打印地址 → 0xc0000b4008(类似)

    *p = 100 // 通过指针修改原变量
    fmt.Println("修改后 x:", x) // → 100

    // 零值:指针的零值是 nil
    var q *int
    fmt.Println("q == nil:", q == nil) // → true
}

要点:*int 的 * 是类型修饰符(“int 的指针”),*p 的 * 是操作符(“解引用”)。


2. 指针零值 nil

var p *int // nil,不指向任何内存

// 常见用法:用 nil 表示"无值"
type Node struct {
    Val  int
    Next *Node // 链表场景,默认 nil 表示没有下一个节点
}

// 判断是否为空
if p == nil {
    fmt.Println("p 是空指针")
}

C++ 对照:nullptr / NULL。Go 中 nil 可以和指针、slice、map、channel、interface 比较。


3. 指针不能做算术运算

arr := [3]int{10, 20, 30}
p := &arr[0]

// ❌ 以下全部编译报错:
// p++
// p + 1
// p - 1

// ✅ 正确做法:用切片遍历
for i, v := range arr {
    fmt.Printf("arr[%d] = %d\n", i, v)
}

// 如果确实需要偏移访问,用索引
fmt.Println(arr[1]) // → 20

这是 Go 最重要的安全设计之一:彻底杜绝了 C/C++ 中指针越界的可能。


4. new 函数

// new(T) 分配零值内存,返回 *T(指向该内存的指针)
p := new(int)       // *int,指向 0
q := new(string)    // *string,指向 ""
r := new([]int)     // *[]int,指向 nil slice

fmt.Println(*p, *q, *r) // → 0 "" []

// 等价写法(更常用):
p2 := &int{}   // 字面量取地址,效果相同

与 C++ new 的区别:

  • C++ new int 调用构造函数,可能得到未定义值(POD 类型)
  • Go new(int) 一定给你零值(0、false、""、nil),GC 保障生命周期

5. make 与 new 的区别

// new(T) → 返回 *T(指针),零值
p := new([]int)      // *[]int,底层 slice 是 nil

// make → 返回 T(值),初始化后的值
s := make([]int, 5)  // []int{0, 0, 0, 0, 0},长度 5
m := make(map[string]int) // 空但已初始化的 map,可以安全使用
ch := make(chan int)  // 已初始化的 channel

// 典型错误:对 map 用 new
m2 := new(map[string]int)
// (*m2)["key"] = 1  // ❌ panic! map 是 nil,没有初始化

// 正确做法
m3 := make(map[string]int)
m3["key"] = 1        // ✅ 安全
new(T)make(T, ...)
返回类型*T(指针)T(值本身)
内存状态零值初始化(分配底层数据结构)
适用类型任意类型仅 slice、map、channel
类比 C++new T()类似构造函数内部的初始化

6. 指针作为函数参数(引用传递效果)

// C++: void increment(int& x)  或  void increment(int* x)
// Go:  没有引用,统一用指针

func increment(p *int) {
    *p++ // 通过指针修改原值
}

func main() {
    x := 10
    increment(&x)
    fmt.Println(x) // → 11
}

// 实际应用:交换函数
func swap(a, b *int) {
    *a, *b = *b, *a
}

// 多返回值 + 指针的惯用写法
func divmod(a, b int) (result int, ok bool) {
    if b == 0 {
        return 0, false // ok = false 表示失败
    }
    return a / b, true
}

Go 没有"按引用传递"的概念,所有参数都是值传递。指针只是传递了"地址的副本",效果类似引用。


7. 指针与结构体:自动解引用

type Point struct {
    X, Y int
}

func main() {
    p := Point{X: 1, Y: 2}
    pp := &p // pp 是 *Point

    // C++ 中需要 pp->X,Go 中直接 pp.X(编译器自动解引用)
    fmt.Println(pp.X) // → 1
    pp.X = 10         // 直接修改,不需要 (*pp).X

    // 调用方法也自动解引用
    p.Print()   // ✅ 值接收者,编译器自动取地址
    pp.Print()  // ✅ 指针接收者,直接调用
}

func (p Point) Print() {
    fmt.Printf("(%d, %d)\n", p.X, p.Y)
}

这是 Go 的语法糖:编译器会在需要时自动插入 & 或 *,让 *T 和 T 在方法调用上行为一致。


8. unsafe.Pointer 简介

import "unsafe"

func main() {
    x := 42
    p := unsafe.Pointer(&x)

    // 可以转换为任意指针类型(绕过类型系统)
    p2 := (*float64)(p)
    fmt.Println(*p2) // → 42(按 float64 解释同一块内存,结果无意义)

    // 可以做偏移运算(唯一的指针算术入口)
    arr := [3]int{10, 20, 30}
    base := unsafe.Pointer(&arr[0])
    offset := unsafe.Pointer(uintptr(base) + unsafe.Sizeof(arr[0]))
    p3 := (*int)(offset)
    fmt.Println(*p3) // → 20
}

使用建议:

  • 绝大多数场景不需要 unsafe.Pointer
  • 它绕过了类型安全和 GC 的保护,可能导致不可预期的行为
  • 典型使用场景:reflect 包内部、与 C 交互(CGo)、极致性能优化
  • 如果你觉得自己需要用它,先考虑是否有更 Go 的写法

常见坑

1. nil 指针解引用 → panic

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

C++ 中解引用空指针是未定义行为(段错误/数据损坏)。Go 中是确定的 panic,不会造成内存损坏。

2. new 返回零值,不调用"构造函数"

type User struct {
    Name string
    Age  int
}

u := new(User)
fmt.Println(u.Name) // → ""(零值),不是 "default"

// 如果需要初始化,用结构体字面量
u2 := &User{Name: "张三", Age: 25}

3. 指针比较是比较地址

a := 10
b := 10
pa := &a
pb := &b
fmt.Println(pa == pb) // → false(两个不同变量,地址不同)

// 同一个变量
pc := &a
fmt.Println(pa == pc) // → true(同一个地址)

4. 返回局部变量的指针是安全的

func newUser() *User {
    u := User{Name: "李四"} // 局部变量
    return &u               // ✅ Go 允许!编译器自动逃逸到堆
}

// C++ 中这是悬垂指针(dangling pointer),Go 由 GC 管理,完全安全

5. *T 和 T 的选择

场景推荐原因
基本类型(int, bool, float64)T值很小,指针反而多一次间接寻址
小结构体(< 3 个字段,总共 < 32 字节)T复制成本低,避免 GC 压力
大结构体*T避免大值拷贝
需要修改原值*T值传递的副本改不了原值
struct 方法需要修改接收者*T(指针接收者)否则修改只作用于副本
slice / map / channelT本身就是引用语义(内部含指针)

经验法则:如果拿不定主意,先用值类型 T,需要时再改为 *T。Go 编译器和 go vet 会提醒你。


练习任务

  • 声明一个 *int 变量,用 & 取地址赋值,用 * 解引用读取
  • 写一个 swap(a, b *int) 函数,用指针交换两个整数的值
  • 用 new 分配一个结构体指针,验证字段是零值
  • 对比 new([]int) 和 make([]int, 3) 的区别(打印各自的类型和值)
  • 写一个 Person 结构体,分别用值接收者和指针接收者实现 String() string 方法,观察调用差异
  • 写一个函数接收 *Person,在函数内修改字段,验证原值是否被修改
  • 用 unsafe.Pointer 将 *int 转为 *float64,观察输出(理解类型系统外的操作)
  • 思考:为什么 slice、map、channel 不需要传指针就能在函数内修改?(提示:内部结构)