在 Go 中,只有可寻址的变量才能作为赋值操作的左值;函数返回的结构体值(非指针)是临时、不可寻址的,因此无法直接对其字段赋值,而返回指针则可寻址并支持字段修改。
在 Go 中,只有可寻址的变量才能作为赋值操作的左值;函数返回的结构体值(非指针)是临时、不可寻址的,因此无法直接对其字段赋值,而返回指针则可寻址并支持字段修改。
Go 语言里有一条挺硬的规则:赋值语句的左边,必须是一个在内存里“有固定位置”的东西,也就是能通过 &x 拿到地址的变量。这不是随便定的,而是为了保证每次赋值都有实实在在的副作用——你要是能随便改一个临时值,那改完就丢,谁也没法知道,毫无意义。
来,看个简单例子就明白了:
package main
type Employee struct {
ID int
Salary float64
}
func EmployeeByID(id int) *Employee {
return &Employee{ID: id, Salary: 5000.0}
}
func EmployeeByIDValue(id int) Employee {
return Employee{ID: id, Salary: 5000.0}
}
func main() {
id := 123
// ✅ 合法:*Employee 是可寻址的(指向堆/栈上的实际结构体)
EmployeeByID(id).Salary = 0 // 修改生效,影响底层对象
// ❌ 编译错误:Employee 值是临时的、不可寻址的
// EmployeeByIDValue(id).Salary = 0 // compile error: cannot assign to EmployeeByIDValue(id).Salary
// ✅ 合法(但效果不同):先绑定到变量,再赋值
emp := EmployeeByIDValue(id)
emp.Salary = 0 // 修改的是 emp 的副本,原返回值已丢弃,无外部影响
}
这里的关键就两点:
- 第一个函数返回的是
*Employee,解引用之后(比如(*ptr).Salary)其实是在访问底层那个实实在在的Employee实例,它当然是可寻址的,所以直接赋值完全没问题。 - 第二个函数返回的是
Employee本身,这个表达式的结果是一个临时值——Go 语言规范明确说了,临时值不可寻址,所以x.f = v这种写法根本过不了编译。
⚠️ 有几点需要特别提醒:
- 就算结构体的字段是导出的(大写字母开头),也不影响可寻址性规则,该报错还是报错。
- 用
&struct{}字面量(比如&Employee{})返回指针是安全的,Go 会在必要时把字面量分配到堆上,或者延长其生命周期,保证指针一直有效。 - 如果非得用值类型,那就先把它绑定到一个局部变量上,比如
e := f(); e.field = v。但要注意:这时候改的是副本,不是原始返回值,调用方那边不会受影响。
总结一下:Go 通过强制“可寻址性”这个机制,把内存安全和语义清晰都照顾到了——只有真正持有状态的对象(变量、切片元素、解引用后的指针等等)才允许你对它的字段进行赋值。理解了这一点,写 Go 代码的时候就能避免很多莫名其妙的错误,也更容易写出符合语言惯用法的代码。