Go/encoding/json · map 编码

按键排序后编码 JSON 对象

Go 的 JSON 编码器先收集 map 的键值,再用 PDQsort 按键名排序,避免直接沿用 map 的遍历顺序。

go1.23.0检索与计算核验 2026-10-12
相关概念

工程问题#

Go map 的遍历顺序不固定。编码 JSON 对象时,需要按照转换后的字符串键组织输出,同时处理大小不同、原本有序或分布不均的键集合。

实际实现#

mapEncoder.encode 将键转换为字符串并收集值,调用 slices.SortFunc 按字符串比较排序。go1.23.0 的 SortFunc 使用 pattern-defeating quicksort:以枢轴划分区间,小区间转插入排序,过多不平衡划分后转堆排序。

处理流程#

  1. 把 map 的键转换为 JSON 键名,保存键值对
  2. 按键名调用 slices.SortFunc,进入 PDQsort
  3. 划分并递归处理较短区间;小区间及退化输入走相应回退
  4. 按排序结果逐个输出键和值

源码片段#

固定版本 · 原文摘录
src/encoding/json/encode.gogo1.23.0 · L750–L764
查看原文件
750	// Extract and sort the keys.751	var (752		sv  = make([]reflectWithString, v.Len())753		mi  = v.MapRange()754		err error755	)756	for i := 0; mi.Next(); i++ {757		if sv[i].ks, err = resolveKeyName(mi.Key()); err != nil {758			e.error(fmt.Errorf("json: encoding error for type %q: %q", v.Type().String(), err.Error()))759		}760		sv[i].v = mi.Value()761	}762	slices.SortFunc(sv, func(i, j reflectWithString) int {763		return strings.Compare(i.ks, j.ks)764	})
mapEncoder.encode → slices.SortFunc

JSON 编码器收集转换后的字符串键和对应值,再按键名排序;下方循环按排序结果编码成员。

src/slices/sort.gogo1.23.0 · L21–L33
查看原文件
21// SortFunc sorts the slice x in ascending order as determined by the cmp22// function. This sort is not guaranteed to be stable.23// cmp(a, b) should return a negative number when a < b, a positive number when24// a > b and zero when a == b or a and b are incomparable in the sense of25// a strict weak ordering.26//27// SortFunc requires that cmp is a strict weak ordering.28// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.29// The function should return 0 for incomparable items.30func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {31	n := len(x)32	pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp)33}
slices.SortFunc

SortFunc 明确是不稳定排序,实际分派到 pdqsortCmpFunc,limit 取输入长度的位数。

src/slices/zsortanyfunc.gogo1.23.0 · L61–L81
查看原文件
61func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) {62	const maxInsertion = 126364	var (65		wasBalanced    = true // whether the last partitioning was reasonably balanced66		wasPartitioned = true // whether the slice was already partitioned67	)6869	for {70		length := b - a7172		if length <= maxInsertion {73			insertionSortCmpFunc(data, a, b, cmp)74			return75		}7677		// Fall back to heapsort if too many bad choices were made.78		if limit == 0 {79			heapSortCmpFunc(data, a, b, cmp)80			return81		}
pdqsortCmpFunc:短区间与退化回退

长度不超过 12 时调用插入排序;坏划分预算耗尽时调用堆排序。这是快速排序的工程变体。

src/slices/zsortanyfunc.gogo1.23.0 · L106–L127
查看原文件
106		// Probably the slice contains many duplicate elements, partition the slice into107		// elements equal to and elements greater than the pivot.108		if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) {109			mid := partitionEqualCmpFunc(data, a, b, pivot, cmp)110			a = mid111			continue112		}113114		mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp)115		wasPartitioned = alreadyPartitioned116117		leftLen, rightLen := mid-a, b-mid118		balanceThreshold := length / 8119		if leftLen < rightLen {120			wasBalanced = leftLen >= balanceThreshold121			pdqsortCmpFunc(data, a, mid, limit, cmp)122			a = mid + 1123		} else {124			wasBalanced = rightLen >= balanceThreshold125			pdqsortCmpFunc(data, mid+1, b, limit, cmp)126			b = mid127		}
pdqsortCmpFunc:分区及递归

重复值可走独立分区;普通路径按枢轴划分,只递归较短一侧,另一侧继续循环。

四段原文分别连接实际编码调用、公共排序入口以及混合排序的分区和回退。完整文件还包括枢轴选择、模式打乱与输出循环。

收益与取舍#

工程分析

带来的收益

  • 键名排序不依赖 map 的遍历次序
  • 同一排序实现处理短数组、部分有序输入和不平衡划分

付出的代价

  • 先收集键值对需要 O(n) 辅助存储,键名转换另有分配成本
  • 比较次数与字符串比较成本应分开计算;长公共前缀会增加每次比较的工作量
  • SortFunc 不保证稳定;不同原始键转换为相同字符串的情况,不能由此推出规范化 JSON 或唯一字节表示

可比较的选择

按 map 遍历顺序直接编码省去排序,但输出键顺序不确定,不满足此编码器按键排序的约定。

稳定归并排序可保留相等键的原始顺序,但 map 原始遍历本身并不固定,而且需要不同的临时存储或合并策略。

版本与适用范围#

对应 Go go1.23.0 的 encoding/json mapEncoder 与 slices.SortFunc。这里只证明键排序调用链及 PDQsort 的混合策略,不把 JSON 键排序等同于数字、转义等规则完整的 canonical JSON。

参考来源#

源码、官方文档与原始论文
01
mapEncoder.encode → slices.SortFunc

src/encoding/json/encode.go · mapEncoder.encode → slices.SortFunc

源码go1.23.0
02
slices.SortFunc

src/slices/sort.go · slices.SortFunc

源码go1.23.0
03
pdqsortCmpFunc:短区间与退化回退

src/slices/zsortanyfunc.go · pdqsortCmpFunc:短区间与退化回退

源码go1.23.0
04
pdqsortCmpFunc:分区及递归

src/slices/zsortanyfunc.go · pdqsortCmpFunc:分区及递归

源码go1.23.0
19 个匹配案例