问题
用户反馈说某个本来应该是某个顺序固定的列表,每次刷新顺序都不固定。原因可能是排序问题, 追踪的时候发现了下面的代码(做了简化)
func testSortMap() []map[string]interface{} { normalMap := make(map[string]int, 0) result := make([]map[string]interface{}, 0) normalMap["a"] = 1 normalMap["b"] = 2 normalMap["c"] = 3 for key, value := range normalMap { result = append(result, map[string]interface{}{ "key": key, "value": value, }) } return result }
原因
实际的过程并不是在一个函数中完成的,还涉及到数据库的读取以及一些运算,刚开始的时候觉得返回的是一个 Slice, 也很奇怪为什么会出现排序不固定的问题,又往下层追踪才发现实际上这个 Slice 是由一个 map 转换过来的。(也不知道 当初为什么要这么设计),golang 中 map 默认是不排序的。所以下面的代码输出是不固定的:
func printMap() { normalMap := make(map[string]int, 0) normalMap["a"] = 1 normalMap["b"] = 2 normalMap["c"] = 3 for key, value := range normalMap { fmt.Println(key, value) } }
解决
至于为什么不固定,这里不深究,但如果只是要把 map 根据 value 来排序,可以通过写 len/less/swap 函数 实现 sort 接口,来对 map 排序。
首先需要定义一个结构体:
type Pair struct { Key string Value int } type PairList []Pair
然后实现 sort 接口:
func (p PairList) Len() int { return len(p) } func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value } func (p PairList) Swap(i, j int){ p[i], p[j] = p[j], p[i] }
最后就可以按照如下方法使用:
func rankByWordCount(wordFrequencies map[string]int) PairList{ pl := make(PairList, len(wordFrequencies)) i := 0 for k, v := range wordFrequencies { pl[i] = Pair{k, v} i++ } sort.Sort(sort.Reverse(pl)) return pl }
(以上代码来自:https://stackoverflow.com/questions/18695346/how-to-sort-a-mapstringint-by-its-values) 同理可以修改上述代码按照 key 排序。
但问题也来了,上面的方法需要定义结构体,需要改动较多的代码。下面的代码可以比较“无痛”排序:
package main import "fmt" import "sort" func main() { m := map[string]int{ "One": 100, "Two": 2, "Three": 3, "Ten": 10, "Fifty": 50, } vs := NewValSorter(m) fmt.Printf("%v\n", *vs) vs.Sort() fmt.Printf("%v\n", *vs) } type ValSorter struct { Keys []string Vals []int } func NewValSorter(m map[string]int) *ValSorter { vs := &ValSorter{ Keys: make([]string, 0, len(m)), Vals: make([]int, 0, len(m)), } for k, v := range m { vs.Keys = append(vs.Keys, k) vs.Vals = append(vs.Vals, v) } return vs } func (vs *ValSorter) Sort() { sort.Sort(vs) } func (vs *ValSorter) Len() int { return len(vs.Vals) } func (vs *ValSorter) Less(i, j int) bool { return vs.Vals[i] < vs.Vals[j] } func (vs *ValSorter) Swap(i, j int) { vs.Vals[i], vs.Vals[j] = vs.Vals[j], vs.Vals[i] vs.Keys[i], vs.Keys[j] = vs.Keys[j], vs.Keys[i] }
基本原理也是实现 sort 接口,简单版如下:
type sortedMap struct { m map[string]int s []string } func (sm *sortedMap) Len() int { return len(sm.m) } func (sm *sortedMap) Less(i, j int) bool { return sm.m[sm.s[i]] > sm.m[sm.s[j]] } func (sm *sortedMap) Swap(i, j int) { sm.s[i], sm.s[j] = sm.s[j], sm.s[i] } func sortedKeys(m map[string]int) []string { sm := new(sortedMap) sm.m = m sm.s = make([]string, len(m)) i := 0 for key, _ := range m { sm.s[i] = key i++ } sort.Sort(sm) return sm.s }
最终
本着“一定有更简洁的方法”原则,其实 golang(1.8之后)提供了 sort.Slice 函数,两行代码解决问题:
sort.Slice(ss, func(i, j int) bool { return ss[i].Value > ss[j].Value // 降序 })
因为实际代码比例子中的复杂,实现上述几种比较复杂的方法有一定的难度,但是这个最终的解决方法非常契合需求, 而且简单明了,所以记录一下。