几个常见的 slice 错误
最近看到 medium 上有篇文章[1]把關于 slice 的常見錯誤總結出來了,有些甚至是老司機也容易犯的。每個錯誤都先描述問題,再給出修改建議,最后再展示一個代碼樣例。
之前饒大寫過一篇關于 slice 的文章《深度解密 Go 語言之 Slice》,如果看懂了,很多相關的問題都能理解。
新舊 slice 共用底層數組
問題
如果我們用類似 b := a[:3] 這樣的方式基于 a 創建一個新的 slice,a 和 b 這時指向同一個底層數組。如果對 a 進行的一些操作,影響到了底層數組,最后也會影響到 b。這個隱蔽的 bug 可能會耗費你不少時間來排查。
修復
解決辦法很簡單,從老 slice 拷貝一個新 slice,這樣對老 slice 的修改就不會影響新 slice。
demo
package?mainimport?("fmt""sort" )func?main()?{a?:=?[]int{1,?2,?3,?4,?5,?6,?7,?8,?9}b?:=?a[4:7]fmt.Printf("before?sorting?a,?b?=?%v\n",?b)?//?before?sorting?a,?b?=?[5?6?7]sort.Slice(a,?func(i,?j?int)?bool?{return?a[i]>?a[j]})//?b?is?not?[5,6,7]?anymore.?If?we?code?something?to?use?[5,6,7]?in?b,?then//?there?can?be?some?unpredicted?behavioursfmt.Printf("after?sorting?a,?b?=?%v\n",?b)?//?after?sorting?a,?b?=?[5?4?3]//?Fix://?To?avoid?that,?that?part?of?the?slice?should?be?copied?to?a?different?slice.//?Then?that?values?are?in?different?underlying?array?and?changes?to?a?will//?not?be?affected?to?thatc?:=?[]int{1,?2,?3,?4,?5,?6,?7,?8,?9}d?:=?make([]int,?3)copy(d,?c[4:7])fmt.Printf("before?sorting?c,?d?=?%v\n",?d)?//?before?sorting?c,?d?=?[5?6?7]sort.Slice(c,?func(i,?j?int)?bool?{return?c[i]>?c[j]})fmt.Printf("after?sorting?c,?d?=?%v\n",?d)?//after?sorting?c,?d?=?[5?6?7] } view?raw將循環變量取址賦給 slice
問題
如果一個 slice 里面的元素是指針類型,當我們在遍歷另一個 slice 的過程中將循環變量取址后 append 到這個指針類型的 slice,那么每次 append 的是其實是同一個元素。這是因為在整個循環的過程中,循環變量是同一個,對它的取址當然也是一樣的。
修復
將循環變量賦值給一個新變量,將新變量取址后 append 到這個指針類型的 slice。
demo
package?mainimport?"fmt"func?main()?{a?:=?make([]*int,?0)//?simplest?scenario?of?the?mistake.?create?*int?slice?and//?put?elements?in?a?loop?using?iterator?variable's?pointerfor?i?:=?0;?i?<?3;?i++?{a?=?append(a,?&i)}//?all?elements?have?same?pointer?value?and?value?is?the?last?value?of//?the?iterator?variable?because?i?is?the?same?variable?throughout?the?loopfmt.Printf("a?=?%v\n",?a)?//?a?=?[0xc000018058?0xc000018058?0xc000018058]fmt.Printf("a[0]?=?%v,?a[1]?=?%v,?a[2]?=?%v\n\n",?*a[0],?*a[1],?*a[2])//?a[0]?=?3,?a[1]?=?3,?a[2]?=?3type?A?struct?{a?int}b?:=?[]A{{a:?2},{a:?4},{a:?6},}//?append?pointer?to?iteration?variable?a?and?it's?memory?address?is?same//?through?out?the?loop?so?all?the?elements?will?append?same?pointer?and?value//?is?the?last?value?of?the?loop?because?a?is?the?same?variable?throughout//?the?loopaa?:=?make([]*A,?0)for?_,?a?:=?range?b?{aa?=?append(aa,?&a)}fmt.Printf("aa?=?%v\n",?a)?//?aa?=?[0xc000018058?0xc000018058?0xc000018058]fmt.Printf("aa[0]?=?%v,?aa[1]?=?%v,?aa[2]?=?%v\n\n",?*aa[0],?*aa[1],?*aa[2])//?aa[0]?=?{6},?aa[1]?=?{6},?aa[2]?=?{6}//?Fix://?To?avoid?that?iteration?value?should?be?copied?to?a?different?variable//?and?pointer?to?that?should?be?appendedbb?:=?make([]*A,?0)for?_,?a?:=?range?b?{a?:=?abb?=?append(bb,?&a)}fmt.Printf("bb?=?%v\n",?a)?//?bb?=?[0xc000018058?0xc000018058?0xc000018058]fmt.Printf("bb[0]?=?%v,?bb[1]?=?%v,?bb[2]?=?%v\n",?*bb[0],?*bb[1],?*bb[2])//?bb[0]?=?{2},?bb[1]?=?{4},?bb[2]?=?{6}}當函數參數是 slice 時,執行 append
問題
當一個函數的參數(形參)是 slice 時,如果在函數內部向這個 slice append 元素,那么原始的 slice(實參)將不受影響。因為 append 之后會形成一個新的 slice,原 slice 不會變。
修復
將形參 slice,改成指針類型:*slice,并且將 append 之后得到的 slice 賦給這個指針。
或者將新 slice 通過返回值返回后,將它賦給原來的那個 slice。
demo
package?mainimport?"fmt"func?main()?{a?:=?[]int{1,?2,?3}fmt.Printf("before?append,?a?=?%v\n",?a?)?//?before?append,?a?=?[1?2?3]//?a?is?not?changed?because?append?returns?a?new?slice?with?appended?elementsmyAppend(a,?4)fmt.Printf("after?append,?a?=?%v\n",?a?)?//?after?append,?a?=?[1?2?3]//?Fix://?to?fix?this,?use?pointer?to?slice?to?append?in?separate?function?or//?get?returned?appended?slice?from?that?functionmyAppend2(&a,?4)fmt.Printf("after?append?with?pointer,?a?=?%v\n",?a?)//?after?append?with?pointer,?a?=?[1?2?3?4]a?=?myAppend3(a,?5)fmt.Printf("after?append?with?return,?a?=?%v\n",?a?)//?after?append?with?return,?a?=?[1?2?3?4?5]}func?myAppend(a?[]int,?i?int)?{a?=?append(a,?i) }func?myAppend2(a?*[]int,?i?int)??{*a?=?append(*a,?i) }func?myAppend3(a?[]int,?i?int)?[]int?{a?=?append(a,?i)return?a }用 range 遍歷 slice 時企圖改變元素值
問題
當我們在遍歷一個 slice 時,如果想通過循環變量需要改變元素值。因為循環變量只是 slice 元素的一個拷貝,修改循環變量并不能影響原來的 slice。
修復
想要修改原 slice,用切片下標來訪問 slice 元素并做修改。
demo
package?mainimport?"fmt"func?main()?{a?:=?[]int{1,?2,?3}fmt.Printf("before?adding?1?to?elements,?a?=?%v?\n",?a)//?before?adding?1?to?elements,?a?=?[1?2?3]for?_,?n?:=?range?a?{n?+=?1}//?slice?elements?haven't?changed?because?n?is?a?copy?of?slice?elements.fmt.Printf("after?adding?1?to?elements,?a?=?%v?\n",?a)//?after?adding?1?to?elements,?a?=?[1?2?3]//?Fix://?to?change?that?address?the?elements?with?the?index?and?it?should?be?changedfor?i,?_?:=?range?a?{a[i]?+=?1}fmt.Printf("after?adding?1?to?elements?with?index,?a?=?%v?\n",?a)//?after?adding?1?to?elements?with?index,?a?=?[2?3?4] }向相同 slice append 元素來構建不同 slice
問題
如果想通過每次向原 slice append 不同的元素,從而創建出多個 slice。假如原 slice 的容量恰好夠用,那么這些新創建的 slice 和最后創建出來的 slice 內容相同。
修復
明確指定長度來創建一個新 slice,并使用 copy 將原 slice 拷貝到新 slice。之后,將元素 ?append 到新 slice。
demo
package?mainimport?"fmt"func?main()?{a?:=?make([]int,?3,?10)a[0],?a[1],?a[2]?=?1,?2,?3b?:=?append(a,?4)c?:=?append(a,?5)//?c?==?b?because?both?refer?to?same?underlying?array?and?capacity?of?that?is?10//?so?appending?to?a?will?not?create?new?array.fmt.Printf("b?=?%v?\n",?b)?//?b?=?[1?2?3?5]fmt.Printf("c?=?%v?\n\n",?c)?//?c?=?[1?2?3?5]//?fix://?to?avoid?this,?a?should?be?copied?to?b?and?c?and?then?appendb?=?make([]int,?3)copy(b,?a)b?=?append(b,?4)c?=?make([]int,?3)copy(c,?a)c?=?append(c,?5)fmt.Printf("after?copy\n")fmt.Printf("b?=?%v?\n",?b)?//?b?=?[1?2?3?4]fmt.Printf("c?=?%v?\n",?c)?//?c?=?[1?2?3?5] }使用內建的 copy 函數向一個空的 slice 里拷貝元素
問題
向一個空的 slice 里面拷貝元素什么也不會發生。拷貝時,只有 min(len(a), len(b)) 個元素會被成功拷貝。
修復
想從原 slice 拷貝多少個元素過來,就先創建一個指定長度的 slice,再執行拷貝。
demo
package?mainimport?"fmt"func?main()?{a?:=?[]int{2,?4,?6}//?b?has?no?any?elements?of?a?because?copy?copies//?min(len(a),?len(b))?number?of?elementsb?:=?make([]int,?0)n?:=?copy(b,?a)fmt.Printf("b?=?%v\n",?b)?//?b?=?[]fmt.Printf("%d?elements?copied?from?a?to?b\n\n",?n)//?0?elements?copied?from?a?to?b//Fix:?make?slice?with?a?length?that?number?of?elements?need?to?be?copied.c?:=?make([]int,?2)n?=?copy(c,?a)fmt.Printf("c?=?%v\n",?c)?//?c?=?[2?4]fmt.Printf("%d?elements?copied?from?a?to?c\n\n",?n)//?2?elements?copied?from?a?to?c }可以看到,如果對之前的那篇文章有足夠的理解,這些錯誤一眼就能看出原因。不過理解是一回事,平時在工作中其實也難免寫出有問題的代碼來。多看看類似的陷阱總會有好處。
參考資料
[1]
文章: https://medium.com/@nsspathirana/common-mistakes-with-go-slices-95f2e9b362a9
總結
以上是生活随笔為你收集整理的几个常见的 slice 错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 内联函数和编译器对Go代码的优化
- 下一篇: 以高并发著称的 Go 如何与 MySQL