我正在浏览A Go of Go修改一些基础知识,并遇到了一个练习,在该练习中,您必须编写一个简单的结构来实现Go io.Reader接口。 io.Reader具有单个Read(b [] byte)(int,error)方法,该方法应从接口实现保存的数据中读取并写入b。
我首先尝试使用Go内置的附加功能对Read方法进行简单的实现,但这种方法不起作用,最终我意识到我应该使用内置的复制功能。 但是,我也意识到,在修改切片时,我经常会犯错,并且想了解,并写下创建,传递给函数并使用附加或复制函数进行修改时实际发生的情况。
本文中的代码示例仅用于调试/学习目的,并不打算在实际程序中使用。
Go函数参数
在Go函数中,参数(和接收器)是通过副本而不是引用传递的,这意味着该函数将接收调用该函数时传递的值的副本。 如果在函数内部修改了类似struct的值,则应该为函数提供指向该结构的指针,否则只能在函数内部修改该结构的副本,而更改不会反映在函数外部。
func main() { m := MyStruct{"foo"} ModifyStructFunc(m, "bar") fmt.Printf("In main: m is %+v, pointer to m is %p", m, &m)}func ModifyStructFunc(m MyStruct, s string) { m.X = s fmt.Printf("In ModifyStructFunc m is %+v, pointer to m is %p", m, &m)}type MyStruct struct { X string }// Outputs:// In ModifyStructFunc: m is {X:bar}, pointer to m is 0x40c140// In main: m is {X:foo}, pointer to m is 0x40c138// Here, the change made in ModifyStructFunc is not seen in main// The different pointers show that those are different structs
切片也通过副本传递,但是由于切片不保存实际数据,因此仅指向基础数组的指针,因此副本仍将指向同一数组,并且可以通过一个切片指向另一个数组来更改数组 指向同一数组的切片。
func main() { s := []int{0} ModifySliceFunc(s) fmt.Printf("In main: s is %v, pointer to s is %p", s, &s)}func ModifySliceFunc(s []int) { s[0] = 999 fmt.Printf("In ModifySliceFunc: s is %v, pointer to s is %p", s, &s)}// Outputs:// In ModifySliceFunc: s is [999], pointer to s is 0x40a0f0// In main: s is [999], pointer to s is 0x40a0e0// Here, the change made in ModifySliceFunc is visible in main too// The different pointers show that these are different slices
内建附加功能
因此,我认为我可以使用一个非常简单的Read方法进行测试,使用append来修改byte slice参数:
func main() { b := make([]byte, 8) m := MyReader{} m.Read(b) fmt.Printf("b in main: %v", b)}func (m *MyReader) Read(b []byte) (int, error) { b = append(b, byte('A')) // Placeholder data fmt.Printf("b in Read: %v", b) return 1, nil // Some placeholder values}type MyReader struct {}// Outputs:// b in Read: [0 0 0 0 0 0 0 0 65]// b in main: [0 0 0 0 0 0 0 0]// Does not work- after calling Read method, b still has the initial value
上面的代码不起作用的原因与Go slice以及内置的make和append函数的工作方式有关。
切片包含对基础数组,长度(其指向的数组段的长度)和容量(从切片段中第一个元素开始的底层数组的总长度)的引用。 可以调整切片的大小,使其指向基础数组的不同段。
make(slice [] T,len,cap int)函数可用于初始化新的切片。 它接受三个参数:类型,长度和可选容量(默认情况下,容量等于长度)。 它将使元素个数上限的数组归零,并返回一个切片,该切片指向长度为len的数组段。
append(slice [] T,args…T)[] T函数将args附加到slice。 它使用切片的调整大小功能-如果在切片的分段之后,底层数组的部分中有足够的容量,则将在切片的最后一个元素之后将args写入现有数组,并且将调整切片的大小以包含args和 回。 如果没有足够的容量,将创建一个新的数组,切片中的元素以及写入新数组的args,并返回指向新数组的切片。
在上面的代码示例中,b:= make([] byte,8)创建一个长度为8的数组和一个长度也为8的slice b。调用append时,没有足够的容量来写入byte('A' )到旧数组,从而创建一个新数组,将sliceb和byte(A)的内容写入新数组,最后返回指向该新数组的切片的指针。 同时,main中的切片b仍指向旧数组。
我想观察数组的变化,找到Go的%p打印动词,它对一个切片显示第0个元素的地址,非常有用:
func main() { b := make([]byte, 8) r := MyReader{} r.Read(b) fmt.Printf("%p", b) // 0x40e020}func (m *MyReader) Read(b []byte) (int, error) { fmt.Printf("%p", b) // 0x40e020 b = append(b, byte('A')) fmt.Printf("%p", b) // 0x40e040 return 1, nil}type MyReader struct {}// After running append, memory address of b[0] has changed
我可以通过确保有一些额外的容量来附加byte('A')来解决此问题。 在这里,即使运行追加后,切片仍指向同一数组(但main中的切片仍具有初始数组):
func main() { // make a slice of length 0 with underlying array of length 8 b := make([]byte, 0, 8) r := MyReader{} r.Read(b) fmt.Printf("In main after calling Read: b is %v, addr of b[0] is %p", b, b)}func (m *MyReader) Read(b []byte) (int, error) { fmt.Printf("In Read before calling append: b is %v, addr of b[0] is %p", b, b) b = append(b, byte('A')) fmt.Printf("In Read after calling append: b is %v, addr of b[0] is %p", b, b) return 1, nil}type MyReader struct {}// Outputs:// In Read before calling append: b is [], addr of b[0] is 0x40e020// In Read after calling append: b is [65], addr of b[0] is 0x40e020// In main after calling Read: b is [], addr of b[0] is 0x40e020// Address of b[0] has not changed after calling append, so// there has not been a new array created// However - slice b in main still has the initial value even after calling Read
上面的代码未按预期工作的原因是因为main中的slice b的长度仍为0,因此我们看不到写入数组的新元素。 我们可以通过在调用Read之后调整main中slice的大小来解决此问题:
func main() { // make a slice of length 0 pointing at an array of length 8 b := make([]byte, 0, 8) r := MyReader{} r.Read(b) // Resize slice b to include the 0th element of the array b = b[:1] fmt.Printf("%v", b) // [65]}func (m *MyReader) Read(b []byte) (int, error) { b = append(b, byte('A')) return 1, nil}type MyReader struct {}
现在main中的b片具有append所做的更改。 但是,基于附加的解决方案将有许多问题。 必须调整main内部的切片大小以查看写入的值很笨拙。 但最重要的是,实际上应该以某种方式实现Reader接口,即Read(b [] byte)(int,error)函数每次调用都会将len(b)个字节读入b。 这可能无法使用append干净地完成。
复制
copy(d,s [] T)int函数将元素从源切片复制到目标切片,并返回复制的元素数。
func main() { s := []string{"foo", "bar", "baz"} d := make([]string, 3) // will copy s -> d and return number of elements copied n := copy(d, s) fmt.Printf("d: %v, n: %d", d, n) // d: [foo bar baz], n: 3s1 := []string{"alpha", "beta", "gamma"} d1 := make([]string, 2) // len(d1) < len(s), so will only copy len(d) elements n1 := copy(d1, s1) fmt.Printf("d1: %v, n1: %d", d1, n1) // d1: [alpha beta], n1: 2}
复制函数可在Read(b [] byte)(int,error)内部使用,以从初始化Reader的任何源复制tolen(b)个元素,并返回复制的元素数。 我们将要跟踪已复制了源中的多少个元素,并在复制了所有元素后返回特定错误(io.EOF)。
func main() { m := NewReader("One two three..") // bytes from MyReader will be written to this slice b := make([]byte, 8) for { n, err := m.Read(b) // break if all bytes have been read if err == io.EOF { break } if err != nil { log.Fatalf("Error: %v", err) } fmt.Printf("n: %v, b: %v", n, b) }}// Reads len(b) bytes to bfunc (m *MyReader) Read(b []byte) (int, error) { if m.index >= len(m.contents) { return 0, io.EOF } c := copy(b, m.contents[m.index:]) // Move the index forward by the number of bytes copied m.index += c return c, nil}// Initialises a new MyReaderfunc NewReader(s string) *MyReader { return &MyReader{ contents: []byte(s), }}// Holds contents slice and index of where to read from in contentstype MyReader struct { contents []byte index int}// Outputs:// n: 8, b: [79 110 101 32 116 119 111 32]// n: 7, b: [116 104 114 101 101 46 46 32]
这是一个简单的测试实现,并且不考虑源包含由多个字节组成的字符时会发生什么情况(因为Read可能不会拆分字符)。 Go字符串包中的Theio.Reader实现显示了如何解决此问题。
结论:
· append(s [] T,args … T)[] T将args附加到切片。 如果容量不足,将创建一个新阵列。 返回修改后的切片
· copy(d,s [] T)int将元素从源复制到目标切片。 如果len(d)
· 尽管所有Go函数参数都是通过副本传递的,但对参考值(例如切片)的更改可能会影响调用方作用域中的值
· Go%p打印动词对于调试非常有用
(本文翻译自Irbe Krumina的文章《Go slices, functions, append and copy》,参考:https://medium.com/@irbekrm/go-slices-functions-append-and-copy-e4afa7646ec4)