Go 處理大數組:使用 for range 還是 for 迴圈?
<span style="font-size: 15px;">while</span>
、##do...while<span style="font-size: 15px;"></span>
# 等迴圈控制語法,而僅保留了一種語句,即for 迴圈。 for i := 0; i < n; i++ { ... ... }
但是,經典的三段式循環語句,需要取得迭代物件的長度 n。有鑑於此,為了更方便Go 開發者對複合資料型別進行迭代,例如array、slice、channel、map,Go 提供了for 迴圈的變體,即for range<span style="font-size: 15px;"></span>
循環。
複製問題
range 在帶來便利的同時,也給 Go 初學者帶來了一些麻煩。因為使用者需要明白一點:for range 中,參與循環表達式的只是物件的副本。
func main() { var a = [5]int{1, 2, 3, 4, 5} var r [5]int fmt.Println("original a =", a) for i, v := range a { if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } fmt.Println("after for range loop, r =", r) fmt.Println("after for range loop, a =", a) }
你認為這段程式碼會輸出以下結果嗎?
original a = [1 2 3 4 5] after for range loop, r = [1 12 13 4 5] after for range loop, a = [1 12 13 4 5]
但是,实际输出是
original a = [1 2 3 4 5] after for range loop, r = [1 2 3 4 5] after for range loop, a = [1 12 13 4 5]
为什么会这样?原因是参与 for range 循环是 range 表达式的副本。也就是说,在上面的例子中,实际上参与循环的是 a 的副本,而不是真正的 a。
为了让大家更容易理解,我们把上面例子中的 for range 循环改写成等效的伪代码形式。
for i, v := range ac { //ac is a value copy of a if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v }
ac 是 Go 临时分配的连续字节序列,与 a 根本不是同一块内存空间。因此,无论 a 如何修改,它参与循环的副本 ac 仍然保持原始值,因此从 ac 中取出的 v 也依然是 a 的原始值,而不是修改后的值。
那么,问题来了,既然 for range 使用的是副本数据,那 for range 会比经典的 for 循环消耗更多的资源并且性能更差吗?
性能对比
基于副本复制问题,我们先使用基准示例来验证一下:对于大型数组,for range 是否一定比经典的 for 循环运行得慢?
package main import "testing" func BenchmarkClassicForLoopIntArray(b *testing.B) { b.ReportAllocs() var arr [100000]int for i := 0; i < b.N; i++ { for j := 0; j < len(arr); j++ { arr[j] = j } } } func BenchmarkForRangeIntArray(b *testing.B) { b.ReportAllocs() var arr [100000]int for i := 0; i < b.N; i++ { for j, v := range arr { arr[j] = j _ = v } } }
在这个例子中,我们使用 for 循环和 for range 分别遍历一个包含 10 万个 int 类型元素的数组。让我们看看基准测试的结果
$ go test -bench . forRange1_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz BenchmarkClassicForLoopIntArray-8 47404 25486 ns/op 0 B/op 0 allocs/op BenchmarkForRangeIntArray-8 37142 31691 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 2.978s
从输出结果可以看出,for range 的确会稍劣于 for 循环,当然这其中包含了编译器级别优化的结果(通常是静态单赋值,或者 SSA 链接)。
让我们关闭优化开关,再次运行压力测试。
$ go test -c -gcflags '-N -l' . -o forRange1.test $ ./forRange1.test -test.bench . goos: darwin goarch: amd64 pkg: workspace/example/forRange cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz BenchmarkClassicForLoopIntArray-8 6734 175319 ns/op 0 B/op 0 allocs/op BenchmarkForRangeIntArray-8 5178 242977 ns/op 0 B/op 0 allocs/op PASS
当没有编译器优化时,两种循环的性能都明显下降, for range 下降得更为明显,性能也更加比经典 for 循环差。
遍历结构体数组
上述性能测试中,我们的遍历对象类型是 int 值的数组,如果我们将 int 元素改为结构体会怎么样?for 和 for range 循环各自表现又会如何?
package main import "testing" type U5 struct { a, b, c, d, e int } type U4 struct { a, b, c, d int } type U3 struct { b, c, d int } type U2 struct { c, d int } type U1 struct { d int } func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) { b.ReportAllocs() var arr [100000]U5 for i := 0; i < b.N; i++ { for j := 0; j < len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) { b.ReportAllocs() var arr [100000]U4 for i := 0; i < b.N; i++ { for j := 0; j < len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) { b.ReportAllocs() var arr [100000]U3 for i := 0; i < b.N; i++ { for j := 0; j < len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) { b.ReportAllocs() var arr [100000]U2 for i := 0; i < b.N; i++ { for j := 0; j < len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) { b.ReportAllocs() var arr [100000]U1 for i := 0; i < b.N; i++ { for j := 0; j < len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkForRangeLargeStructArrayU5(b *testing.B) { b.ReportAllocs() var arr [100000]U5 for i := 0; i < b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU4(b *testing.B) { b.ReportAllocs() var arr [100000]U4 for i := 0; i < b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU3(b *testing.B) { b.ReportAllocs() var arr [100000]U3 for i := 0; i < b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU2(b *testing.B) { b.ReportAllocs() var arr [100000]U2 for i := 0; i < b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU1(b *testing.B) { b.ReportAllocs() var arr [100000]U1 for i := 0; i < b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } }
在这个例子中,我们定义了 5 种类型的结构体:U1~U5,它们的区别在于包含的 int 类型字段的数量。
性能测试结果如下
$ go test -bench . forRange2_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz BenchmarkClassicForLoopLargeStructArrayU5-8 44540 26227 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU4-8 45906 26312 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU3-8 43315 27400 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU2-8 44605 26313 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU1-8 45752 26110 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU5-8 3072 388651 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU4-8 4605 261329 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU3-8 5857 182565 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU2-8 10000 108391 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU1-8 36333 32346 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 16.160s
我们看到一个现象:不管是什么类型的结构体元素数组,经典的 for 循环遍历的性能比较一致,但是 for range 的遍历性能会随着结构字段数量的增加而降低。
带着疑惑,发现了一个与这个问题相关的 issue:cmd/compile: optimize large structs:https://github.com/golang/go/issues/24416。这个 issue 大致是说:如果一个结构体类型有超过一定数量的字段(或一些其他条件),就会将该类型视为 unSSAable。如果 SSA 不可行,那么就无法通过 SSA 优化,这也是造成上述基准测试结果的重要原因。
结论
对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显。
另外,由于在 Go 中切片的底层都是通过数组来存储数据,尽管有 for range 的副本复制问题,但是切片副本指向的底层数组与原切片是一致的。这意味着,当我们将数组通过切片代替后,不管是通过 for range 或者 for 循环均能得到一致的稳定的遍历性能。
以上是Go 處理大數組:使用 for range 還是 for 迴圈?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

在Go中,可以使用gorilla/websocket包發送WebSocket訊息。具體步驟:建立WebSocket連線。傳送文字訊息:呼叫WriteMessage(websocket.TextMessage,[]byte("訊息"))。發送二進位訊息:呼叫WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

在Go中,可以使用正規表示式比對時間戳記:編譯正規表示式字串,例如用於匹配ISO8601時間戳記的表達式:^\d{4}-\d{2}-\d{2}T \d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$ 。使用regexp.MatchString函數檢查字串是否與正規表示式相符。

Go和Go語言是不同的實體,具有不同的特性。 Go(又稱Golang)以其並發性、編譯速度快、記憶體管理和跨平台優點而聞名。 Go語言的缺點包括生態系統不如其他語言豐富、文法更嚴格、缺乏動態類型。

記憶體洩漏會導致Go程式記憶體不斷增加,可通過:關閉不再使用的資源,如檔案、網路連線和資料庫連線。使用弱引用防止記憶體洩漏,當物件不再被強引用時將其作為垃圾回收目標。利用go協程,協程棧記憶體會在退出時自動釋放,避免記憶體洩漏。

在Go中傳遞map給函數時,預設會建立副本,對副本的修改不影響原map。如果需要修改原始map,可透過指標傳遞。空map需小心處理,因為技術上是nil指針,傳遞空map給期望非空map的函數會發生錯誤。

在Golang中,錯誤包裝器允許你在原始錯誤上追加上下文訊息,從而創建新錯誤。這可用於統一不同程式庫或元件拋出的錯誤類型,簡化偵錯和錯誤處理。步驟如下:使用errors.Wrap函數將原有錯誤包裝成新錯誤。新錯誤包含原始錯誤的上下文資訊。使用fmt.Printf輸出包裝後的錯誤,提供更多上下文和可操作性。在處理不同類型的錯誤時,使用errors.Wrap函數統一錯誤類型。

在Go語言中建立優先權Goroutine有兩步驟:註冊自訂Goroutine建立函數(步驟1)並指定優先權值(步驟2)。這樣,您可以建立不同優先順序的Goroutine,優化資源分配並提高執行效率。

如何在Golang單元測試中使用Gomega進行斷言在Golang單元測試中,Gomega是一個流行且功能強大的斷言庫,它提供了豐富的斷言方法,使開發人員可以輕鬆驗證測試結果。安裝Gomegagoget-ugithub.com/onsi/gomega使用Gomega進行斷言以下是使用Gomega進行斷言的一些常用範例:1.相等斷言import"github.com/onsi/gomega"funcTest_MyFunction(t*testing.T){
