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

Mad*_*han 4 go slice

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

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

kos*_*tix 12

一点理论

\n

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

\n

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

\n

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

\n

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

\n
    \n
  • Go 中的一切,永远、永远都是按值传递的。

    \n

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

    \n

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

    \n
  • \n
  • Go\xe2\x80\x94 中的类型,无论是内置类型还是用户定义的类型(包括标准库中定义的类型)\xe2\x80\x94 在赋值时都可以具有值语义和引用语义。

    \n

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

    \n

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

    \n

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

    \n

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

    \n
  • \n
\n

让我们继续讨论切片。

\n

切片与切片指针

\n

从逻辑上讲,切片是由struct三个字段组成的:一个指向切片的支持数组的指针,该数组实际上包含切片的元素,以及两个ints:切片的容量及其长度。\n当您传递和分配切片值时,struct将复制这些值:一个指针和两个整数。\n如您所见,当您在支持数组周围传递切片值时,不会复制\xe2\x80\x94,仅复制指向它的指针。

\n

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

\n

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

\n
    \n
  • 切片的变量(您将处理的指针)几乎肯定最终会被分配在堆上,以便编译器可以确保它的值能够跨越函数调用的边界\xe2\x80\x94,因此你将为使用内存管理器付费,并且垃圾收集器将有更多工作。
  • \n
  • 使用一层间接可减少数据局部性:访问 RAM 的速度很慢,因此 CPU 具有缓存,可以在读取数据的地址之后的地址预取数据。如果控制流立即读取另一个位置的内存,则预取的数据将被丢弃:缓存垃圾。
  • \n
\n

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

\n
func append(*[]T, T...)\n
Run Code Online (Sandbox Code Playgroud)\n

代替

\n
func append([]T, T...) []T\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n

值切片与值指针切片

\n

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

\n

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

\n

所以,有两个考虑:

\n
    \n
  • 元素是否相当小,以便复制它们不会对内存和 CPU 资源造成压力?

    \n

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

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

    \n
  • \n
\n

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

\n

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

\n

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

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

    \n

    不过,拥有指向切片的指针是有正当理由的。

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

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

关于正确性与性能的警告

\n

上面的文本包含很多性能考虑因素。\n我之所以提出它们,是因为 Go 是一种相当低级的语言:不像C、C++ 和 Rust那样低级,但仍然为程序员提供了足够的回旋空间当性能受到威胁时使用。 \n不过,您应该很好地理解,在您的学习曲线上的这一点上,正确性必须是您的首要目标\xe2\x80\x94如果不是唯一的\xe2\x80\x94目标:请不要冒犯,但是如果您在调整一些 Go 代码以节省一些 CPU 时间来执行它之后,那么您一开始就没有提出您的问题。\n换句话说,请将以上所有内容视为一组事实和注意事项指导您学习和探索该主题,但不要陷入首先考虑性能的陷阱。使您的程序正确且易于阅读和修改。

\n
\n

\xc2\xb9\xc2\xa0 接口值是一对指针:指向包含已放入接口值的值的变量,以及 Go 运行时内描述该变量类型的特殊数据结构。\n因此您可以直接将切片值放入 \xe2\x80\x94 类型的变量中interface{},因为在语言 \xe2\x80\x94 中它完全没问题,如果值的类型本身不是单个指针,则编译器必须在堆上分配一个变量来包含您的值的副本,并将指向该新变量的指针存储到类型的值中interface{}。\n这需要保持“所有内容始终按值传递”语义Go 作业。
\n因此,如果将切片值放入 类型的变量中interface{},最终会在堆上得到该值的副本。\n因此,在数据结构中保留指向切片的指针,例如sync.Map会使代码更丑陋,但结果会更小记忆搅动。

\n

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

\n