Go 结构体指针与返回值类型 知识点汇总

1. 函数返回指针 *T vs 返回值 T

🔹 返回指针 *Employee — 可以直接修改原始数据

1
2
3
4
5
6
7
func EmployeeByID(id int) *Employee { /* ... */ }

// ✅ 读取
fmt.Println(EmployeeByID(id).Position)

// ✅ 修改 — 修改的是原始数据
EmployeeByID(id).Salary = 0

内存模型:

1
2
EmployeeByID(id)  →  指针  →  内存中的 Employee
↑ 修改 Salary,改的是它

💡 返回指针时,你拿到的是原始数据的地址,通过指针修改就是直接改原始数据。


🔹 返回值 Employee — 不能直接修改

1
2
3
4
5
6
7
func EmployeeByID(id int) Employee { /* ... */ }

// ✅ 读取
fmt.Println(EmployeeByID(id).Position)

// ❌ 修改 — 编译错误!
EmployeeByID(id).Salary = 0

原因分析:

1
2
EmployeeByID(id)  →  临时副本(不可寻址)
↑ 赋值毫无意义,编译器直接报错

⚠️ 函数返回的值类型是一个临时副本,不是变量,不可寻址,Go 禁止对它赋值。


🔹 用变量接收返回值 — 可以修改,但改的是副本

1
2
3
4
5
func EmployeeByID(id int) Employee { /* 返回值类型 */ }

e := EmployeeByID(id)
e.Salary = 0 // ✅ 编译通过,但只改了本地副本
// ❌ 原始的 Employee 数据没有变

🔹 对比总结

场景 能否编译 修改的是谁?
返回 *Employee,直接 .Salary = 0 原始数据
返回 Employee,直接 .Salary = 0 ❌ 编译错误
返回 Employee,用变量接收后修改 副本,原始数据不变
返回 *Employee,用变量接收后修改 原始数据

🔹 形象类比

1
2
3
4
5
🎯 返回指针  → 给你一张纸条:“员工档案在 3 号柜子“
→ 你去柜子里改档案 ✅

🎯 返回值 → 给你一份档案的复印件
→ 你在复印件上改,原件不变 ❌

2. 赋值语句左边必须是可寻址的变量

🔹 核心原则

赋值语句左边必须是一个可寻址的变量(addressable)。函数直接返回的值类型是临时的、不可寻址的。

🔹 代码示例

1
2
3
4
5
6
7
8
9
10
// ✅ 变量有地址,能赋值
var e Employee
e.Salary = 100

// ✅ 指针指向真实变量,能赋值
p := &e
p.Salary = 200 // 修改的是 e

// ❌ 函数返回值是临时的,不可寻址
getEmployee().Salary = 300 // 返回值类型时,编译错误

🔹 什么是“可寻址“?

表达式 是否可寻址 说明
var e Employee 变量本身有内存地址
p := &e 指针指向真实变量
e.Field 结构体字段可寻址(当 e 可寻址时)
p.Field 指针字段可寻址(自动解引用)
getEmployee() 函数返回值是临时副本
getEmployee().Field 临时副本的字段也不可寻址
map[key] map 元素不可寻址(特殊限制)

3. 指针访问成员的语法糖

🔹 Go 中 p.Salary(*p).Salary 完全等价

1
2
3
4
p := &e

(*p).Salary = 200 // 显式解引用,完整写法
p.Salary = 200 // ✨ 简写,Go 自动解引用(推荐)

💡 编译器看到 p 是指针类型,会自动将 p.Salary 翻译成 (*p).Salary


🔹 与 C 语言的对比

1
2
3
// C 语言:必须区分两种运算符
e.Salary // 值用 .
p->Salary // 指针用 ->
1
2
3
// Go:统一用 . 运算符 ✨
e.Salary // 值
p.Salary // 指针,自动解引用

▎ Go 追求简洁,不区分 .->,全部用 .,编译器自动处理。实际开发中没有人写 (*p).Salary


🔑 关键要点速记

要点 说明
返回指针 可修改原始数据,适合大结构体或需要修改的场景
返回值 返回副本,修改不影响原始数据,适合小结构体或只读场景
不可寻址 函数返回的值类型是临时副本,不能直接赋值
变量接收 用变量接收返回值后可修改,但改的是副本
语法糖 p.Field 等价于 (*p).Field,推荐用简写
统一运算符 Go 中指针和值都用 . 访问字段,无需区分 ->

🎯 实践建议

✅ 什么时候返回指针?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 需要修改原始数据
func UpdateSalary(id int, newSalary int) {
e := EmployeeByID(id) // 返回 *Employee
e.Salary = newSalary // 直接修改
}

// 2. 结构体较大,避免拷贝开销
type LargeStruct struct {
data [1024]byte
// ... 很多字段
}
func GetLarge() *LargeStruct { /* ... */ }

// 3. 可能需要返回 nil 表示"不存在"
func FindUser(id int) *User {
if notFound {
return nil // 值类型无法表示"不存在"
}
return &user
}

✅ 什么时候返回值?

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 小结构体,拷贝开销可忽略
type Point struct{ X, Y int }
func Origin() Point { return Point{0, 0} }

// 2. 函数式风格,不修改外部状态
func WithName(e Employee, name string) Employee {
e.Name = name
return e // 返回新副本,不修改原数据
}

// 3. 避免指针逃逸到堆(微优化)
// 返回值可能在栈上分配,返回指针可能逃逸到堆

📌 一句话总结:理解“可寻址“是掌握 Go 指针赋值的关键;返回指针改原件,返回值改副本;指针访问字段用 . 简写,编译器会自动解引用。