指针接收者允许方法修改原始结构体字段,值接收者仅操作副本;指针接收者是接口实现和大结构体高效调用的必要选择,方法集差异决定接口赋值合法性。

值接收者传入的是结构体副本,所有字段修改都只作用于副本,调用方的原始变量完全不受影响;指针接收者传入的是地址,方法内通过 p.Field = value 直接改写原始内存里的数据。
func (u *User) SetEmail(email string)
func (u User) Name() string)天然适合只读计算,语义更清晰user.SetEmail("a@b.c") 这种写法,即使 user 是值变量 —— 编译器自动转成 (&user).SetEmail(...),但前提是 user 必须是可寻址的(不能是字面量 User{})如果一个类型只用指针接收者实现了某个接口,那么只有 *T 能赋给该接口变量,T 会直接编译报错:cannot use t (type T) as type Interface in assignment。
type Speaker interface { Speak() }
type Dog struct{}
func (d *Dog) Speak() { fmt.Println("Woof!") }
var d Dog
var s Speaker = d // ❌ 编译失败s = &d 或构造时就用指针:s = &Dog{}
不是“一定”,而是“不这样做会付出明显代价”。假设结构体有 20 个字段(约 160 字节),每次调用值接收者方法都会复制整块内存 —— 对高频方法(如 String()、MarshalJSON())就是隐性性能瓶颈。
sync.Mutex、bytes.Buffer、http.Request)都用指针接收者,既是惯例,也是实践验证过的合理选择type ID int、type Point struct{X,Y int})用值接收者反而更轻量,无须间接寻址Go 中「类型 T 的方法集」和「类型 *T 的方法集」是两个不同的集合。这是理解接口赋值行为的底层依据,不是语法糖,是硬性规则。
T 的方法集:只包含值接收者方法(func (T) M())*T 的方法集:包含值接收者 + 指针接收者方法(func (T) M() 和 func (*T) M())*T 总能赋给接口,而 T 只能在所有接口方法都是值接收者时才可赋值&,Go 也在背后严格按这套规则做检查和分发。