不理解切片和指针
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()
),并以 []byte
或 string
.
的形式获取其内容
每个参数都是传递值的副本。修改参数只修改本副本
如果你这样做:
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 可能是不方便的。如果我们想给一些函数 permission 来 change 一些存储在某个变量中的值,我们可以定义我们的函数来接受一个 指向那个变量的指针:
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 的时候他来过。)
当前项目让我获取一个结构(带有注释标记)并将数据作为平面文件写出。该文件是一个分栏文件,因此数据的定位很重要。这些位置和长度在我的字段级别的结构标签中设置。
我遇到的问题是,我正在将指向我的 []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()
),并以 []byte
或 string
.
每个参数都是传递值的副本。修改参数只修改本副本
如果你这样做:
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)使用“按值传递”机制,无论您是将常量值还是变量传递给某个函数:
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 可能是不方便的。如果我们想给一些函数 permission 来 change 一些存储在某个变量中的值,我们可以定义我们的函数来接受一个 指向那个变量的指针:
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 的时候他来过。)