你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!

简介: 本文深入解析Go中结构体方法接收器的选择难题:值接收器操作副本,无法修改原值;指针接收器直接操作原始数据。核心选择原则——需修改必用指针,大结构体优先指针,小结构体/无修改场景推荐值接收器,并兼顾接口实现与方法集一致性。(239字)

前几天在技术群里看到有小伙伴问了一个经典问题:"Go 结构体方法调用时,什么时候用值接收器,什么时候用指针接收器?"

虽然这是个老生常谈的话题,但是,我发现很多同学(包括工作几年的)对此还是一知半解。正好最近在梳理 Go 基础知识,就写下这篇文章。

我在网上搜索时发现,这个问题从 Go 诞生至今一直困扰着开发者,真是"一代传一代"的经典问题。

结构体是什么

在 Go 语言中有个基本类型,我们称之为结构体(struct)。是 Go 语言中非常常用的,基本定义如下:

type struct_variable_type struct {
   
    member definition
    member definition
    ...
    member definition
}

结构体(struct)是 Go 中的一种复合数据类型,可以将不同类型的数据组合在一起。

简单示例:

package main

import "fmt"

type User struct {
   
    Name string
    Age  int
}

func main() {
   
    u := User{
   "张三", 25}
    u.Age = 26
    fmt.Println(u.Age)
}

输出结果:

26

这部分属于基础知识,因此不再过多解释。如果看不懂,建议重新再去看一看 Go 语言语法基础。

结构体和指针调用

讲解前置概念后,直接进入本文主题。如下例子:

type User struct {
   
    Name string
    Age  int
}

func (u User) SetName1(name string) {
   
    u.Name = name
}

func (u *User) SetName2(name string) {
   
    u.Name = name
}

上面的代码声明了一个 User 结构体,其包含两个结构体方法,分别是 SetName1SetName2 方法,两者之间的差异就是接收器的类型不同。

那么,这两者有什么区别,什么情况下要用哪种,有没有啥注意事项呢?

两者区别

从许多小伙伴的反馈来看,这二者之间确实会让人感到困惑,经常会有人纠结要不要使用 "指针",又担心性能、内存什么的。

实际上,情况并没那么复杂,看看下面的栗子🌰:

func (u User) SetName1(name string)
func (u *User) SetName2(name string)

当在一个类型上定义一个方法时,接收器(在上面的例子中是 u)的行为就像它是方法的一个参数一样。其相当于:

其实上方的代码就是下方代码的一种语法糖而已。

func SetName1(u User, name string) {
   
    u.Name = name
}

func SetName2(u *User, name string) {
   
    u.Name = name
}

因此结构体方法是要将接收器定义成值,还是指针。这本质上与函数参数应该是值还是指针是同一个问题。是不是看到这里,有同学就已经茅塞顿开了?

实际效果对比

让我们通过一个完整的例子来看看两者的实际区别:

package main

import "fmt"

type Counter struct {
   
    Value int
}

// 值接收器
func (c Counter) IncrementByValue() {
   
    c.Value++
    fmt.Printf("方法内部值:%d\n", c.Value)
}

// 指针接收器
func (c *Counter) IncrementByPointer() {
   
    c.Value++
    fmt.Printf("方法内部值:%d\n", c.Value)
}

func main() {
   
    counter := Counter{
   Value: 10}

    fmt.Printf("初始值:%d\n", counter.Value)

    // 使用值接收器
    counter.IncrementByValue()
    fmt.Printf("调用值接收器后:%d\n", counter.Value)

    // 使用指针接收器
    counter.IncrementByPointer()
    fmt.Printf("调用指针接收器后:%d\n", counter.Value)
}

输出结果:

初始值:10
方法内部值:11
调用值接收器后:10
方法内部值:11
调用指针接收器后:11

看到了吗?这就是核心区别所在。值类型不可修改,指针类型可直接修改。

如何选择?

整体有以下几个考虑因素,按重要程度顺序排列:

1. 在使用上的考虑:方法是否需要修改接收器? 如果需要,接收器必须是一个指针。
2. 在效率上的考虑: 如果接收器很大,比如:一个大的结构体,使用指针接收器会好很多。
3. 在一致性上的考虑: 如果类型的某些方法必须有指针接收器,那么其余的方法也应该有指针接收器,所以无论类型如何使用,方法集都是一致的。

回到上面的例子中,从功能使用角度来看:

  • 如果 IncrementByPointer 方法修改了 c 的字段,调用者是可以看到这些字段值变更的,因为其是指针引用,本质上是同一份。
  • 相对 IncrementByValue 方法来讲,该方法是用调用者参数的副本来调用的,本质上是值传递,它所做的任何字段变更对调用者来说是看不见的。

另外对于基本类型、切片和小结构等类型,值接收器是非常廉价的。

因此除非方法的语义需要指针,那么值接收器是最高效和清晰的。在内存管理方面,也不需要过度关注。出现问题时再优化就好了。

Go 的语法糖

可能有小伙伴会疑问:"我明明定义的是指针接收器,为什么可以用结构体变量直接调用?"

func main() {
   
    user := User{
   Name: "张三"}

    // 这样调用为什么不报错?
    user.SetName2("李四")  // SetName2 接收器是 *User
}

这是因为 Go 提供了便利的语法糖:

  • 当你用结构体变量调用指针接收器方法时,Go 会自动转换为 (&user).SetName2("李四")
  • 当你用结构体指针调用值接收器方法时,Go 会自动转换为 (*userPtr).SetName1("王五")

也就是下面的代码,完全不会报错。

package main

type User struct {
   
    Name string
    Age  int
}

func (u User) SetName1(name string) {
   
    u.Name = name
}

func (u *User) SetName2(name string) {
   
    u.Name = name
}

func main() {
   
    u := User{
   Name: "Alice", Age: 30}
    u.SetName1("Bob")
    println(u.Name) // 输出: Alice

    u.SetName2("Charlie")
    println(u.Name) // 输出: Charlie

    u1 := &User{
   Name: "David", Age: 25}
    u1.SetName1("Eve")
    println(u1.Name) // 输出: David

    u1.SetName2("Frank")
    println(u1.Name) // 输出: Frank
}

这个语法糖让代码更加简洁,但最好还是理解一下底层机制。

接口实现的坑

这里有一个很重要的细节,容易踩坑:

package main

import (
    "fmt"
)

type Printer interface {
   
    Print()
}

type Document struct {
   
    Content string
}

func (d *Document) Print() {
   
    fmt.Println(d.Content)
}

func main() {
   
    var p Printer
    doc := Document{
   Content: "Hello"}

    // 这样会编译错误!
    // p = doc //  Document does not implement Printer (Print method has pointer receiver)

    // 正确的做法
    p = &doc // *Document implements Printer
    p.Print()
}

记住:如果方法是指针接收器,那么只有指针类型才实现了该接口。

性能考量

对于大结构体,性能差异是显著的:

type LargeStruct struct {
   
    Data [10000]int  // 40KB 的数据
}

func (ls LargeStruct) ProcessByValue() {
   
    // 每次调用都复制 40KB
}

func (ls *LargeStruct) ProcessByPointer() {
   
    // 只传递 8 字节指针(64 位系统)
}

显然,对于大结构体,指针接收器是更好的选择。

总结

在本文中,我们针对 Go 结构体和结构体指针调用有什么区别,这个问题进行了深入浅出的分析和说明。

  1. 值接收器:操作副本,无法修改原始数据,适合小结构体和不需修改的场景
  2. 指针接收器:操作原始数据,可以修改,适合大结构体和需要修改的场景
  3. 选择原则:优先考虑是否需要修改,其次考虑性能,最后考虑一致性

而在本文中所介绍的部分内容,实际上在 Go 官方文档中都有相应说明。这确实是一个被问了无数次的经典问题。

谁再疑惑这个问题,转发这篇文章,学就完了。

如果对你有所帮助,那么,就帮忙点个赞支持一下吧~

相关文章
|
4天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
394 124
|
6天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
667 4
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
4天前
|
缓存 人工智能 运维
阿里云618百炼大模型Qwen3.7-Max功能、免费试用、订阅计费、配置接入详解
Qwen3.7-MAX是阿里云百炼平台推出的通义千问3.7系列旗舰大语言模型,专为智能体时代复杂任务打造,依托阿里云全域算力与自研技术,在逻辑推理、长文本处理、代码工程、长周期自主执行等领域达到行业顶尖水平。2026年618期间,该模型推出多重免费试用权益、按量计费5折、订阅套餐优惠等专属福利,覆盖个人开发者、团队与企业全场景需求,以下从核心功能、免费试用、订阅计费、配置接入四方面展开详细解析。
389 123
|
2天前
|
人工智能 自然语言处理 API
阿里云Token Plan团队版解析:功能、三档套餐与省钱订阅指南
阿里云百炼平台推出的Token Plan团队版,是面向企业与团队的AI大模型订阅服务,以Credits为统一计量单位,整合文本与图像生成模型,提供团队管理、数据安全、多工具兼容等核心能力,解决团队零散订阅AI服务的管理混乱、成本失控、数据安全等痛点。本文将从核心定位、套餐详情、计费规则、团队管理、工具兼容、便宜订阅技巧等方面,全面解析Token Plan团队版,帮助企业与团队高效、低成本地使用AI服务。
293 108
|
17天前
|
缓存 测试技术 API
Qwen 3.7 Plus 与 Max 实测:性价比与多模态能力差异解析(2026)
2026 年 6 月 1 日,阿里悄无声息地发布了 Qwen 3.7 Plus,距 Qwen 3.7 Max 上线刚好 11 天。同样的 1M 上下文,同样的 35 小时自治上限。但价格才是头条:Plus 是 0.40/M输入,Max是 2.50/M——便宜约 6 倍——并且还能看图、看视频。Vision Arena 上 Plus 已经排到 #16。所以这周真正值得讨论的问题不是”要不要为视觉能力买单”,而是”Max 凭什么用 6 倍价格换来 2 个百分点的 benchmark 领先”。
|
3天前
|
存储 人工智能 数据可视化
别再手动复制 Skill 了:多 Agent 时代的 Skill 管理方案
多 Agent 场景下 Skill 的统一管理与同步。
218 123
|
10天前
|
缓存 人工智能 运维
GLM 5.2自托管全流程实战:硬件选型、vLLM/SGLang部署与成本盈亏测算
2026年智谱发布GLM 5.2超大混合专家模型,区别于以往仅开放API的闭源大模型,该模型权重以MIT开源协议对外发布,企业与开发者可完整下载、本地审计、私有化部署,实现数据不出环境、自定义微调、自主调度推理资源。GLM 5.2拥有753B总参数,原生支持百万级上下文窗口,在代码生成、长文档推理、数学逻辑等多项基准测试中对标国际顶尖商用模型,是首款可完整自托管的前沿代码向大模型。
831 0
|
3天前
|
SQL 存储 运维
日志能不能改?SLS LogStore 原生支持更新和删除了
随着日志承载的业务语义越来越多,数据订正、回填、清理等需求变得越来越常见。SLS 现已为 LogStore 提供原生 update/delete 能力——支持按 RowID 精确修改,按查询条件批量操作,类似计费调账、标签刷新、反馈回填等场景都可以直接在 LogStore 内完成闭环。
188 124