我正在瀏覽A Go of Go修改一些基礎知識,並遇到了一個練習,在該練習中,您必須編寫一個簡單的結構來實現Go io.Reader接口。 io.Reader具有單個Read(b [] byte)(int,error)方法,該方法應從接口實現保存的數據中讀取並寫入b。
我首先嚐試使用Go內置的附加功能對Read方法進行簡單的實現,但這種方法不起作用,最終我意識到我應該使用內置的複製功能。 但是,我也意識到,在修改切片時,我經常會犯錯,並且想了解,並寫下創建,傳遞給函數並使用附加或複製函數進行修改時實際發生的情況。
本文中的代碼示例僅用於調試/學習目的,並不打算在實際程序中使用。
Go函數參數
在Go函數中,參數(和接收器)是通過副本而不是引用傳遞的,這意味著該函數將接收調用該函數時傳遞的值的副本。 如果在函數內部修改了類似struct的值,則應該為函數提供指向該結構的指針,否則只能在函數內部修改該結構的副本,而更改不會反映在函數外部。
<code>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/<code>
切片也通過副本傳遞,但是由於切片不保存實際數據,因此僅指向基礎數組的指針,因此副本仍將指向同一數組,並且可以通過一個切片指向另一個數組來更改數組 指向同一數組的切片。
<code>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\\n", 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/<code>
內建附加功能
因此,我認為我可以使用一個非常簡單的Read方法進行測試,使用append來修改byte slice參數:
<code>func main() {
b := make([]byte, 8)
m := MyReader{}
m.Read(b)
fmt.Printf("b in main: %v\\n", b)
}
func (m *MyReader) Read(b []byte) (int, error) {
b = append(b, byte('A')) // Placeholder data
fmt.Printf("b in Read: %v\\n", 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/<code>
上面的代碼不起作用的原因與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個元素的地址,非常有用:
<code>func main() {
b := make([]byte, 8)
r := MyReader{}
r.Read(b)
fmt.Printf("%p\\n", b) // 0x40e020
}
func (m *MyReader) Read(b []byte) (int, error) {
fmt.Printf("%p\\n", b) // 0x40e020
b = append(b, byte('A'))
fmt.Printf("%p\\n", b) // 0x40e040
return 1, nil
}
type MyReader struct {}
// After running append, memory address of b[0] has changed/<code>
我可以通過確保有一些額外的容量來附加byte('A')來解決此問題。 在這裡,即使運行追加後,切片仍指向同一數組(但main中的切片仍具有初始數組):
<code>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\\n", 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\\n", b, b)
b = append(b, byte('A'))
fmt.Printf("In Read after calling append: b is %v, addr of b[0] is %p\\n", 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/<code>
上面的代碼未按預期工作的原因是因為main中的slice b的長度仍為0,因此我們看不到寫入數組的新元素。 我們可以通過在調用Read之後調整main中slice的大小來解決此問題:
<code>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\\n", b) // [65]
}
func (m *MyReader) Read(b []byte) (int, error) {
b = append(b, byte('A'))
return 1, nil
}
type MyReader struct {}/<code>
現在main中的b片具有append所做的更改。 但是,基於附加的解決方案將有許多問題。 必須調整main內部的切片大小以查看寫入的值很笨拙。 但最重要的是,實際上應該以某種方式實現Reader接口,即Read(b [] byte)(int,error)函數每次調用都會將len(b)個字節讀入b。 這可能無法使用append乾淨地完成。
複製
copy(d,s [] T)int函數將元素從源切片複製到目標切片,並返回複製的元素數。
<code>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\\n", d, n) // d: [foo bar baz], n: 3
s1 := []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\\n", d1, n1) // d1: [alpha beta], n1: 2
}/<code>
複製函數可在Read(b [] byte)(int,error)內部使用,以從初始化Reader的任何源複製tolen(b)個元素,並返回複製的元素數。 我們將要跟蹤已複製了源中的多少個元素,並在複製了所有元素後返回特定錯誤(io.EOF)。
<code>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\\n", err)
}
fmt.Printf("n: %v, b: %v\\n", n, b)
}
}
// Reads len(b) bytes to b
func (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 MyReader
func NewReader(s string) *MyReader {
return &MyReader{
contents: []byte(s),
}
}
// Holds contents slice and index of where to read from in contents
type 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]/<code>
這是一個簡單的測試實現,並且不考慮源包含由多個字節組成的字符時會發生什麼情況(因為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)
"閱讀更多 聞數起舞 的文章