[原]可能被忽略的Golang细节——range

本文深入探讨了Go语言中range关键字的功能与使用细节,包括如何迭代array、slice、map、string和channel等数据结构,以及在迭代过程中的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

range关键字是Go语言中一个非常有用的迭代array,slice,map, string, channel中元素的内置关键字。

range的使用

range的使用非常简单,对于遍历array,*array,string它返回两个值分别是数据的索引和值,遍历map时返回的两个值分别是key和value,遍历channel时,则只有一个返回数据。各种类型的返回值参考下表:

range expression1st Value2nd Value(optional)notes
array[n]E,*[n]Eindexintvalue E[i]
slice []Eindexintvalue E[i]
string abcdindexintrune int对于string,range迭代的是Unicode而不是字节,所以返回的值是rune
mapmap[k]vkeykvalue v
channelelementnone

使用方式非常简单,下面直接贴一段gobyexample中的示例代码作为参考,执行结果请点击链接跳转到gobyexample

package main
import "fmt"
func main() {

    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)

    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }

    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }

    for k := range kvs {
        fmt.Println("key:", k)
    }

    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

range的详细说明也请跳转官方文档的for_statement章节自行阅读。本文主要介绍一些平时可(yi)能(bu)被(xiao)忽(xin)略(jiu)的(diao)细(keng)节(li)。

range表达式构建

先来看看下面这段代码的输出是什么?这段代码会无限循环的执行下去吗?

func modifySlice() {
    v := []int{1, 2, 3, 4}
    for i := range v {
        v = append(v, i)
        fmt.Printf("Modify Slice: value:%v\n", v)
    }
}

答案肯定不会无限循环的,这么低级的错误,Go的开发者肯定是不会范的。那结果会是什么呢?执行这段代码会打印下面的内容:

Modify Slice: value:[1 2 3 4 0]
Modify Slice: value:[1 2 3 4 0 1]
Modify Slice: value:[1 2 3 4 0 1 2]
Modify Slice: value:[1 2 3 4 0 1 2 3]

可以看到每次循环在map中插入新的内容后,map的长度确实发生了变化,但是循环只执行了三次,正好是执行range前map的长度。说明range在执行之初就构建好了range表达式的内容了,虽然后面map的内容增加了,但是并不会影响初始构建的结果。官方文档对于range表达式的构建描述是这样的:

The range expression x is evaluated once before beginning the loop, with one exception: if at most one iteration variable is present and len(x) is constant, the range expression is not evaluated.

就是说range表达式在开始执行循环之前就已经构建了,但是有一个例外就是:如果最多只有一个迭代变量,且len(x)表达的是值是一个常量的时候range表达式不会构建。

那什么时候len(x)是一个常量呢?按照通常的理解,len(string), len(slice), len(array)…的返回应该都是常量啊?事实上不是这样的,这其实是由数据结构的特性决定的。因为相比较于其他语言,对于这一类容器数据结构,在Go语言中不仅有长度属性可以通过内建函数len()获取,还有一个可以通过内建函数cap()获取的容量属性,尤其是slice,map这一类可变长的数据结构。所以对于常量的定义,Go官方文档Length and capacity有详细的说明。

如果到这里,你以为你已经理解了range的构建,并且一眼就能看出一个for-range循环的迭代方式和执行情况。前面可能就已经有一个大坑为你准备好了。这时候官方文档里面下面这样一段话可能就被你忽略了,让我先贴出来:

The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.

用中国话解释下,首先对于map的迭代来说,map中内容的迭代顺序没有指定,也不保证,简单的说就是迭代map的时候将以随机的顺序返回里面的内容。这个好理解,也就是说如果你想按顺序迭代一些东西,map就不要指望了,换其他数据结构吧。

然后就进入高潮部分了,如果一个map的key在还没有被迭代到的时候就被delete掉了,那么它所对应的迭代值就不会被产生了。然后对于在迭代过程中新增加的key,则它可能会被迭代到也可能不会。如何选择会根据key增加的时机以及从上一次到下一次的迭代而不同。你可能会想,What?你TM在逗我么,说好的提前构建的呢…但是很不幸,事实就是这样,将前面的示例代码改成使用map我执行了几次结果都不一样。所以这种坑还是绕过为好。至于为什么会这样,容我有空再研究研究,下次重开一篇文章介绍。

func modifyMap() {
    data := map[string]string{"a": "A", "b": "B", "c": "C"}
    for k, v := range data {
        data[v] = k
        fmt.Println("modify Mapping", data)
    }
}

结果1,迭代了6次

modify Mapping map[a:A b:B c:C A:a]
modify Mapping map[b:B c:C A:a B:b a:A]
modify Mapping map[c:C A:a B:b C:c a:A b:B]
modify Mapping map[a:A b:B c:C A:a B:b C:c]
modify Mapping map[A:a B:b C:c a:A b:B c:C]
modify Mapping map[a:A b:B c:C A:a B:b C:c]

结果2,迭代了4次:

modify Mapping map[a:A b:B c:C A:a]
modify Mapping map[b:B c:C A:a B:b a:A]
modify Mapping map[a:A b:B c:C A:a B:b C:c]
modify Mapping map[B:b C:c a:A b:B c:C A:a]

range string

使用range迭代字符串时,需要主要的是range迭代的是Unicode而不是字节。返回的两个值,第一个是被迭代的字符的UTF-8编码的第一个字节在字符串中的索引,第二个值的为对应的字符且类型为rune(实际就是表示unicode值的整形数据)。

总结下来就是使用range迭代string时,需要注意下面两点:

  • 迭代的是Unicode不是字节,第一个返回值是UTF-8编码第一个字节的索引,所以索引值可能并不是连续的。
  • 第二个返回值的类型为rune,不是string类型的,如果要使用该值需要格式化。

看下面代码:

//string
func rangeString() {
    datas := "aAbB"

    for k, d := range datas {
        fmt.Printf("k_addr:%p, k_value:%v\nd_addr:%p, d_value:%v\n----\n", &k, k, &d, d)
    }
}

这段代码的输出如下,这里使用的string是只占用一个字节的字符的字符串,所以返回的第一个索引是连续的,可以看到第二个值都是整型数字。

k_addr:0xc420014148, k_value:0
d_addr:0xc420014150, d_value:97
k_addr:0xc420014148, k_value:1
d_addr:0xc420014150, d_value:65
k_addr:0xc420014148, k_value:2
d_addr:0xc420014150, d_value:98
k_addr:0xc420014148, k_value:3
d_addr:0xc420014150, d_value:66

range可以对string做更多的事情

前面说到range是对Unicode进行迭代来迭代字符串的,所以range还能够在迭代过程中发现字符串中非Unicode的字符,并使用U+FFFD字符替换改无效字符。

看下面代码的执行:

func rangeStringMore() {
    for pos, char := range "中\x80文" { // \x80 is an illegal UTF-8 encoding
        fmt.Printf("character %#U starts at byte position %d\n", char, pos)
    }
}

上面这段代码使用range迭代字符串"中\x80文",字符串中\x80是一个无效的的Unicode字符,所以range在迭代时会使用U+FFFD将其替换。另外UTF-8使用变长方式编码,第一个汉字占用了3个字节,所以遍历第二个字符的时候,其索引已经是3了,但是其只占一个字节,所以第三个字符的索引4.

character U+4E2D '中' starts at byte position 0
character U+FFFD '�' starts at byte position 3
character U+6587 '文' starts at byte position 4

range表达式是指针

前面说过range可以迭代的数据类型包括array,slice,map,string和channel。在这些数据类型里面只有array类型能以指针的形式出现在range表达式中。

具体参考下面的代码:

func rangePointer() {
    //compile error: cannot range over datas (type *string)
    //d := "aAbBcCdD"
    d := [5]int{1, 2, 3, 4, 5} //range successfully
    //d := []int{1, 2, 3, 4, 5} //compile error: cannot range over datas (type *[]int)
    //d := make(map[string]int) //compile error: cannot range over datas (type *map[string]int)
    datas := &d

    for k, d := range datas {
        fmt.Printf("k_addr:%p, k_value:%v\nd_addr:%p, d_value:%v\n----\n", &k, k, &d, d)
    }
}

参考链接:

range
For statements
Length and capacity
Rune literals
Go by Example: Range

<think>好的,用户想了解Golangrange关键字的用法,包括示例和解释。首先,我需要回忆一下Golangrange的各种常见用法,确保覆盖不同的数据结构,比如数组、切片、map、字符串和通道。然后,根据用户提供的引用资料,特别是引用2和3中的示例,需要详细说明这些用法,并指出需要注意的地方,比如值复制的问题。 首先,我应该分点说明range在不同数据结构中的应用。比如在数组和切片中,range返回索引和值;在map中返回键和值;在字符串中返回字符的索引和Unicode值;在通道中则用于迭代接收值直到关闭。同时,必须提到使用下划线_来忽略不需要的变量,比如忽略索引或键。 接下来,要注意用户提供的引用3中的示例,其中提到遍历数组时,v是复制后的值,修改它不会影响数组。这一点需要特别强调,避免用户在使用时出现错误。此外,引用2中的三段式for循环和range结合的遍历方式也需要对比说明,突出range的简洁性。 然后,需要生成相关问题,这些问题应该围绕用户可能进一步感兴趣的点,比如range的底层机制、性能优化、与指针的结合使用、在并发中的安全性,以及与其他语言遍历语法的对比。这些问题能够帮助用户更深入地理解range的应用场景和注意事项。 最后,确保整个回答结构清晰,每个部分都有示例代码和解释,并且正确使用LaTeX格式,如变量名用$...$包裹。同时,引用用户提供的参考资料,在适当的位置添加引用标识,比如在说明值复制时引用[^3],在遍历map时引用[^2]。</think>### Golangrange关键字的用法解析 在Golang中,`range`是用于迭代数据结构(数组、切片、字符串、map、通道)的关键字,其行为根据数据结构类型有所不同。以下是具体用法和示例: --- #### 1. **数组/切片遍历** 语法:返回索引和值的副本 ```go arr := []int{10, 20, 30} for index, value := range arr { fmt.Printf("索引:%d 值:%d\n", index, value) } ``` - 示例说明:遍历时`value`是元素的副本,修改`value`不会影响数组[^3] - 忽略索引:用`_`占位符 ```go for _, v := range arr { /* 仅用值 */ } ``` --- #### 2. **map遍历** 语法:返回键值对(无序) ```go m := map[string]int{"a": 1, "b": 2} for key, val := range m { fmt.Printf("键:%s 值:%d\n", key, val) } ``` - 注意:遍历顺序不固定,每次运行可能不同 --- #### 3. **字符串遍历** 语法:返回字符索引和Unicode值 ```go str := "Go语言" for i, r := range str { fmt.Printf("位置:%d Unicode码点:%U 字符:%c\n", i, r, r) } ``` - 注意:中文字符会占用多个字节,索引可能不连续 --- #### 4. **通道遍历** 语法:持续接收值直到通道关闭 ```go ch := make(chan int) go func() { ch <- 1 ch <- 2 close(ch) }() for v := range ch { fmt.Println("接收:", v) } ``` --- ### 关键注意事项 1. **值复制机制** 遍历时获取的是元素的副本,修改副本不影响数据: ```go a := [3]int{1, 2, 3} for _, v := range a { v += 100 // 数组保持[1,2,3] } ``` 若需修改数据,需通过索引访问: ```go for i := range a { a[i] += 100 // 数组变为[101,102,103] } ``` 2. **性能优化** 对大型数组建议使用索引直接访问,避免值复制的开销。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值