指针与方法
指针是 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{}。
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 指针传递
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,决定方法能不能修改原对象。
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的方法集 + 指针方法集 - ✅ 一致性:同一类型的方法用同一种接收者
下一章 接口。