当前位置:首页 > 文章列表 > Golang > Go问答 > 使用 slice 作为 *Item 是否正确,因为 Slice 默认是指针

使用 slice 作为 *Item 是否正确,因为 Slice 默认是指针

来源:stackoverflow 2024-04-28 22:06:34 0浏览 收藏

学习知识要善于思考,思考,再思考!今天golang学习网小编就给大家带来《使用 slice 作为 *Item 是否正确,因为 Slice 默认是指针》,以下内容主要包含等知识点,如果你正在学习或准备学习Golang,就都不要错过本文啦~让我们一起来看看吧,能帮助到你就更好了!

问题内容

在 Go 中使用切片的正确方法是什么?根据 Go 文档,默认情况下切片是指针,因此将切片创建为 *[]Item 是正确的方法吗?由于切片默认是指针,因此不能通过这种方式创建切片,使其指向指针。

我觉得创建切片的正确方法是 []Item[]*item (保存项目指针的切片)


正确答案


一些理论

你的问题没有意义:没有“正确”或“错误”或“正确”和“不正确”:你可以有一个指向切片的指针,也可以有一个指向切片指针的指针,并且您可以无休止地添加此类间接级别。

做什么取决于您在特定情况下的需要。

为了帮助您进行推理,我将尝试提供一些事实并得出一些结论。

关于 go 中的类型和值,首先要了解的两件事是:

  • go 中的所有内容始终都是按值传递的。

    这意味着变量赋值(=:=)、将值传递给函数和方法调用,以及复制内部发生的内存,例如重新分配切片的支持数组或重新平衡映射时。

    按值传递意味着分配的值的实际位被物理复制到“接收”该值的变量中。

  • go 中的类型(内置类型和用户定义类型(包括标准库中定义的类型))在赋值时可以具有值语义和引用语义。

    这个有点棘手,通常会导致新手错误地认为上面解释的第一条规则不成立。

    “技巧”是,如果类型包含指针(变量的加法器)或由单个指针组成,则在复制该类型的值时会复制该指针的值。

    这是什么意思? 非常简单:如果将 int 类型的变量的值分配给 int 类型的另一个变量,两个变量都包含相同的位,但它们是完全独立的:更改其中任何一个的内容,另一个变量将不受影响。 如果将包含指针(或由单个指针组成)的变量分配给另一个变量,则它们都将包含相同的位,并且是独立的,因为更改其中任何一个中的这些位都不会影响另一个。 但由于这两个变量中的指针都包含同一内存位置的地址,因此使用这些指针修改它们所指向的内存位置的内容将修改同一内存。
    换句话说,区别在于 int引用任何东西,而指针自然引用另一个内存位置 - 因为它包含其地址。 因此,如果一个类型至少包含一个指针(它可以通过包含另一个类型的字段来实现,而该类型本身包含一个指向任何嵌套级别的指针,依此类推),则该类型的值将具有引用赋值语义:您将一个值分配给另一个变量,最终会得到两个引用同一内存位置的值。

    这就是为什么映射、切片和字符串具有引用语义:当您分配这些类型的变量时,两个变量都指向相同的底层内存位置。

让我们继续讨论切片。

切片与切片指针

从逻辑上讲,切片是一个包含三个字段的 struct:一个指向切片的支持数组(实际上包含切片元素)的指针,以及两个 int:切片的容量及其长度。 当您传递并分配切片值时,将复制这些 struct 值:一个指针和两个整数。 正如您所看到的,当您在支持数组周围传递切片值时,不会复制它 - 只是指向它的指针。

现在让我们考虑一下何时要使用普通切片或指向切片的指针。

如果您关心性能(内存分配和/或复制内存所需的 cpu 周期),那么这些担忧是没有根据的:在当今的硬件上,在传递切片时复制三个整数是非常便宜的。 使用指向切片的指针将使复制速度稍微快一点(单个整数而不是三个整数),但这些节省很容易被两个事实所抵消:

  • 切片的值几乎肯定最终会被分配在堆上,以便编译器可以确保其值能够跨越函数调用的边界而继续存在,因此您需要为使用内存管理器付费,并且垃圾收集器将有更多工作。
  • 使用一级间接寻址可以减少 data locality:访问 ram 的速度很慢,因此 cpu 具有缓存,可以在读取数据的地址之后的地址预取数据。如果控制流立即读取另一个位置的内存,则预取的数据将被丢弃:缓存垃圾。

好的,那么是否存在您需要指向切片的指针的情况? 是的。例如,内置 append 函数可以定义为

func append(*[]t, t...)

而不是

func append([]T, T...) []T

(注意,这里的 t 实际上意味着“任何类型”,因为 append 不是一个库函数,不能在普通 go 中合理定义;所以它是一种伪代码。)

也就是说,它可以接受指向切片的指针,并可能替换指针指向的切片,因此您可以将其称为 append(&slice, element) 而不是 slice =追加(切片,元素)

但老实说,在我处理过的现实 go 项目中,我记得使用指向切片的指针的唯一情况是关于池化大量重用的切片,以节省内存重新分配。唯一的情况只是由于 sync.Pool 保留了 interface{} 类型的元素,这在使用指针时可能更有效。

值切片与值指针切片

与上述逻辑完全相同,适用于有关此案例的推理。

当您将值放入切片时,该值将被复制。当切片需要增长其支持数组时,该数组将被重新分配,重新分配意味着将所有现有元素物理复制到新的内存位置。

因此,有两个考虑因素:

  • 元素是否足够小,以便复制它们不会对内存和 cpu 资源造成压力?

    (请注意,“小”与“大”也很大程度上取决于工作程序中此类复制的频率:偶尔复制几兆字节并不是什么大问题;甚至在很短的时间内复制几十千字节时间关键的循环可能是一件大事。)

  • 您的程序可以处理相同数据的多个副本吗? (例如,某些类型的值(如 sync.Mutex)在首次使用后不得复制。²)

如果任一问题的答案为“否”,则应考虑将指针保留在切片中。但是,当您考虑保留指针时,还要考虑上面解释的数据局部性:如果切片包含用于时间关键的数字运算的数据,那么最好不要让 cpu 追逐指针。

回顾一下:当您询问“正确”或“正确”的做某事的方法时,如果没有指定我们可以对问题的所有可能解决方案进行分类的一组标准,那么这个问题就没有意义。尽管如此,在设计存储和操作数据的方式时必须考虑一些因素,我已尝试解释这些考虑因素。

一般来说,关于切片的经验法则可能是:

  • 切片被设计为“按原样”传递——作为值,而不是指向包含其值的变量的指针。

    不过,有合理的理由拥有指向切片的指针。

  • 大多数时候,您将值保存在切片的元素中,而不是指向具有这些值的变量的指针。
    此一般规则的例外情况:

    • 您打算存储在切片中的值占用了太多空间,因此看起来使用它们的切片的设想模式会带来过多的内存压力。
    • 要存储在切片中的值类型要求不得复制它们,而只能引用它们,每个值都作为单个实例存在。一个很好的例子是包含/嵌入 sync.Mutex 类型字段的类型(或者实际上是 sync 包中任何其他类型的变量,除了那些本身具有引用语义的变量,例如 sync.Pool)²。

关于正确性与性能的警告

上面的文字包含了很多性能方面的考虑。 我之所以介绍它们,是因为 go 是一种相当低级的语言:不像 c、c++ 和 rust 那样低级,但在性能达到一定水平时仍然为程序员提供了足够的回旋余地。赌注。 不过,您应该很好地理解,在您的学习曲线上的这一点上,正确性必须是您的首要目标(如果不是唯一目标):请不要冒犯,但如果您是在调整一些 go 代码之后为了节省一些cpu时间来执行它,你一开始就没有问你的问题。 换句话说,请将以上所有内容视为一组事实和注意事项,以指导您学习和探索该主题,但不要陷入首先考虑性能的陷阱。使您的程序正确且易于阅读和修改。

¹ 接口值是一对指针:指向包含您放入接口值中的值的变量,以及 go 运行时内描述该变量类型的特殊数据结构。 因此,虽然您可以直接将切片值放入 interface{} 类型的变量中(从某种意义上说,它在语言中完全没问题),但如果值的类型本身不是单个指针,则编译器将必须在堆上分配一个变量来包含您的值的副本,并将指向该新变量的指针存储到 interface{} 类型的值中。 这是为了保持 go 赋值的“一切总是按值传递”语义所必需的。
因此,如果将切片值放入 interface{} 类型的变量中,最终将在堆上得到该值的副本。 因此,在数据结构(例如 sync.Map)中保留指向切片的指针会使代码变得更丑陋,但会减少内存变动。

² 所有同步原语在编译为机器代码时,都在内存位置上工作 - 也就是说,正在运行的程序中需要在同一原语上同步的所有部分实际上都使用表示该原语的内存块的相同地址原始。因此,考虑一下,如果您锁定互斥体,将其值复制到一个新变量(这意味着一个不同的内存位置),然后解锁该副本,最初锁定的副本将不会注意到,并且所有使用它进行同步的程序的其他部分也不会注意到,这意味着您的代码中有一个严重的错误。

好了,本文到此结束,带大家了解了《使用 slice 作为 *Item 是否正确,因为 Slice 默认是指针》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多Golang知识!

版本声明
本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
iPhone充电速度遭汽车博主吐槽:竟比纯电车还慢!iPhone充电速度遭汽车博主吐槽:竟比纯电车还慢!
上一篇
iPhone充电速度遭汽车博主吐槽:竟比纯电车还慢!
WIN7打开休眠功能的操作步骤
下一篇
WIN7打开休眠功能的操作步骤
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    511次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    498次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 扣子空间(Coze Space):字节跳动通用AI Agent平台深度解析与应用
    扣子-Space(扣子空间)
    深入了解字节跳动推出的通用型AI Agent平台——扣子空间(Coze Space)。探索其双模式协作、强大的任务自动化、丰富的插件集成及豆包1.5模型技术支撑,覆盖办公、学习、生活等多元应用场景,提升您的AI协作效率。
    13次使用
  • 蛙蛙写作:AI智能写作助手,提升创作效率与质量
    蛙蛙写作
    蛙蛙写作是一款国内领先的AI写作助手,专为内容创作者设计,提供续写、润色、扩写、改写等服务,覆盖小说创作、学术教育、自媒体营销、办公文档等多种场景。
    16次使用
  • AI代码助手:Amazon CodeWhisperer,高效安全的代码生成工具
    CodeWhisperer
    Amazon CodeWhisperer,一款AI代码生成工具,助您高效编写代码。支持多种语言和IDE,提供智能代码建议、安全扫描,加速开发流程。
    35次使用
  • 畅图AI:AI原生智能图表工具 | 零门槛生成与高效团队协作
    畅图AI
    探索畅图AI:领先的AI原生图表工具,告别绘图门槛。AI智能生成思维导图、流程图等多种图表,支持多模态解析、智能转换与高效团队协作。免费试用,提升效率!
    58次使用
  • TextIn智能文字识别:高效文档处理,助力企业数字化转型
    TextIn智能文字识别平台
    TextIn智能文字识别平台,提供OCR、文档解析及NLP技术,实现文档采集、分类、信息抽取及智能审核全流程自动化。降低90%人工审核成本,提升企业效率。
    66次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码