不理解切片和指针

Not understanding slices and pointers

当前项目让我获取一个结构(带有注释标记)并将数据作为平面文件写出。该文件是一个分栏文件,因此数据的定位很重要。这些位置和长度在我的字段级别的结构标签中设置。

我遇到的问题是,我正在将指向我的 []byte 结果切片的指针传递给我的函数,但无论我做什么,原始切片都没有容纳数据。这是一个简短的示例代码,演示了我在做什么。

package main

import (
    "fmt"
    "strconv"
)

func writeInt(value int, fieldData *[]byte, col, length int) {
    v := fmt.Sprintf("%+0" + strconv.Itoa(length) +"d", value)
    copyData(fieldData, v, col, length)
}

func writeString(value string, fieldData *[]byte, col, length int) {
    v := fmt.Sprintf("%-" + strconv.Itoa(length) + "s", value)
    copyData(fieldData, v, col, length)
}

func copyData(fieldData *[]byte, v string, col, length int) {
    data := *fieldData
    if len(data) < col + length {
        temp := make([]byte, col + length - 1)
        copy(temp, data)
        data = temp
    }
    copy(data[col - 1:length], v)
    fieldData = &data
}

func main() {
    var results []byte

    writeInt(13, &results, 1, 3)
    writeString("TEST", &results, 4, 10)

    fmt.Print(results)

}

预期结果(字符串形式)应为:

'013TEST      ' - zero pad in front of int and space pad behind string

但我得到 []

我是不是完全看错了,或者我只是不明白什么?

事先注意:不要使用指向切片的指针(切片已经很小 headers 指向后备数组)。您可以在没有指针的情况下修改元素,如果您需要修改 header(例如向其添加元素),return 新切片,就像内置 append() 一样。

此外,使用 bytes.Buffer 类型更容易实现您尝试做的事情。它实现了 io.Writer,您可以直接写入它(甚至使用 fmt.Fprint()),并以 []bytestring.

的形式获取其内容

每个参数都是传递值的副本。修改参数只修改本副本

如果你这样做:

fieldData = &data

尽管 fieldData 是一个指针,但您只是在修改副本。您必须修改 pointed 值:

*fieldData = data

打印结果:

fmt.Println(results)
fmt.Printf("%q\n", string(results))

输出(在 Go Playground 上尝试):

[43 49 51 84 69 83 84 32 32 32 0 0 0]
"+13TEST   \x00\x00\x00"

请参阅 ,尤其是“事先注意”部分,以了解您的 具体情况 。有关指针的一般性讨论,请继续阅读;请注意,其中只有一部分特定于 Go 本身。

指针为您所做的是为您提供间接级别。某些语言(包括 Go)使用“按值传递”机制,无论您是将常量值还是变量传递给某个函数:

f(3)

或:

f(x)

函数f接收,而不是变量的名称或类似的任何东西。 (其他语言不同,在某些情况下具有“按名称传递”、1“按引用传递”或“value-result”语义。另请参阅 Why [do] so many languages [use pass] by value? f 接收值而不是变量名或类似的东西这一事实在 没有 变量时很有用,例如 f(3) 的情况,或:

f(x + y)

我们必须首先进行求和,因此不涉及 单个 变量。

现在,特别是在 Go 中,函数可以而且经常有多个 return 值:

func g(a int) (bool, int, error) { ... }

所以如果我们希望能够更新一些东西,我们可以写:

ok, x, err = g(x)

收集所有三个值,包括更新的 x,就在我们想要的地方。但这确实暴露了更新的细节 x,and/or 可能是不方便的。如果我们想给一些函数 permissionchange 一些存储在某个变量中的值,我们可以定义我们的函数来接受一个 指向那个变量的指针:

func g2(a *int) (bool, error) { ... }

现在我们可以写 ok, err = g2(&x).

而不是 ok, x, err = g(x)

对于这种特殊情况,这根本算不上什么改进。但是假设 int 现在不是一个简单的 int,而是一个具有一堆复杂状态的结构,例如,将从一系列文件中读取输入的东西,自动切换到下一个文件:

x := multiFileReader(...)

现在如果我们希望 multi-file-reader 的部分能够访问 x 表示的任何结构中的各个字段,我们可以使 x 本身成为一个指针变量,指向struct。那么:

str, err := readNextString(x)

传递一个指针(因为 x 一个指针)允许 readNextString 更新 x 中的一些字段。 (如果 x 是一个方法,例如 io.Reader,同样的通用逻辑也适用,但在这种情况下,我们开始使用 interface,这增加了一堆额外的皱纹。我忽略了那些在这里专注于指针方面。)

添加间接会增加复杂性

当我们做这种事情时——传递一个指向原始变量的指针值,其中原始变量本身保存一些初始值或中间值,我们在更新时更新它继续——接收这个指针值的函数现在有一个指向T类型的变量T。这个额外的变量一个变量。这意味着我们可以为其分配一个新值。如果当我们这样做时,我们将丢失原始值:

func g2(a *int) (bool, error) {
    ... section 1: do stuff with `*a` ...
    var another int
    a = &another
    ... section 2: do more stuff with `*a` ...
    return ok, err
}

在第 1 节中,a 指向 ,而 *a 因此 指向 ,任何变量调用者传递给 g2。对 *a 所做的更改将显示在那里。但在第 2 节中,a 指向 another,并且 *a 最初为零(因为 another 为零)。在此处对 *a 进行更改 不会 显示在调用方中。

如果愿意,您可以避免直接使用 *a

func g2(a *int) (bool, error) {
    b := *a
    p := &b
    var ok bool
    // presumably there's a `var err error` or equivalent too
    for attempts := 0; !ok && attempts < 5; attempts++ {
        ... do things ...
        ... if things are working well, set ok = true and set p = a ...
        ... update *p ...
    }
    return ok, err
}

有时这就是您想要的:如果事情进展顺利,我们会小心不要覆盖*a 通过改写 b,但是如果事情 进展顺利,我们通过让 p 指向 a 来覆盖 *a “更新 *p”部分。但更难推理的是:在诉诸此类事情之前,请确保您获得了非常明显的好处。

当然,如果我们有一个指向某个变量的指针存储在一个变量中,我们也可以使用一个指向那个变量的指针:i := 3; a := &i; pa := &a。这让我们有机会添加另一个间接级别 ppa := &pa,这给了我们另一个机会,依此类推。它是 海龟一路向下 指针一直向上,除了在最后我们必须有某种最终答案,如 i.


1Pass-by-name 特别棘手,而且不是很常见;请参阅 What is "pass-by-name" and how does it work exactly? 但这确实引发了尼克劳斯·沃思 (Niklaus Wirth) 曾经讲过的一个关于他自己的笑话,有些人会说“Nick-louse Veert”,因此会直呼他的名字,而其他人会说“Nickle's Worth”因此按价值称呼他。 (我想我是间接听到的——我不认为我在 U 的时候他来过。)