目录

Go数组对象排序

先明确核心前提:Go中数组是值类型,长度固定,排序时会复制副本(修改副本不影响原数组);切片是引用类型,排序时直接操作底层数组(推荐用切片做排序场景)。排序核心依赖标准库sort包,对象排序需满足其接口规范,下面按场景优先级讲解。

使用标准库 sort 进行排序

一、基础类型数组/切片排序

对于int、string、float64等基础类型,sort包已封装好现成方法,无需自定义逻辑,直接调用即可。但要注意:数组需转切片后排序(否则排序副本无效),切片排序会直接修改原数据。

以int和string类型为例,演示数组转切片排序、直接切片排序的实现,包含正序和逆序:

package main

import (
    "fmt"
    "sort"
)

func main() {
    // 场景1:int数组排序(需转切片,否则排序副本)
    intArr := [5]int{3, 1, 4, 2, 5}
    // 数组转切片:&intArr[0:len(intArr)] 或 intArr[:]
    intSlice := intArr[:]
    sort.Ints(intSlice) // 正序排序(直接修改切片底层数组)
    fmt.Println("int数组排序后:", intArr) // 输出:[1 2 3 4 5](原数组被修改)

    // 场景2:int切片逆序排序
    intSlice2 := []int{7, 5, 9, 3}
    sort.Sort(sort.Reverse(sort.IntSlice(intSlice2)))
    fmt.Println("int切片逆序:", intSlice2) // 输出:[9 7 5 3]

    // 场景3:string切片排序(按ASCII码排序,中文需额外处理)
    strSlice := []string{"banana", "apple", "cherry"}
    sort.Strings(strSlice) // 正序
    fmt.Println("string切片排序:", strSlice) // 输出:[apple banana cherry]

    // 场景4:float64切片排序
    floatSlice := []float64{3.14, 1.59, 2.65, 0.78}
    sort.Float64s(floatSlice)
    fmt.Println("float64切片排序:", floatSlice) // 输出:[0.78 1.59 2.65 3.14]
}

关键注意点:数组必须转切片后排序(如intArr[:]),如果直接对数组调用sort.Ints会报错(类型不匹配);切片排序会直接修改原数据,若需保留原数据,需先复制切片再排序。

如果不想修改原切片,可先用copy函数复制副本,对副本排序:

func main() {
    original := []int{5, 3, 1}
    // 复制副本
    sortedCopy := make([]int, len(original))
    copy(sortedCopy, original)
    // 对副本排序
    sort.Ints(sortedCopy)

    fmt.Println("原切片:", original)   // 输出:[5 3 1](未修改)
    fmt.Println("排序副本:", sortedCopy) // 输出:[1 3 5]
}

二、结构体对象数组/切片排序

用sort.Slice实现排序(无需定义接口,更简洁)

Go 1.8及以上版本提供了sort.Slicesort.SliceStable方法,无需自定义结构体切片类型和实现接口,直接通过匿名函数定义排序规则,代码更简洁,是业务开发的首选简化方案。

简单示例如下:

package main

import (
    "fmt"
    "sort"
)

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{
        {"张三", 25},
        {"李四", 30},
        {"王五", 22},
    }
    fmt.Println("排序前:", users)

    // 用sort.Slice排序:匿名函数定义Less规则(i,j是索引)
    sort.Slice(users, func(i, j int) bool {
        // 按年龄正序
        return users[i].Age < users[j].Age
    })
    fmt.Println("简化排序后:", users) // 输出:[{王五 22} {张三 25} {李四 30}]

    // 逆序排序:调整Less规则的比较符号
    sort.Slice(users, func(i, j int) bool {
        return users[i].Age > users[j].Age
    })
    fmt.Println("简化逆序后:", users) // 输出:[{李四 30} {张三 25} {王五 22}]
}

优势:无需定义额外类型和方法,一行代码完成排序规则定义,可读性和开发效率更高。sort.Slice会直接修改原切片,

若需稳定排序(相等元素保持原顺序),用sort.SliceStable

比如“按注册时间正序,时间相同保持原顺序”,用sort.SliceStable实现稳定排序:

package main

import (
    "fmt"
    "sort"
    "time"
)

type User struct {
    Name       string
    RegisterAt time.Time // 注册时间
}

func main() {
    // 构造数据:张三和李四注册时间相同
    users := []User{
        {"张三", time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC)},
        {"李四", time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC)},
        {"王五", time.Date(2024, 1, 1, 9, 0, 0, 0, time.UTC)},
    }
    fmt.Println("排序前:", users)

    // 稳定排序:按注册时间正序,相同时间保持原顺序
    sort.SliceStable(users, func(i, j int) bool {
        return users[i].RegisterAt.Before(users[j].RegisterAt)
    })
    fmt.Println("稳定排序后:", users)
    // 输出:[{王五 ...9点} {张三 ...10点} {李四 ...10点}](张三仍在李四前)
}

常见问题

Q1. 问题:排序数组后原数组无变化?

原因是Go数组是值类型,直接对数组排序时,传递的是数组副本,修改副本不会影响原数组。

解决:永远将数组转切片后排序(如arr[:]),切片是引用类型,排序会直接操作底层数组,原数组会同步变化(如前面基础类型排序示例)。

// 错误:直接排序数组副本
arr := [3]int{3,1,2}
// sort.Ints(arr) // 编译报错:类型不匹配(需切片)
// 正确:转切片排序
sort.Ints(arr[:])
fmt.Println(arr) // 输出:[1 2 3](原数组变化)

Q2. 问题:结构体排序报“does not implement sort.Interface”?

原因是直接对结构体切片调用sort.Sort,但切片类型未实现sort.Interface的三个方法。

解决:两种解决方案选其一:

① 自定义结构体切片类型并实现三个方法(适合重复使用场景);

② 用sort.Slice简化实现(无需定义接口,适合一次性场景)。

需要 go 1.8+

Q3. 问题:中文排序乱序(按ASCII码排序不符合预期)?

基础的string排序按ASCII码处理,中文会按Unicode编码排序,不符合中文拼音顺序(如“李四”排在“张三”前)。

解决:用第三方库github.com/axgle/mahoniagolang.org/x/text/cases处理中文编码,转成拼音后再排序:

import (
    "fmt"
    "sort"
    "github.com/mozillazg/go-pinyin"
)

type User struct {
    Name string
}

func main() {
    users := []User{{"张三"}, {"李四"}, {"王五"}}
    // 按中文拼音正序排序
    sort.Slice(users, func(i, j int) bool {
        // 转拼音(无声调)
        pinyinI := pinyin.LazyPinyin(users[i].Name, pinyin.NoTone)
        pinyinJ := pinyin.LazyPinyin(users[j].Name, pinyin.NoTone)
        // 拼接拼音字符串后比较
        return strings.Join(pinyinI, "") < strings.Join(pinyinJ, "")
    })
    fmt.Println("中文拼音排序:", users) // 输出:[{李四} {王五} {张三}]
}

Q4. 问题:排序后切片的容量变化?

有同学发现排序后切片容量变了,担心数据丢失。其实sort包的排序是“原地排序”,不会修改切片的长度和容量,只会调整元素顺序。

解决:排序前可通过len()cap()记录切片长度容量,排序后验证:

s := []int{3,1,2}
fmt.Println("排序前:len=", len(s), "cap=", cap(s)) // len=3 cap=3
sort.Ints(s)
fmt.Println("排序后:len=", len(s), "cap=", cap(s)) // len=3 cap=3(无变化)

Q5. 问题:并发场景下排序导致数据竞争?

如果多个goroutine同时读写一个切片并排序,会出现数据竞争问题(排序过程中切片被修改)。

解决:

① 排序前加互斥锁(sync.Mutex),禁止并发修改;

② 对切片副本排序,避免操作原切片:

import "sync"

var (
    data []int
    mu   sync.Mutex
)

func safeSort() {
    mu.Lock()
    defer mu.Unlock()
    // 对副本排序,或直接排序原切片(加锁后安全)
    copyData := make([]int, len(data))
    copy(copyData, data)
    sort.Ints(copyData)
    // 如需更新原数据,加锁后赋值
    data = copyData
}

Q6. 问题:特殊排序规则如何实现?

比如排序字段不是常规的数字、时间,也不想用默认字符串字典排序

解决:自定义字段值映射进行排序

import (
    "fmt"
    "sort"

)


func main() {
  var names=[]string{"张三","李四","王五"}
  var nameMap=map[string]int{
    "李四":1,
    "王五":2,
    "张三":3,
  }
    // 按映射值排序
    sort.Slice(names, func(i, j int) bool {
      namei:=nameMap[names[i]]
      namej:=nameMap[names[j]]
      return namei>namej
    })
    fmt.Println("排序:", names) // 输出:[{李四} {王五} {张三}]
}

总结

  1. 基础类型排序:直接用sort.Intssort.Strings等封装方法,数组转切片后使用;

  2. 结构体排序(一次性场景):优先用sort.Slice(简洁高效),稳定排序用sort.SliceStable

  3. 结构体排序(重复使用场景):自定义结构体切片类型并实现sort.Interface(可复用排序规则,适合工具类代码)。

  4. 特殊排序:自定义字段值映射,实现自定义排序规则

最后:

数组排序先转切片,结构体排序选对简化方法,中文排序需转拼音,并发场景加锁或用副本。

如果大家在复杂场景(如嵌套结构体排序)中踩过坑,欢迎在评论区交流~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-sort-slice/

备用原文链接: https://blog.fiveyoboy.com/articles/go-sort-slice/