網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
for?range語(yǔ)句是業(yè)務(wù)開發(fā)中編寫頻率很高的代碼,其中會(huì)有一些常見的坑,看完這篇文章會(huì)讓你少入坑。
for?range基本用法
range是Golang提供的一種迭代遍歷手段,可操作的類型有數(shù)組、切片、string、map、channel等
1、遍歷數(shù)組
myArray := [3]int{1, 2, 3} for i, ele := range myArray { fmt.Printf("index:%d,element:%d\n", i, ele) fmt.Printf("index:%d,element:%d\n", i, myArray[i]) }
直接取元素或通過(guò)下標(biāo)取
2、遍歷slice
mySlice := []string{"I", "am", "peachesTao"} for i, ele := range mySlice { fmt.Printf("index:%d,element:%s\n", i, ele) fmt.Printf("index:%d,element:%s\n", i, mySlice[i]) }
直接取元素或通過(guò)下標(biāo)取
3、遍歷string
s:="peachesTao" for i,item := range s { fmt.Println(string(item)) fmt.Printf("index:%d,element:%s\n", i, string(s[i])) }
直接取元素或通過(guò)下標(biāo)取
注意:循環(huán)體中string中的元素實(shí)際上是byte類型,需要轉(zhuǎn)換為字面字符
4、遍歷map
myMap := map[int]string{1:"語(yǔ)文",2:"數(shù)學(xué)",3:"英語(yǔ)"} for key,value := range myMap { fmt.Printf("key:%d,value:%s\n", key, value) fmt.Printf("key:%d,value:%s\n", key, myMap[key]) }
直接取元素或通過(guò)下標(biāo)取
5、遍歷channel
myChannel := make(chan int) go func() { for i:=0;i<10;i++{ time.Sleep(time.Second) myChannel <- i } }() go func() { for c := range myChannel { fmt.Printf("value:%d\n", c) } }()
channel遍歷是循環(huán)從channel中讀取數(shù)據(jù),如果channel中沒(méi)有數(shù)據(jù),則會(huì)阻塞等待,如果channel已被關(guān)閉,則會(huì)退出循環(huán)。
for?range?和?for的區(qū)別
for?range可以直接訪問(wèn)目標(biāo)對(duì)象中的元素,而for必須通過(guò)下標(biāo)訪問(wèn)
for?frange可以訪問(wèn)map、channel對(duì)象,而for不可以
for?range容易踩的坑
下面的例子是將mySlice中每個(gè)元素的后面都加上字符"-new"
mySlice := []string{"I", "am", "peachesTao"} for _, ele := range mySlice { ele=ele+"-new" } fmt.Println(mySlice)
結(jié)果:
[I am peachesTao]
打印mySlice發(fā)現(xiàn)元素并沒(méi)有更新,為什么會(huì)這樣?
原因是for?range語(yǔ)句會(huì)將目標(biāo)對(duì)象中的元素copy一份值的副本,修改副本顯然不能對(duì)原元素產(chǎn)生影響
為了證明上述結(jié)論,在遍歷前和遍歷中打印出元素的內(nèi)存地址
mySlice := []string{"I", "am", "peachesTao"} fmt.Printf("遍歷前首元素內(nèi)存地址:%p\n",&mySlice[0]) for _, ele := range mySlice { ele=ele+"-new" fmt.Printf("遍歷中元素內(nèi)存地址:%p\n",&ele) } fmt.Println(mySlice)
結(jié)果:
遍歷前第一個(gè)元素內(nèi)存地址:0xc000054180
遍歷前第二個(gè)元素內(nèi)存地址:0xc000054190
遍歷前第三個(gè)元素內(nèi)存地址:0xc0000541a0
遍歷中元素內(nèi)存地址:0xc000010200
遍歷中元素內(nèi)存地址:0xc000010200
遍歷中元素內(nèi)存地址:0xc000010200
[I am peachesTao]
可以得出兩個(gè)結(jié)論:
- 遍歷體中的元素內(nèi)存地址已經(jīng)發(fā)生了變化,生成了元素副本,至于產(chǎn)生副本的原因在“for?range底層原理”段落中會(huì)有介紹
- 遍歷體中的只生成了一個(gè)全局的元素副本變量,不是每個(gè)元素都會(huì)生成一個(gè)副本,這個(gè)特點(diǎn)也值得大家注意,否則會(huì)踩坑。
比如遍歷mySlice元素生成一個(gè)[]*string類型的mySliceNew,要通過(guò)一個(gè)中間變量取中間變量的地址(或者通過(guò)下標(biāo)的形式訪問(wèn)元素也可以)加入mySliceNew,如果直接取元素副本的地址會(huì)導(dǎo)致mySliceNew中所有元素都是一樣的,如下:
mySlice := []string{"I", "am", "peachesTao"} var mySliceNew []*string for _, item := range mySlice { itemTemp := item mySliceNew = append(mySliceNew, &itemTemp) //mySliceNew = append(mySliceNew, &item) 錯(cuò)誤的做法 }
回到剛才那個(gè)問(wèn)題,如何能在遍歷中修改元素呢?答案是直接通過(guò)下標(biāo)訪問(wèn)slice中的元素對(duì)其賦值,如下:
mySlice := []string{"I", "am", "peachesTao"} for i, _ := range mySlice { mySlice[i] = mySlice[i]+"-new" } fmt.Println(mySlice)
結(jié)果:
[I-new?am-new?peachesTao-new]
可以看到元素已經(jīng)被修改
for?range和for性能比較
我們定義一個(gè)結(jié)構(gòu)體Item,包含int類型的id字段,對(duì)結(jié)構(gòu)體數(shù)組分別使用for、for?range?item、for?range?index的方式進(jìn)行遍歷,下面是測(cè)試代碼(直接引用“Go語(yǔ)言高性能編程”這篇文章中的例子,下面的reference中有鏈接地址)
type Item struct { id int } func BenchmarkForStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { length := len(items) var tmp int for k := 0; k < length; k++ { tmp = items[k].id } _ = tmp } } func BenchmarkRangeIndexStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for k := range items { tmp = items[k].id } _ = tmp } } func BenchmarkRangeStruct(b *testing.B) { var items [1024]Item for i := 0; i < b.N; i++ { var tmp int for _, item := range items { tmp = item.id } _ = tmp } }
運(yùn)行基準(zhǔn)測(cè)試命令:
go test -bench . test/for_range_performance_test.go
測(cè)試結(jié)果:
goos: darwin
goarch: amd64
BenchmarkForStruct-4 ? ? ? ? ? ? 3176875 ? ? ? ? ? ? ? 375 ns/op
BenchmarkRangeIndexStruct-4 ? ? ?3254553 ? ? ? ? ? ? ? 369 ns/op
BenchmarkRangeStruct-4 ? ? ? ? ? 3131196 ? ? ? ? ? ? ? 384 ns/op
PASS
ok ? ? ?command-line-arguments ?4.775s
可以看出:
for?range?通過(guò)Index和直接訪問(wèn)元素的方式和for的方式遍歷性能幾乎無(wú)差異
下面我們?cè)贗tem結(jié)構(gòu)體添加一個(gè)byte類型長(zhǎng)度為4096的數(shù)組字段val
type Item struct { id int val [4096]byte }
再運(yùn)行一遍基準(zhǔn)測(cè)試,結(jié)果如下:
goos: darwin
goarch: amd64
BenchmarkForStruct-4 ? ? ? ? ? ? 2901506 ? ? ? ? ? ? ? 393 ns/op
BenchmarkRangeIndexStruct-4 ? ? ?3160203 ? ? ? ? ? ? ? 381 ns/op
BenchmarkRangeStruct-4 ? ? ? ? ? ? ?1088 ? ? ? ? ? ?948678 ns/op
PASS
ok ? ? ?command-line-arguments ?4.317s
可以看出:
- for?range通過(guò)下標(biāo)遍歷元素的性能跟for相差不大
- for?range直接遍歷元素的性能比f(wàn)or慢近1000倍
結(jié)論:
- for?range通過(guò)下標(biāo)遍歷元素的性能跟for相差不大
- for?range直接遍歷元素的性能在元素為小對(duì)象的情況下跟for相差不大,在元素為大對(duì)象的情況下比f(wàn)or慢很多
for?range的底層原理
對(duì)于for-range語(yǔ)句的實(shí)現(xiàn),可以從編譯器源碼中找到答案。
編譯器源碼gofrontend/go/statements.cc/For_range_statement::do_lower()【鏈接見下方
reference】
方法中有如下注釋。
// Arrange to do a loop appropriate for the type. We will produce // for INIT ; COND ; POST { // ITER_INIT // INDEX = INDEX_TEMP // VALUE = VALUE_TEMP // If there is a value // original statements // }
可見range實(shí)際上是一個(gè)C風(fēng)格的循環(huán)結(jié)構(gòu)。range支持string、數(shù)組、數(shù)組指針、切片、map和channel類型,對(duì)于不同類型有些細(xì)節(jié)上的差異。
1、range?for?slice
下面的注釋解釋了遍歷slice的過(guò)程:
For_range_statement::lower_range_slice
// The loop we generate: // for_temp := range // len_temp := len(for_temp) // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = for_temp[index_temp] // index = index_temp // value = value_temp // original body // }
遍歷slice前會(huì)先獲得slice的長(zhǎng)度len_temp作為循環(huán)次數(shù),循環(huán)體中,每次循環(huán)會(huì)先獲取元素值,如果for-range中接收index和value的話,則會(huì)對(duì)index和value進(jìn)行一次賦值,這就解釋了對(duì)大元素進(jìn)行遍歷會(huì)影響性能,因?yàn)榇髮?duì)象賦值會(huì)產(chǎn)生gc
由于循環(huán)開始前循環(huán)次數(shù)就已經(jīng)確定了,所以循環(huán)過(guò)程中新添加的元素是沒(méi)辦法遍歷到的。
另外,數(shù)組與數(shù)組指針的遍歷過(guò)程與slice基本一致,不再贅述。
2、range?for?map?
下面的注釋解釋了遍歷map的過(guò)程:
For_range_statement::lower_range_map
// The loop we generate: // var hiter map_iteration_struct // for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { // index_temp = *hiter.key // value_temp = *hiter.val // index = index_temp // value = value_temp // original body // }
遍歷map時(shí)沒(méi)有指定循環(huán)次數(shù),循環(huán)體與遍歷slice類似。由于map底層實(shí)現(xiàn)與slice不同,map底層使用hash表實(shí)現(xiàn),插入數(shù)據(jù)位置是隨機(jī)的,所以遍歷過(guò)程中新插入的數(shù)據(jù)不能保證遍歷到。
3、range?for?channel
遍歷channel是最特殊的,這是由channel的實(shí)現(xiàn)機(jī)制決定的:
For_range_statement::lower_range_channel
// The loop we generate: // for { // index_temp, ok_temp = <-range // if !ok_temp { // break // } // index = index_temp // original body // }
一直循環(huán)讀數(shù)據(jù),如果有數(shù)據(jù)則取出,如果沒(méi)有則阻塞,如果channel被關(guān)閉則退出循環(huán)
注:
上述注釋中index_temp實(shí)際上描述是有誤的,應(yīng)該為value_temp,因?yàn)閕ndex對(duì)于channel是沒(méi)有意義的。
總結(jié)
使用index,value接收range返回值會(huì)產(chǎn)生一次數(shù)據(jù)拷貝,視情況考慮不接收,以提高性能
for-range的實(shí)現(xiàn)實(shí)際上是C風(fēng)格的for循環(huán)
到此這篇關(guān)于go語(yǔ)言中for range使用方法及避坑指南的文章就介紹到這了,更多相關(guān)go語(yǔ)言for range使用內(nèi)容請(qǐng)搜索AB教程網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持AB教程網(wǎng)!
參考資料
【《Go專家編程》Go range實(shí)現(xiàn)原理及性能優(yōu)化剖析 https://my.oschina.net/renhc/blog/2396058
【面試官:用過(guò)go中的for-range嗎?這幾個(gè)問(wèn)題你能解釋一下原因嗎?】https://zhuanlan.zhihu.com/p/217987219
【Go語(yǔ)言高性能編程】https://geektutu.com/post/hpg-range.html
原文鏈接:https://blog.csdn.net/taoerchun/article/details/120642937
相關(guān)推薦
- 2022-11-20 golang?實(shí)現(xiàn)?pdf?轉(zhuǎn)高清晰度?jpeg的處理方法_Golang
- 2022-11-26 使用HttpClient消費(fèi)ASP.NET?Web?API服務(wù)案例_實(shí)用技巧
- 2023-01-21 flask中響應(yīng)錯(cuò)誤的處理及errorhandler的應(yīng)用方式_python
- 2022-06-16 Python數(shù)據(jù)結(jié)構(gòu)之遞歸可視化詳解_python
- 2022-04-24 如何在Python中隱藏和加密密碼示例詳解_python
- 2022-08-21 Android設(shè)置重復(fù)文字水印背景的方法_Android
- 2022-08-20 Pygame?精準(zhǔn)檢測(cè)圖像碰撞的問(wèn)題_python
- 2022-11-08 python安裝包出現(xiàn)Retrying?(Retry(total=4,?connect=None,?
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支