切片在My Code中隨處可用。如果我正在使用 MongoDB 中的資料,它將儲存在切片中。如果我需要在運行操作後跟蹤一系列問題,它將儲存在一個切片中。如果你還不瞭解切片是如何工作的,或者像我在開始時一樣避免使用切片,請閱讀下面這兩篇文章以瞭解更多資訊。[理解 Go 中的 slice](http://www.goinggo.net/2013/08/understanding-slices-in-go-programming.html)[Go 中不定長度集合](https://studygolang.com/articles/14132)我在編碼時經常問自己的一個問題是,“我想使用指向這個值的指標還是我想製作一個副本?” 雖然 Go 可以用作函數式程式設計語言,但它本質上卻是一種命令式程式設計語言。這有什麼不同?函數式程式設計語言不允許你在建立且初始設定變數或值之後改變他們。這意味著變數和值是不可變的,它們不能被更改。如果你需要更改變數的狀態或值,則必須創造一個副本並初始化副本為更改後的變數和值。函數始終是傳遞副本,傳回值也始終是副本。在命令式程式設計語言中,我們可以建立可變或可更改的變數和值。我們可以將任何變數或值的指標傳遞給函數,而函數又可以根據需要更改狀態。函數式程式設計語言希望您根據輸入併產生結果的數學函數方式來進行思考。在命令式程式設計語言中,我們可以構建類似的函數,但是我們也可以構建對可以存在於記憶體中的任何位置的狀態執行操作的函數。能夠使用指標有優勢,但也可以讓你陷入困境。使用指標可以減輕記憶體約束並儘可能的提高效能。但它會創造同步問題,例如對值和資源的共用訪問。找到每個用例最適合的解決方案。對於你的 Go 程式,我建議在安全和實用的時候使用指標。Go 是一種命令式程式設計語言,所以利用好它的這些優勢。在 Go 中,一切都是按值傳遞的,記住這一點非常重要。我們可以通過值傳遞對象的地址,或者通過值傳遞對象的副本。當我們在 Go 中使用指標時,它有時會令人混淆,因為 Go 處理我們的所有引用。不要誤會我的意思,Go做到這一點非常棒,但有時候你可以忘記變數的實際值。在每個程式的某個時刻,我需要迭代一個切片來執行一些工作。在 Go 中,我們使用 for 迴圈結構來迭代切片。在開始時,我在迭代切片時犯了一些非常嚴重的錯誤,因為我誤解了 range 關鍵字是如何工作的。我將向您展示一個令人討厭的錯誤,我建立了一個讓我困惑的迭代切片的功能。現在對我來說很明顯為什麼代碼執行結果不對,但當時並不知道原因。讓我們建立一些簡單的值並將它們放在切片中。然後我們將迭代切片,看看會發生什麼。```gopackage mainimport ( "fmt")type Dog struct { Name string Age int}func main() { jackie := Dog{ Name: "Jackie", Age: 19, } fmt.Printf("Jackie Addr: %p\n", &jackie) sammy := Dog{ Name: "Sammy", Age: 10, } fmt.Printf("Sammy Addr: %p\n", &sammy) dogs := []Dog{jackie, sammy} fmt.Println("") for _, dog := range dogs { fmt.Printf("Name: %s Age: %d\n", dog.Name, dog.Age) fmt.Printf("Addr: %p\n", &dog) fmt.Println("") }}```該程式建立兩隻類型為狗的對象,並將它們放入狗類型的切片 dogs 中。我們顯示每隻狗的地址。然後我們迭代顯示每隻狗的名字,年齡和地址的切片。這是該程式的輸出:```Jackie Addr: 0x2101bc000Sammy Addr: 0x2101bc040Name: Jackie Age: 19Addr: 0x2101bc060Name: Sammy Age: 10Addr: 0x2101bc060```那麼為什麼狗的值在迴圈內是不同的,為什麼同一個地址出現兩次呢?這一切都與 Go 的值傳遞的事實有關。在這個程式碼範例中,我們實際上在記憶體中建立了每個 Dog 的2個額外副本。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/iterating-over-slices-in-go/iterating-over-slices-in-go.png)每個 Dog 的初始存在是使用複合欄位建立的:```gojackie := Dog{ Name: "Jackie", Age: 19,}```將值放入切片時,將建立值的第一個副本:```godogs := []Dog{jackie, sammy}```當我們遍曆切片時,會建立值的第二個副本:```godog := range dogs```現在我們可以看到為什麼迴圈中變數狗的地址總是相同的。我們正在顯示變數狗的地址,該變數恰好是 Dog 類型的局部變數,包含切片的每個索引的 Dog 的副本。對於切片的每次迭代,變數狗的地址是相同的。變數狗的值正在改變。我之前談到的那個令人討厭的錯誤與我認為變數狗的地址可以用作指向切片內每個 Dog 值的指標。像這樣的東西:```goallDogs := []*Dog{}for _, dog := range dogs { allDogs = append(allDogs, &dog)}for _, dog := range allDogs { fmt.Printf("Name: %s Age: %d\n", dog.Name, dog.Age)}```我建立了一個新的切片,用於儲存指向 Dog 值的指標。然後我遍曆 dogs ,儲存每個 Dog 值的地址的放入新切片 allDogs 中。至少我認為我儲存了每個 Dog 值的地址。如果我將此代碼添加到程式並運行它,這是輸出:```Name: Sammy Age: 10Name: Sammy Age: 10```我最終得到一個切片,其中每個元素具有相同的地址。該地址指向我們迭代的最後一個值的副本。哎呀!如果製作所有這些副本不是您想要的,您可以使用指標。以下是使用指標的樣本程式:```gopackage mainimport ( "fmt")type Dog struct { Name string Age int}func main() { jackie := &Dog{ Name: "Jackie", Age: 19, } fmt.Printf("Jackie Addr: %p\n", jackie) sammy := &Dog{ Name: "Sammy", Age: 10, } fmt.Printf("Sammy Addr: %p\n\n", sammy) dogs := []*Dog{jackie, sammy} for _, dog := range dogs { fmt.Printf("Name: %s Age: %d\n", dog.Name, dog.Age) fmt.Printf("Addr: %p\n\n", dog) }}```這是輸出:```Jackie Addr: 0x2101bb000Sammy Addr: 0x2101bb040Name: Jackie Age: 19Addr: 0x2101bb000Name: Sammy Age: 10Addr: 0x2101bb040```這次我們建立一個指向 Dog 值的指標。當我們遍曆此切片時,變數狗的值是我們儲存在切片中的每個 Dog 值的地址。我們使用與複合文字建立的相同的初始 Dog 值,而不是為每個 Dog 值建立兩個額外的副本。當切片是 Dog 值的集合或 Dog 值的指標集合時,範圍迴圈是相同的。```gofor _, dog := range dogs { fmt.Printf("Name: %s Age: %d\n", dog.Name, dog.Age)}```無論我們是否使用指標, Go 都會處理對 Dog 值的訪問。這很棒,但有時會導致一些混亂。至少這對我來說開始的時候是這樣的。我不能告訴你何時應該使用指標或何時應該使用副本。但請記住, Go 將按價值傳遞一切。這包括函數參數,傳回值以及在切片、 map 或 channel上迭代時。是的,你也可以遍曆一個 channel 。看看我在 Ewen Cheslack-Postava 撰寫的部落格文章中改編的範例程式碼:[http://ewencp.org/blog/golang-iterators/](http://ewencp.org/blog/golang-iterators/)```gopackage mainimport ( "fmt")type Dog struct { Name string Age int}type DogCollection struct { Data []*Dog}func (this *DogCollection) Init() { cloey := &Dog{"Cloey", 1} ralph := &Dog{"Ralph", 5} jackie := &Dog{"Jackie", 10} bella := &Dog{"Bella", 2} jamie := &Dog{"Jamie", 6} this.Data = []*Dog{cloey, ralph, jackie, bella, jamie}}func (this *DogCollection) CollectionChannel() chan *Dog { dataChannel := make(chan *Dog, len(this.Data)) for _, dog := range this.Data { dataChannel <- dog } close(dataChannel) return dataChannel}func main() { dc := DogCollection{} dc.Init() for dog := range dc.CollectionChannel() { fmt.Printf("Channel Name: %s\n", dog.Name) }}```如果您運行該程式,您將獲得以下輸出:```Channel Name: CloeyChannel Name: RalphChannel Name: JackieChannel Name: BellaChannel Name: Jamie```我真的很喜歡這個範例程式碼,因為它展示了關閉的 channel 的美感。使該程式有效關鍵是關閉的 channel 始終處於可發出訊號的狀態。這意味著通道上的任何讀取都將立即返回。如果通道為空白,則返回預設值。這使得 range 可以迭代傳遞到通道中的所有資料,並在通道為空白時完成。一旦通道為空白,通道上的下一次讀取將返回 nil 。這會導致迴圈終止。切片非常棒,重量輕,功能強大。您應該使用它們並獲得它們提供的好處。請記住,當您在切片上進行迭代時,您將獲得切片的每個元素的副本。如果這恰好是一個對象,那麼您將獲得該對象的副本。不要在迴圈中使用局部變數的地址。這是一個局部變數,它包含切片元素的副本,並且只有內容。不要和我一樣犯同樣的錯誤。
via: https://www.ardanlabs.com/blog/2013/09/iterating-over-slices-in-go.html
作者:William Kennedy 譯者:LFasMike 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
127 次點擊