是的, 默认情况下,向一个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
关键字)是随机的,而不是遵循元素的添加顺序。这是什么意思?很特别么?
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!!!