Learn
Go/09-pointers-and-methods

指针与方法

指针是 Go 区分"传值"和"传引用"的唯一方式。理解指针与方法接收者的关系,是写出符合 Go 习惯的 API 的关键。

1. 指针基础

& 取地址,* 解引用。指针的零值是 nil。

指针基础
package main
 
import "fmt"
 
func main() {
    a := 42
    p := &a            // p 是 *int
    fmt.Println(p)     // 地址
    fmt.Println(*p)    // 42
 
    *p = 100
    fmt.Println(a)     // 100(a 被改)
 
    var q *int
    fmt.Println(q)     // <nil>
    if q == nil {
        fmt.Println("q 是 nil 指针")
    }
}
💡为什么要用指针
  • 想在函数内修改外部变量时
  • 避免拷贝大对象(大结构体、大数组)
  • 表达"这里可能是 nil"的语义

2. new 函数

new(T) 返回 *T,指向 T 的零值。new 不常用,结构体多用 &T{}。

new 函数
package main
 
import "fmt"
 
type Point struct{ X, Y int }
 
func main() {
    p := new(Point)   // *Point,X、Y = 0
    p.X = 10
    p.Y = 20
    fmt.Println(*p)
 
    q := &Point{1, 2} // 字面量更常用
    fmt.Println(*q)
}

3. 值传递 vs 指针传递

值传递 vs 指针传递
package main
 
import "fmt"
 
type User struct{ Name string }
 
// 值传递:拷贝一份,原对象不变
func renameByValue(u User, newName string) {
    u.Name = newName
}
 
// 指针传递:原地修改
func renameByPtr(u *User, newName string) {
    u.Name = newName
}
 
func main() {
    u := User{Name: "Alice"}
    renameByValue(u, "Bob")
    fmt.Println("值传递后:", u.Name)   // Alice
 
    renameByPtr(&u, "Carol")
    fmt.Println("指针传递后:", u.Name) // Carol
}
⚠️不要过度使用指针
  • 小的不可变数据(如 time.Time、Point)按值传即可
  • 指针让并发同步变复杂
  • Go 社区惯例:指针用于修改,值用于只读

4. 方法接收者:值 vs 指针

方法是带接收者的函数。接收者可以是值 T 或指针 *T,决定方法能不能修改原对象。

值接收者 vs 指针接收者
package main
 
import "fmt"
 
type Counter struct{ N int }
 
// 值接收者:操作副本
func (c Counter) IncValue() {
    c.N++
}
 
// 指针接收者:操作原对象
func (c *Counter) IncPtr() {
    c.N++
}
 
func main() {
    c := Counter{N: 0}
    c.IncValue()
    fmt.Println("IncValue 后:", c.N)   // 0
 
    c.IncPtr()
    fmt.Println("IncPtr 后:  ", c.N)   // 1
}
ℹ️自动取址规则

c.IncPtr() 中 c 是值,但 Go 编译器自动取址为 (&c).IncPtr()。反过来:只有可寻址的值才能调用指针接收者方法。

何时用哪种接收者?

场景推荐
方法要修改接收者指针接收者
结构体很大(> 几十字节)指针接收者
包含 sync.Mutex 等不可拷贝字段指针接收者
小、不可变、值语义(如 time.Time)值接收者

一致性原则:一个类型的所有方法最好用同一种接收者。混用会让 API 变得难用。

5. 方法集(Method Set)

方法集决定哪些方法可以通过哪种类型访问,影响接口实现。

  • T 的方法集:只包含值接收者方法
  • *T 的方法集:包含值接收者 + 指针接收者方法
方法集差异
package main
 
import "fmt"
 
type Person struct{ Name string }
func (p Person) Hello()  { fmt.Println("Hi,", p.Name) }
func (p *Person) Rename(s string) { p.Name = s }
 
type Greeter interface{ Hello() }
 
func main() {
    p := Person{Name: "Alice"}
 
    // *Person 满足 Greeter(自动取址)
    var g Greeter = &p
    g.Hello()
 
    // Person 也满足 Greeter
    var g2 Greeter = p
    g2.Hello()
}
💡经验法则
  • 几乎所有 Go 代码都用指针接收者
  • 唯一的例外是 time.Time 这种小而不可变的值类型
  • 这能避免"明明改了却没生效"的 bug

6. 综合练习:单向链表

单向链表
package main
 
import "fmt"
 
type Node struct {
    Value int
    Next  *Node
}
 
type LinkedList struct {
    Head *Node
    Size int
}
 
func (l *LinkedList) Push(v int) {
    l.Head = &Node{Value: v, Next: l.Head}
    l.Size++
}
 
func (l *LinkedList) String() string {
    var out []int
    for n := l.Head; n != nil; n = n.Next {
        out = append(out, n.Value)
    }
    return fmt.Sprintf("%v", out)
}
 
func main() {
    ll := &LinkedList{}
    for _, v := range []int{3, 1, 4, 1, 5, 9, 2, 6} {
        ll.Push(v)
    }
    fmt.Println(ll)
    fmt.Println("size =", ll.Size)
}

🎯 练习

双向链表
// 任务:扩展上面的 Node 为双向链表节点
// 1. 增加 Prev *Node 字段
// 2. 实现 PushFront(在头部插入)
// 3. 实现 PopBack(删除并返回尾节点值)
//
// 提示:为了能在 O(1) 删除尾节点,LinkedList 需要 Tail 字段
 
package main
 
import "fmt"
 
type Node struct {
    Value int
    Next  *Node
    Prev  *Node
}
 
type LinkedList struct {
    Head *Node
    Tail *Node
    Size int
}
 
func (l *LinkedList) PushFront(v int) {
    n := &Node{Value: v}
    if l.Head == nil {
        l.Head, l.Tail = n, n
    } else {
        n.Next = l.Head
        l.Head.Prev = n
        l.Head = n
    }
    l.Size++
}
 
func (l *LinkedList) PopBack() (int, bool) {
    if l.Tail == nil {
        return 0, false
    }
    v := l.Tail.Value
    l.Tail = l.Tail.Prev
    if l.Tail == nil {
        l.Head = nil
    } else {
        l.Tail.Next = nil
    }
    l.Size--
    return v, true
}
 
func (l *LinkedList) String() string {
    out := []int{}
    for n := l.Head; n != nil; n = n.Next {
        out = append(out, n.Value)
    }
    return fmt.Sprintf("%v", out)
}
 
func main() {
    ll := &LinkedList{}
    for _, v := range []int{1, 2, 3, 4, 5} {
        ll.PushFront(v)
    }
    fmt.Println("初始:", ll)
    v, _ := ll.PopBack()
    fmt.Println("弹出尾:", v, "剩余:", ll)
}

小结

  • ✅ & 取址,* 解引用;指针零值是 nil
  • ✅ new(T) 返回 *T 零值;结构体常用 &T{}
  • ✅ 函数参数按值传递,要修改用指针
  • ✅ 方法接收者:T 是值,*T 是指针
  • ✅ 几乎所有方法都用指针接收者(除了小不可变值)
  • ✅ *T 的方法集 = T 的方法集 + 指针方法集
  • ✅ 一致性:同一类型的方法用同一种接收者

下一章 接口。