幾個(gè)常見(jiàn)的 slice 錯(cuò)誤
最近看到 medium 上有篇文章[1]把關(guān)于 slice 的常見(jiàn)錯(cuò)誤總結(jié)出來(lái)了,有些甚至是老司機(jī)也容易犯的。每個(gè)錯(cuò)誤都先描述問(wèn)題,再給出修改建議,最后再展示一個(gè)代碼樣例。
之前饒大寫(xiě)過(guò)一篇關(guān)于 slice 的文章《深度解密 Go 語(yǔ)言之 Slice》,如果看懂了,很多相關(guān)的問(wèn)題都能理解。
新舊 slice 共用底層數(shù)組問(wèn)題
如果我們用類似 b := a[:3] 這樣的方式基于 a 創(chuàng)建一個(gè)新的 slice,a 和 b 這時(shí)指向同一個(gè)底層數(shù)組。如果對(duì) a 進(jìn)行的一些操作,影響到了底層數(shù)組,最后也會(huì)影響到 b。這個(gè)隱蔽的 bug 可能會(huì)耗費(fèi)你不少時(shí)間來(lái)排查。
修復(fù)
解決辦法很簡(jiǎn)單,從老 slice 拷貝一個(gè)新 slice,這樣對(duì)老 slice 的修改就不會(huì)影響新 slice。
demo
package?main
import?(
?"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?behaviours
?fmt.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?that
?c?:=?[]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
將循環(huán)變量取址賦給 slice問(wèn)題
如果一個(gè) slice 里面的元素是指針類型,當(dāng)我們?cè)诒闅v另一個(gè) slice 的過(guò)程中將循環(huán)變量取址后 append 到這個(gè)指針類型的 slice,那么每次 append 的是其實(shí)是同一個(gè)元素。這是因?yàn)樵谡麄€(gè)循環(huán)的過(guò)程中,循環(huán)變量是同一個(gè),對(duì)它的取址當(dāng)然也是一樣的。
修復(fù)
將循環(huán)變量賦值給一個(gè)新變量,將新變量取址后 append 到這個(gè)指針類型的 slice。
demo
package?main
import?"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?pointer
?for?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?loop
?fmt.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]?=?3
?type?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?loop
?aa?:=?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?appended
?bb?:=?make([]*A,?0)
?for?_,?a?:=?range?b?{
??a?:=?a
??bb?=?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}
}
當(dāng)函數(shù)參數(shù)是 slice 時(shí),執(zhí)行 append問(wèn)題
當(dāng)一個(gè)函數(shù)的參數(shù)(形參)是 slice 時(shí),如果在函數(shù)內(nèi)部向這個(gè) slice append 元素,那么原始的 slice(實(shí)參)將不受影響。因?yàn)?append 之后會(huì)形成一個(gè)新的 slice,原 slice 不會(huì)變。
修復(fù)
- 將形參 slice,改成指針類型:*slice,并且將 append 之后得到的 slice 賦給這個(gè)指針。
- 或者將新 slice 通過(guò)返回值返回后,將它賦給原來(lái)的那個(gè) slice。
demo
package?main
import?"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?elements
?myAppend(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?function
?myAppend2(&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 時(shí)企圖改變?cè)刂?h2 style="font-weight:bold;font-size:22px;color:rgb(0,150,136);padding-left:10px;">問(wèn)題當(dāng)我們?cè)诒闅v一個(gè) slice 時(shí),如果想通過(guò)循環(huán)變量需要改變?cè)刂怠R驗(yàn)檠h(huán)變量只是 slice 元素的一個(gè)拷貝,修改循環(huán)變量并不能影響原來(lái)的 slice。
修復(fù)
想要修改原 slice,用切片下標(biāo)來(lái)訪問(wèn) slice 元素并做修改。
demo
package?main
import?"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?changed
?for?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 元素來(lái)構(gòu)建不同 slice問(wèn)題
如果想通過(guò)每次向原 slice append 不同的元素,從而創(chuàng)建出多個(gè) slice。假如原 slice 的容量恰好夠用,那么這些新創(chuàng)建的 slice 和最后創(chuàng)建出來(lái)的 slice 內(nèi)容相同。
修復(fù)
明確指定長(zhǎng)度來(lái)創(chuàng)建一個(gè)新 slice,并使用 copy 將原 slice 拷貝到新 slice。之后,將元素 ?append 到新 slice。
demo
package?main
import?"fmt"
func?main()?{
?a?:=?make([]int,?3,?10)
?a[0],?a[1],?a[2]?=?1,?2,?3
?b?:=?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?append
?b?=?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]
}
使用內(nèi)建的 copy 函數(shù)向一個(gè)空的 slice 里拷貝元素問(wèn)題
向一個(gè)空的 slice 里面拷貝元素什么也不會(huì)發(fā)生。拷貝時(shí),只有 min(len(a), len(b)) 個(gè)元素會(huì)被成功拷貝。
修復(fù)
想從原 slice 拷貝多少個(gè)元素過(guò)來(lái),就先創(chuàng)建一個(gè)指定長(zhǎng)度的 slice,再執(zhí)行拷貝。
demo
package?main
import?"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?elements
?b?:=?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
}
可以看到,如果對(duì)之前的那篇文章有足夠的理解,這些錯(cuò)誤一眼就能看出原因。不過(guò)理解是一回事,平時(shí)在工作中其實(shí)也難免寫(xiě)出有問(wèn)題的代碼來(lái)。多看看類似的陷阱總會(huì)有好處。
參考資料
[1]文章: https://medium.com/@nsspathirana/common-mistakes-with-go-slices-95f2e9b362a9
