是的, 默认情况下,向一个hash表插入的元素是没有固定顺序的。但是因为很多原因,比如有一些帖子就指出了不是所有的map都是hash表(而且有些语言还有有顺序的hash表,比如java的TreeMap), 我还是能够了解为什么很多人(尤其是对Go map实现机制比较了解的人)会假定遍历map元素的顺序和向map插入元素的顺序是相同的。
我原来的例子是我自己想出来的,并没有演示出大多数版本的Go关于这方面的特点(尽管我听说对于1.3版本可能是可以工作的)。所以我把代码更新了一下,你可以把代码复制到你的编辑器或者Go Playground来看看效果。
Go确实是从随机偏移位置来开始map的元素遍历的,并不是没有规律可循。
好了,现在回来看看这个文章。
过去几周,我看到的人们对Go语言的热情和语言的发展势头真是让我无比惊叹。这里面的一部分原因可能是和Gophercon 2014有关,在我写这篇文章的时候,刚刚举办完。我对那些能参加的人真是羡慕,嫉妒,恨啊!从会议计划和讨论话题来看,会议确实很棒。另外能够去和Rob Pike神搞个基,以及看看那些家伙用Go创造出来的好东西也是很不错的。另外我感觉最近关于Go的博客数量也爆发了。而且,很多人开始重点地将Go在他们的服务中使用。比如Digital Ocean,大规模云计算创业公司,刚刚宣布他们把一大坨Perl代码改为Go实现,并且极大地改进了诸如响应时间等问题。
我从来不会写那种屌丝级必备的“哇塞,我用Golang两周了,真的好棒啊!”这种文章。因为我觉得这些文章没有啥意思,零价值。但是,最近我遇到了一个Go特性,而且我认为这个特性非常酷,并且在我看来反映出了Go作为一门牛逼语言的基本姿态。
Go的map
元素遍历顺序(使用range
关键字)是随机的,而不是遵循元素的添加顺序。这是什么意思?很特别么?
Maps
Map的简单介绍。
从Andrew Gerrand的关于maps的文章直接偷过来用。
计算机科学里面最有用的数据结构之一就是hash表。不同的hash表实现提供了很多独特的特性。但是基本上都包括元素查询,添加和删除。Go提供了一个内置的类型
map
,这个类型实现了hash表的基本功能。
所以在Go语言里面如果你需要使用hash表,那么就用map
吧。因为Go是强类型语言,所以你必须为map的键和对应的值指定具体的类型。这些键或值的类型可以是字符串,整型,指向结构体的指针等。一个常见的用法就是键和值都是字符串类型。go m := make(map[string]string)
使用方法很简单。在元素被赋值之前,key可以不存在,甚至在被取值的时候也可以不存在(这样就返回零值,零值对于不同的类型是不同的,比如整型是0,而字符串是空字符串"")。
m["bandName"] = "Funny Bones" // "create"
websiteTitle := m["bandName"] + " Music" // "read"
m["bandName"] = "Moon Taxi" // "update"
delete(m, "bandName") // "delete"
fmt.Printf(m["bandName"]) // prints nothing since m["bandName"] == ""
可以使用range
关键字来遍历map的所有元素。
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
遍历顺序
第一眼看上去,Go程序员或许会以为下面的代码输出:
package main
import "fmt"
func main() {
blogArticleViews := map[string]int{
"unix": 0,
"python": 1,
"go": 2,
"javascript": 3,
"testing": 4,
"philosophy": 5,
"startups": 6,
"productivity": 7,
"hn": 8,
"reddit": 9,
"C++": 10,
}
for key, views := range blogArticleViews {
fmt.Println("There are", views, "views for", key)
}
}
会是这样的:
$ go run map_iteration_order.go
There are 0 views for unix
There are 1 views for python
There are 2 views for go
There are 3 views for javascript
There are 4 views for testing
There are 5 views for philosophy
There are 6 views for startups
There are 7 views for productivity
There are 8 views for hn
There are 9 views for reddit
There are 10 views for C++
但从Go 1版本开始,map的遍历顺序是随机的。也就是说下面的结果更有可能:
$ go run map_iteration_order.go
There are 3 views for javascript
There are 5 views for philosophy
There are 10 views for C++
There are 0 views for unix
There are 1 views for python
There are 2 views for go
There are 4 views for testing
There are 6 views for startups
There are 7 views for productivity
There are 8 views for hn
There are 9 views for reddit
Go语言的设计者们注意到人们过于依赖这种通常情况下key的存储顺序和key的添加顺序一致的特性。所以他们把key的遍历顺序随机化了。因此,如果你希望key的输出顺序和添加顺序一致的话,你需要自己去追踪哪个值存储在哪个位置,就像这样:
package main
import (
"fmt"
"sort"
)
func main() {
var m = map[string]int{
"unix": 0,
"python": 1,
"go": 2,
"javascript": 3,
"testing": 4,
"philosophy": 5,
"startups": 6,
"productivity": 7,
"hn": 8,
"reddit": 9,
"C++": 10,
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}
上面的代码是又一次厚颜无耻地从Andrew 的大作偷过来直接用的。
我觉得大家对这个特性的态度可以分为两类。
第一类人会有各种反应,从不明白为什么要这么做,到有点不爽,甚至是强烈反对。这些人大部分是喜欢弄些有潜在危险性的或者小技巧的代码,并且他们希望Go语言的设计者也能满足他们的愿望。
另一类人倒是能够完全接受,而且很感激Go设计者们能够为他们着想,不断完善和改进Go语言。
为什么这很特别?
一句话:态度。
这个无伤大雅的语言特性在我看来恰是作为通用语言哲学的一个闪光点。没有过于灵活地允许马马虎虎的代码,Go强迫你从一开始就把事情弄得直接一点。Go程序员参考里面说如果他们的程序可以编译(而且代码符合Go的风格),那么代码有很大的可能可以像预期的那样工作,这种模模糊糊却不错的感觉也有Go严谨性的贡献。没有诡异的类型bug,丢失分号等等错误。
尤其是,在Andrew的参考文章中,他指出这是Go的设计者们所作出的改变。他们不再允许人们依赖于那些破破烂烂的假设。我最痛恨的一点就是那些破烂的,到处是bug的功能(这发生在交付的产品中,或者是编程语言,等等很多地方),通过权衡,接受,从而变成了一个特性,另外尝试修复这些"特性"真的是很恶心。很明显的,PHP和JavaScript的语言文化就是因为各种原因往这个方向发展的(他们使用它,但是注定要付出代价,而且很多东西到最后都是没有解决的)。
例如,PHP的一个最大的缺点是,针与干草堆的问题(在干草堆里面找针)。我理想中的语言所应该具有的特点和这种不一致性格格不入。这也是为什么我发现Go的设计者拒绝糟糕的异常和泛型设计。他们就是想做正确的事情,当然他们知道这需要花费时间。他们不着急,而且向语言中添加特性比删除特性容易多了。
总结
Go是一种令人愉悦的语言,而且很多方面都是经过深思熟虑的。不要因为它缺少一些你常用的功能,比如泛型和动态类型,而急于去评断和批评它。如果你自己愿意试一试,你会发现你不一定需要这些功能。而且,你通过使用简单的并行功能会写出更简单,整洁,优雅的代码。
Go一直在坚定地成长和发展中,这也是Go所能带来的乐趣之一。它绝对是可靠的,而且可以用于生产环境中。同时Go的性能和稳定性也在不断地提高。看看下面由Rob Pike最近贴出来的对比Go 1和最新版(快1.3了)的对比。
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 7102124000 5790215308 -18.47%
BenchmarkFannkuch11 7139655000 4361664854 -38.91%
BenchmarkFmtFprintfEmpty 177 104 -41.24%
BenchmarkFmtFprintfString 575 312 -45.74%
BenchmarkFmtFprintfInt 424 230 -45.75%
BenchmarkFmtFprintfIntInt 682 403 -40.91%
BenchmarkFmtFprintfPrefixedInt 661 394 -40.39%
BenchmarkFmtFprintfFloat 907 598 -34.07%
BenchmarkFmtManyArgs 2787 1663 -40.33%
BenchmarkGobDecode 31284200 10693446 -65.82%
BenchmarkGobEncode 13900550 6919498 -50.22%
BenchmarkGzip 636714400 704154254 +10.59%
BenchmarkGunzip 275620600 139906588 -49.24%
BenchmarkHTTPClientServer 144041 71739 -50.20%
BenchmarkJSONEncode 83472200 32969241 -60.50%
BenchmarkJSONDecode 391968600 120858167 -69.17%
BenchmarkMandelbrot200 9540360 6062905 -36.45%
BenchmarkGoParse 10007700 6760226 -32.45%
BenchmarkRegexpMatchEasy0_32 198 168 -15.15%
BenchmarkRegexpMatchEasy0_1K 540 479 -11.30%
BenchmarkRegexpMatchEasy1_32 175 149 -14.86%
BenchmarkRegexpMatchEasy1_1K 1353 1414 +4.51%
BenchmarkRegexpMatchMedium_32 311 307 -1.29%
BenchmarkRegexpMatchMedium_1K 108924 126452 +16.09%
BenchmarkRegexpMatchHard_32 4972 5681 +14.26%
BenchmarkRegexpMatchHard_1K 157354 181042 +15.05%
BenchmarkRevcomp 1362067000 1162752845 -14.63%
BenchmarkTemplate 714330000 144396424 -79.79%
BenchmarkTimeParse 1651 669 -59.48%
BenchmarkTimeFormat 3215 714 -77.79%
我超爱Go!!!
有疑问加站长微信联系(非本文作者)

1) 相比与讲“HOW”的文章,本人更喜欢讲“WHY”的文章, 学习“go思维”比学习“go语法”更有意思得多多,收获也更大大的多多。
2)楼主此文,讲了半天,最后我们还是没有明白“为什么 hash的顺序要随机?”是技术上不成熟不划算?还是现有的算法都有缺陷?还是有bug做不到? 还是设计者就是牛逼他就是喜爱这么做,他就是故意不那么做,让我们大家适应他赞美他膜拜他? 楼主你总得讲讲道理卜来吧?
3)最后的那个benchmark 什么的没看懂什么意思。看楼主自己都写得高潮了都, 别价光顾着自己爽呀, 给我们大家绍介绍介,让大家跟着你一块爽爽,成不成呀?
1.引用的那篇篇恰恰就是讲WHY的文章 2.很明显,hash map 类似c++里的unordered_map,是无序的,开发者不希望有人觉得有序是内置map的特性 3.这个都看不懂...神也帮不了你...
不知道这有什么好,如果直接按照顺序的话,直接就可以按照添加顺序进行添加,如果随机的话(事实上没有随机的东西),必然还有一个负责随机的函数进行乱序,这就多了一步骤,如果添加一万条数据,就多了一万个步骤,可是究竟有什么用呢?
有同感,没讲清楚,正准备进入高潮就没了
hash map 一般是用key生成一个hash值 用hash值作为数组下标而 数组里的值 是key加value的一个结构。 所以如果key生成的hash值 是随机的那么 数组的下标就是随机的那么数组的顺序就是随机的。你循环这个hash数组就只能是无顺序的。
最后的例子也没有排序成功啊,我看Andrew 那边是要用int类型来排序的,而不是用string
sort.Strings(a []string)好像是按照首字符顺序排序的
大错特错。 并没有什么“不是所有的map都是hash表”。定义上map就是保存映射关系的数据结构,只是很多时候人们用哈希表去实现。但是最为经典的实现是平衡树,比方说java的Treemap是用平衡树实现的(具体是B+还是红黑我没有翻源码就不多说了),才不是什么有顺序的哈希表。java中真正用哈希实现的map是Hashmap。 至于后面的go,你真的懂go?鉴于go的编译器是开源的并且是go语言写的,不去翻源码而是单凭几个简单的测试就武断地开始信口开河,这就是你们golang社区人的学术态度? 话说回来,你真的知道什么叫哈希吗?在用哈希存储key的同时保持顺序,如果你觉得这种事情真的可能的话,我觉得你还是重新学学比较好。
有没有基于value排序的方法?
要排序,最终不能使用 map,只能换数据结构,比如 slice
不知所云
嘤嘤嘤?
自己运行过吗
嘤嘤嘤?
求教个 go map 如果用 struct 作为key 的话,如果struct 的内容改变了,还能找到 value 吗?
乱在写,不解释,看得懂代码的人都知道你在乱写,更别说你去拿别人的代码用了
我对golang 的map 遍历 每次顺序都不一致,完全不能理解; 如果是 非有序的,那么只要插入一次,顺序就变化一次,我能理解; 但是现在是 数据都没变化, 我每次range 遍历的时候,居然 都TMD 不一样;;; 这我及其不能理解; 我做weight 按照权重值获取其中一项也TMD乱了,还必须不用map,自己搞个结构...
map是不得不乱序,而不是有意为之,作者原理不大通,并没有理解哈希的原理,别跟我说什么红黑树,那就不是一个概念,红黑树额外增加了保存顺序的内存,当然是有序的了,手动滑稽,不用红黑树我也能保持顺序,只是占内存而已,咱们讨论的是哈希,而不是其他东西