编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
        • 目录介绍
        • 16.1 本章学习目标
        • 16.2 高频标准库速览
          • 16.2.1 fmt / strings / strconv
          • 16.2.2 time:时间处理
          • 16.2.3 encoding/json
          • 16.2.4 net/http
          • 16.2.5 log/slog(Go 1.21+ 结构化日志)
          • 16.2.6 综合案例与思考
        • 16.3 slices 包(Go 1.21+)
          • 16.3.1 slices.Sort / SortFunc / SortStableFunc
          • 16.3.2 slices.Index / Contains / Equal / Compare
          • 16.3.3 slices.Clone / Delete / Insert / Concat
          • 16.3.4 综合案例与思考
        • 16.4 maps 包(Go 1.21+)
          • 16.4.1 maps.Keys / Values / Clone
          • 16.4.2 maps.Copy / Equal / DeleteFunc
          • 16.4.3 综合案例与思考
        • 16.5 cmp 包:通用比较
          • 16.5.1 综合案例与思考
        • 16.6 泛型语法
          • 16.6.1 类型参数:func FT any
          • 16.6.2 类型约束:接口的新角色
          • 16.6.3 comparable 约束
          • 16.6.4 ~T 底层类型约束
          • 16.6.5 综合案例与思考
        • 16.7 泛型类型 vs 泛型函数
          • 16.7.1 综合案例与思考
        • 16.8 何时用泛型,何时不用
        • 16.9 综合示例:手写一个泛型 LRU Cache
        • 16.10 本章底层原理(简介)
        • 16.11 Go 新手陷阱 Top 5
        • 16.12 思考题
        • 16.13 训练题
        • 16.14 推荐阅读
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

标准库与泛型

# 第 16 章 标准库与泛型

Go 1.18 引入泛型,1.21 引入 slices / maps / cmp 三大标准库——Go 终于"现代化"了。 关键词:slices / maps / cmp、泛型语法、类型约束、comparable、~T、cmp.Ordered


# 目录介绍

  • 16.1 本章学习目标
  • 16.2 高频标准库速览
    • 16.2.1 fmt / strings / strconv
    • 16.2.2 time:时间处理
    • 16.2.3 encoding/json
    • 16.2.4 net/http
    • 16.2.5 log/slog(Go 1.21+ 结构化日志)
    • 16.2.6 综合案例与思考
  • 16.3 slices 包(Go 1.21+)
    • 16.3.1 slices.Sort / SortFunc / SortStableFunc
    • 16.3.2 slices.Index / Contains / Equal / Compare
    • 16.3.3 slices.Clone / Delete / Insert / Concat
    • 16.3.4 综合案例与思考
  • 16.4 maps 包(Go 1.21+)
    • 16.4.1 maps.Keys / Values / Clone
    • 16.4.2 maps.Copy / Equal / DeleteFunc
    • 16.4.3 综合案例与思考
  • 16.5 cmp 包:通用比较
    • 16.5.1 综合案例与思考
  • 16.6 泛型语法
    • 16.6.1 类型参数:func F[T any](v T)
    • 16.6.2 类型约束:接口的新角色
    • 16.6.3 comparable 约束
    • 16.6.4 ~T 底层类型约束
    • 16.6.5 综合案例与思考
  • 16.7 泛型类型 vs 泛型函数
    • 16.7.1 综合案例与思考
  • 16.8 何时用泛型,何时不用
  • 16.9 综合示例:手写一个泛型 LRU Cache
  • 16.10 本章底层原理(简介)
  • 16.11 Go 新手陷阱 Top 5
  • 16.12 思考题
  • 16.13 训练题
  • 16.14 推荐阅读

# 16.1 本章学习目标

学完本章你应当能够:

  • ✅ 能用 slices / maps 替代手写循环,写出更简洁的代码
  • ✅ 能用 cmp.Compare / cmp.Or 做通用比较和缺省值处理
  • ✅ 能写一个泛型函数(func F[T any](v T) T)
  • ✅ 能定义泛型类型(type Stack[T any] struct { ... })
  • ✅ 能解释 comparable 与 cmp.Ordered 的区别——什么场景选哪个
  • ✅ 能使用 ~T 底层类型约束——让泛型适配自定义类型
  • ✅ 知道何时不该用泛型——不是所有重复代码都要泛型化

本章是 Go 入门教程的收尾章。泛型是 Go 1.18 最重要的语言变革——它让 slices.Sort、maps.Keys 这些曾经"写一百遍"的代码变成了标准库的一行调用。


# 16.2 高频标准库速览

# 16.2.1 fmt / strings / strconv

三个字符串处理核心库——格式、查找、转换:

import (
    "fmt"
    "strconv"
    "strings"
)

// fmt——格式化输出
fmt.Sprintf("v%d.%d.%d", 1, 2, 3)          // → "v1.2.3"
fmt.Printf("%-10s | %02d\n", "id", 5)       // → "id         | 05"

// strings——字符串操作
strings.Contains("hello world", "world")     // → true
strings.HasPrefix("file.txt", "file")        // → true
strings.Split("a,b,c", ",")                  // → ["a","b","c"]
strings.Join([]string{"a","b"}, "/")         // → "a/b"
strings.ReplaceAll("foo bar foo", "foo", "baz") // → "baz bar baz"
strings.TrimSpace("  hello \n")              // → "hello"
strings.Builder——高性能拼字符串(见 §16.2.6)

// strconv——字符串与数字互转
strconv.Itoa(42)                             // → "42"
strconv.Atoi("42")                           // → 42, nil
strconv.FormatFloat(3.14, 'f', 2, 64)        // → "3.14"
strconv.ParseBool("true")                    // → true, nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 16.2.2 time:时间处理

import "time"

// 获取时间
now := time.Now()                             // 当前时间
today := time.Now().Truncate(24 * time.Hour)  // 当天零点

// 格式化——用"参考时间"模板(2006-01-02 15:04:05)
now.Format("2006-01-02 15:04:05")            // → "2026-06-13 10:00:00"
now.Format(time.RFC3339)                      // → "2026-06-13T10:00:00Z"

// 解析
t, _ := time.Parse("2006-01-02", "2026-06-13")

// Duration——时间间隔的"类型安全单位"
d := 3 * time.Second + 500 * time.Millisecond
time.Sleep(2 * time.Second)

// 定时器与超时
timer := time.NewTimer(5 * time.Second)
<-timer.C                                    // 等 5 秒

ticker := time.NewTicker(time.Second)        // 每秒触发
for range ticker.C { ... }

select {
case <-time.After(3 * time.Second):          // 超时
    fmt.Println("超时")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 16.2.3 encoding/json

import "encoding/json"

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age,omitempty"`  // omitempty——零值不输出
    Email string `json:"-"`              // ——不序列化
}

// 序列化
u := User{Name: "张三", Age: 28}
data, _ := json.Marshal(u)
fmt.Println(string(data))  // {"name":"张三","age":28}

// 缩进输出——开发调试用
data, _ = json.MarshalIndent(u, "", "  ")

// 反序列化
var u2 User
json.Unmarshal(data, &u2)

// 流式——大 JSON 用 Decoder(不用 ReadAll)
decoder := json.NewDecoder(file)
decoder.Decode(&u)

// 写到 ResponseWriter——直接 Encode
json.NewEncoder(w).Encode(u)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 16.2.4 net/http

import "net/http"

// ① 最简服务——3 行
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s", r.URL.Query().Get("name"))
})
http.ListenAndServe(":8080", nil)

// ② 带超时的客户端
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.example.com/data")
defer resp.Body.Close()

// ③ JSON API——一行返回
json.NewEncoder(w).Encode(response)

// ④ 中间件模式
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 16.2.5 log/slog(Go 1.21+ 结构化日志)

import "log/slog"

// 结构化日志——字段化,可查询
slog.Info("支付完成",
    "order_id", "12345",
    "amount", 99.99,
    "latency", time.Since(start),
)
// 输出(TextHandler):
// time=... level=INFO msg="支付完成" order_id=12345 amount=99.99 latency=50ms

// JSON 格式——适合生产日志采集
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)

// 预绑定公共字段——service 和 env 自动加到每条日志
logger := slog.Default().With("service", "pay", "env", "prod")
logger.Info("服务启动")
// → {"level":"INFO","msg":"服务启动","service":"pay","env":"prod"}

// 不同级别
slog.Debug("调试信息")    // 生产默认不输出
slog.Warn("接近限制")     // 警告
slog.Error("支付失败", "err", err)  // 错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 16.2.6 综合案例与思考

综合案例:用标准库实现日志分析工具

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "time"
)

func main() {
    file, _ := os.Open("access.log")
    defer file.Close()

    scanner := bufio.NewScanner(file)
    var count int
    start := time.Now()

    for scanner.Scan() {
        line := scanner.Text()
        // 用 strings 和 fmt 分析日志行
        if strings.Contains(line, "ERROR") {
            fields := strings.Fields(line)
            if len(fields) >= 2 {
                timestamp := fields[0] + " " + fields[1]
                msg := strings.Join(fields[2:], " ")
                fmt.Printf("[ERROR] %s: %s\n", timestamp, msg)
                count++
            }
        }
    }
    duration := time.Since(start)
    fmt.Printf("\n共 %d 条错误,耗时 %v\n", count, duration)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

案例知识融合:一个日志分析工具集成了 os/bufio/strings/fmt/time 五个标准库——不到 30 行。

思考题:

  1. strings.Builder 和 + 拼字符串有什么区别?什么时候该用 Builder?
  2. json.Unmarshal vs json.NewDecoder——从文件读 JSON 时哪个更好?

# 16.3 slices 包(Go 1.21+)

# 16.3.1 slices.Sort / SortFunc / SortStableFunc

import "slices"

nums := []int{3, 1, 4, 1, 5, 9}

// Sort——升序排序(要求元素类型实现 cmp.Ordered)
slices.Sort(nums)  // → [1, 1, 3, 4, 5, 9]

// SortFunc——自定义比较器
type User struct{ Name string; Age int }
users := []User{{"张三", 28}, {"李四", 22}, {"王五", 35}}

slices.SortFunc(users, func(a, b User) int {
    return a.Age - b.Age  // 按年龄升序
})

// SortStableFunc——稳定排序(相等元素保持原顺序)
slices.SortStableFunc(users, func(a, b User) int {
    return cmp.Compare(a.Age, b.Age)
})

// 反序——用 Reverse
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(b.Age, a.Age)  // 或 slices.Reverse
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 16.3.2 slices.Index / Contains / Equal / Compare

nums := []int{10, 20, 30, 20, 50}

// 查找
slices.Index(nums, 20)         // → 1(第一个匹配的索引)
slices.Contains(nums, 30)      // → true

// 比较
a := []int{1, 2, 3}
b := []int{1, 2, 3}
c := []int{1, 2, 4}
slices.Equal(a, b)             // → true
slices.Equal(a, c)             // → false
slices.Compare(a, c)           // → -1(a < c,字典序)

// 最大值/最小值
slices.Max(nums)               // → 50
slices.Min(nums)               // → 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 16.3.3 slices.Clone / Delete / Insert / Concat

// Clone——深拷贝(不共享底层数组)
original := []int{1, 2, 3}
cloned := slices.Clone(original)

// Delete——删除元素(O(n))
nums := []int{1, 2, 3, 4, 5}
nums = slices.Delete(nums, 1, 3)  // → [1, 4, 5](删除索引 1,2)

// Insert——插入元素(O(n))
nums = slices.Insert(nums, 1, 99) // → [1, 99, 4, 5]

// Concat——拼接多个切片(不用 for 循环)
combined := slices.Concat([]int{1,2}, []int{3,4}, []int{5})
// → [1, 2, 3, 4, 5]

// Grow——预分配容量
buf := slices.Grow(nil, 1000)     // 分配 1000 容量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 16.3.4 综合案例与思考

综合案例:用 slices 重构手写循环

// ❌ 旧代码——手写排序 + 遍历
func oldWay(users []User) []User {
    // 按年龄排序(手写冒泡或 sort.Slice)
    sort.Slice(users, func(i, j int) bool {
        return users[i].Age < users[j].Age
    })
    // 取前 5 个(手写切片)
    if len(users) > 5 {
        users = users[:5]
    }
    return users
}

// ✅ 新代码——slices 包一行搞定
func newWay(users []User) []User {
    slices.SortFunc(users, func(a, b User) int {
        return cmp.Compare(a.Age, b.Age)
    })
    n := min(5, len(users))
    return slices.Clone(users[:n])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

案例知识融合:slices 替代了 sort.Slice + 手写切片操作——原来 8 行变成 2 行核心逻辑。

思考题:

  1. slices.Delete 删除元素后——原底层数组的内存会释放吗?被删除位置的旧值还能被 GC 吗?
  2. slices.Contains 在百万级切片中查找——时间复杂度是多少?什么时候应该换 map?

# 16.4 maps 包(Go 1.21+)

# 16.4.1 maps.Keys / Values / Clone

import "maps"

m := map[string]int{"a": 1, "b": 2, "c": 3}

// Keys——获取所有 key(无序)
keys := maps.Keys(m)
// keys = ["a", "b", "c"](顺序不确定)

// Values——获取所有 value(无序)
values := maps.Values(m)
// values = [1, 2, 3](顺序不确定)

// Clone——浅拷贝(key-value 是标量,独立了)
m2 := maps.Clone(m)
m2["d"] = 4  // 不影响 m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 16.4.2 maps.Copy / Equal / DeleteFunc

dst := map[string]int{"x": 99}
src := map[string]int{"a": 1, "b": 2, "x": 100}

// Copy——合并 map(覆盖同名 key)
maps.Copy(dst, src)
// dst = {"a": 1, "b": 2, "x": 100}

// Equal——比较两个 map 是否相同
maps.Equal(map[string]int{"a": 1}, map[string]int{"a": 1}) // → true

// DeleteFunc——按条件删除
m := map[string]int{"a": 1, "b": 2, "c": 3}
maps.DeleteFunc(m, func(key string, value int) bool {
    return value < 3  // 删除 value < 3 的条目
})
// m = {"c": 3}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 16.4.3 综合案例与思考

综合案例:统计单词频率——老式写法 vs maps 包

// ❌ 老方法——手写循环收集 keys
func oldKeys(counts map[string]int) []string {
    keys := make([]string, 0, len(counts))
    for k := range counts {
        keys = append(keys, k)
    }
    return keys
}

// ✅ 新方法——maps.Keys 一行
keys := maps.Keys(counts)

// 完整统计——slices + maps 组合拳
func topWords(text string, n int) []string {
    counts := make(map[string]int)
    for _, word := range strings.Fields(text) {
        counts[word]++
    }
    keys := maps.Keys(counts)
    slices.SortFunc(keys, func(a, b string) int {
        return cmp.Compare(counts[b], counts[a])  // 按频率降序
    })
    return keys[:min(n, len(keys))]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

案例知识融合:maps.Keys + slices.SortFunc 组合——原来需要 for 循环收集 + 排序的代码,变成了两行链式调用。

思考题:

  1. maps.Keys 返回的切片——每次调用都分配新切片吗?连续两次调用的结果顺序一致吗?
  2. maps.Clone 是浅拷贝——如果 value 是指针,克隆后两个 map 共享底层数据吗?

# 16.5 cmp 包:通用比较

import "cmp"

// Compare——三值比较(-1=小于, 0=等于, 1=大于)
cmp.Compare(3, 5)   // → -1
cmp.Compare(5, 3)   // → 1
cmp.Compare(5, 5)   // → 0
cmp.Compare("a", "b")  // → -1(字典序)

// Or——返回第一个非零值(类似 C 的 ?:)
cmp.Or(0, 0, 42)     // → 42
cmp.Or("", "default") // → "default"
cmp.Or(100, fallback) // → 100(100 非零)

// Ordered 约束——所有可排序的类型
// = int, int8~int64, uint8~uint64, float32, float64, string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 16.5.1 综合案例与思考

// 用 cmp.Compare 做通用比较器——不硬编码减法(防溢出)
func sortBy[T cmp.Ordered](items []T) {
    slices.SortFunc(items, func(a, b T) int {
        return cmp.Compare(a, b)  // ★ 不用 a - b——不会溢出
    })
}

// 用 cmp.Or 做链式缺省
config := cmp.Or(
    os.Getenv("API_URL"),     // 环境变量
    cfg.APIURL,               // 配置文件
    "http://localhost:8080",  // 硬编码默认值
)
1
2
3
4
5
6
7
8
9
10
11
12
13

思考题:

  1. cmp.Compare(a, b) 和 a - b(int 减法)有什么区别?为什么 int 减法不是安全的比较?
  2. cmp.Ordered 包含 float32/float64——按 float 排序有什么陷阱?(NaN 怎么处理?)

# 16.6 泛型语法

# 16.6.1 类型参数:func F[T any](v T)

泛型函数用方括号 [] 声明类型参数——不是尖括号 < >:

// 泛型函数——适用于任何 T
func First[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T         // T 的零值
        return zero, false
    }
    return slice[0], true
}

// 使用——类型推断(不需要写 First[int])
val, ok := First([]int{1, 2, 3})
fmt.Println(val)  // 1

// 也可以显式指定类型参数
val, ok = First[string]([]string{"a", "b"})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 16.6.2 类型约束:接口的新角色

在泛型中,接口被扩展为"类型约束"——不只是"有哪些方法",还可以是"有哪些类型":

// 约束:只要有 String() 方法就行
type Stringer interface {
    String() string
}

func Print[T Stringer](v T) {
    fmt.Println(v.String())
}
1
2
3
4
5
6
7
8

多类型约束——用 | 组合:

// 约束:只能是 int 或 float64
type Number interface {
    int | float64
}

func Sum[T Number](a, b T) T {
    return a + b
}

Sum(1, 2)       // ✅ int
Sum(1.5, 2.5)   // ✅ float64
// Sum("a", "b") // ❌ string 不是 Number
1
2
3
4
5
6
7
8
9
10
11
12

# 16.6.3 comparable 约束

comparable 是内置约束——所有可以用 == 和 != 比较的类型:

// comparable:所有可比类型——作为 map key
func Keys[T comparable, V any](m map[T]V) []T {
    keys := make([]T, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

m := map[string]int{"a": 1}
Keys(m)  // ✅ string 是 comparable

// ❌ 不能用作 comparable 的类型
// 切片、map、函数——这些不能用 == 比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14

comparable vs cmp.Ordered:

comparable cmp.Ordered
包含 所有能用 == 比较的类型 所有能排序的类型
int / string ✅ ✅
slice / map / func ❌ ❌
struct(所有字段 comparable) ✅ ❌
float64 ✅ ✅(但 NaN 排序有问题)
指针 / channel ✅ ❌

# 16.6.4 ~T 底层类型约束

~T 匹配底层类型为 T 的所有类型——包括自定义类型:

// 自定义类型——基于 int
type MyInt int
type YourInt int

// ❌ 不带 ~——只匹配 int 本身
func AddInt[T int](a, b T) T { return a + b }
AddInt(1, 2)       // ✅
// AddInt(MyInt(1), MyInt(2)) // ❌ MyInt 不是 int

// ✅ 带 ~——匹配底层类型为 int 的所有类型
func Add[T ~int](a, b T) T { return a + b }
Add(1, 2)                    // ✅
Add(MyInt(1), MyInt(2))      // ✅
Add(YourInt(3), YourInt(4))  // ✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实际用途——让泛型适配标准库的自定义类型:

// slices.Contains 的定义——接受所有底层为 []E 的类型
// func Contains[S ~[]E, E comparable](s S, v E) bool

// 自定义类型也能用
type MySlice []int
s := MySlice{1, 2, 3}
slices.Contains(s, 2)  // ✅ ~[]E 匹配了 MySlice
1
2
3
4
5
6
7

# 16.6.5 综合案例与思考

综合案例:实现泛型 Set

package main

import (
    "fmt"
    "maps"
)

// 泛型 Set——key 必须 comparable
type Set[T comparable] map[T]struct{}

func NewSet[T comparable](items ...T) Set[T] {
    s := make(Set[T])
    for _, item := range items {
        s[item] = struct{}{}
    }
    return s
}

func (s Set[T]) Add(item T) {
    s[item] = struct{}{}
}

func (s Set[T]) Contains(item T) bool {
    _, ok := s[item]
    return ok
}

func (s Set[T]) Remove(item T) {
    delete(s, item)
}

func (s Set[T]) ToSlice() []T {
    return maps.Keys(s)
}

func main() {
    s := NewSet(1, 2, 3, 2, 1)
    fmt.Println("contains 2:", s.Contains(2))  // true
    fmt.Println("elements:", s.ToSlice())       // [1 2 3](顺序不定)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

案例知识融合:泛型 Set 的核心——类型参数 T comparable 约束了 key 必须可比。map[T]struct{} 是 Go 实现 Set 的标准模式——不占额外内存。

思考题:

  1. comparable 约束允许 struct——但如果 struct 里有 slice 字段,能通过编译吗?
  2. ~int 约束下 MyInt 能用 + 吗?~int 和 int 在运算符层面有什么区别?

# 16.7 泛型类型 vs 泛型函数

除了泛型函数——Go 也支持泛型类型(参数化的 struct / interface):

// 泛型 Stack
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// 使用
intStack := &Stack[int]{}
intStack.Push(42)
val, _ := intStack.Pop()
fmt.Println(val)  // 42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 16.7.1 综合案例与思考

综合案例:泛型 PriorityQueue

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type PriorityQueue[T cmp.Ordered] struct {
    items []T
}

func (pq *PriorityQueue[T]) Push(item T) {
    pq.items = append(pq.items, item)
    // 每次插入后排序(简单策略,生产用堆)
    slices.Sort(pq.items)
}

func (pq *PriorityQueue[T]) Pop() (T, bool) {
    if len(pq.items) == 0 {
        var zero T
        return zero, false
    }
    item := pq.items[0]
    pq.items = pq.items[1:]
    return item, true
}

func main() {
    pq := &PriorityQueue[int]{}
    pq.Push(3)
    pq.Push(1)
    pq.Push(2)
    for {
        if v, ok := pq.Pop(); ok {
            fmt.Println(v)  // 1, 2, 3
        } else {
            break
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

思考题:

  1. 泛型类型的每个类型参数实例化后——是独立的类型吗?Stack[int] 和 Stack[string] 之间能不能互相赋值?

# 16.8 何时用泛型,何时不用

用泛型的信号:

场景 示例
数据结构容器(Stack/Set/Cache) type LRU[K comparable, V any]
算法函数适用于多种类型 func Max[T cmp.Ordered](a, b T) T
消除 interface{} 和类型断言 替换 func Compare(a, b any) int
同一逻辑,不同数据类型 func Clone[T any](s []T) []T

不用泛型的反信号:

场景 不用泛型的原因
只有 2 个调用方(如只用于 int 和 string) 手写两个函数更清晰,泛型增加复杂度
类型差异需要 switch 分支逻辑 用 interface + type switch 更合适
公共 API——泛型让签名"吓人" func Print(v interface{}) 比 func Print[T Stringer](v T) 可读
性能关键路径——担心代码膨胀 Go 的泛型用 GC shape stenciling——同 shape 的类型共享代码,不膨胀

黄金法则:先写具体类型的代码,发现重复后再考虑泛型化。不要为了"未来可能"而泛型。


# 16.9 综合示例:手写一个泛型 LRU Cache

package main

import (
    "container/list"
    "fmt"
    "sync"
)

// LRU Cache——K 必须 comparable,V 任意
type LRU[K comparable, V any] struct {
    mu       sync.Mutex
    capacity int
    items    map[K]*list.Element
    order    *list.List
}

type entry[K comparable, V any] struct {
    key   K
    value V
}

func NewLRU[K comparable, V any](capacity int) *LRU[K, V] {
    return &LRU[K, V]{
        capacity: capacity,
        items:    make(map[K]*list.Element),
        order:    list.New(),
    }
}

func (c *LRU[K, V]) Get(key K) (V, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()

    if elem, ok := c.items[key]; ok {
        c.order.MoveToFront(elem)
        return elem.Value.(*entry[K, V]).value, true
    }
    var zero V
    return zero, false
}

func (c *LRU[K, V]) Put(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()

    // 已存在——更新并移到队首
    if elem, ok := c.items[key]; ok {
        c.order.MoveToFront(elem)
        elem.Value.(*entry[K, V]).value = value
        return
    }

    // 新元素——插入队首
    elem := c.order.PushFront(&entry[K, V]{key, value})
    c.items[key] = elem

    // 超过容量——删除队尾(最久未使用)
    if c.order.Len() > c.capacity {
        oldest := c.order.Back()
        if oldest != nil {
            c.order.Remove(oldest)
            delete(c.items, oldest.Value.(*entry[K, V]).key)
        }
    }
}

func main() {
    cache := NewLRU[string, int](2)
    cache.Put("a", 1)
    cache.Put("b", 2)
    fmt.Println(cache.Get("a"))  // 1, true(a 被访问,移到前面)
    cache.Put("c", 3)             // b 被淘汰
    fmt.Println(cache.Get("b"))  // 0, false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

设计要点:

  • K comparable, V any——两个类型参数,K 必须是 map 的合法 key
  • container/list 双向链表维护访问顺序
  • sync.Mutex 保证并发安全
  • 泛型让一套代码适配所有 key/value 类型——无需 interface{} 和类型断言

# 16.10 本章底层原理(简介)

本章概念 卷三对应篇
泛型编译器实现(GC shape stenciling) 24.泛型与类型约束
slices/maps 的底层优化 24.泛型与类型约束
comparable 的编译器内建支持 24.泛型与类型约束

# 16.11 Go 新手陷阱 Top 5

# 陷阱 说明
1 用 slices.Contains 在百万级切片中查找 O(n)——应换 map。slices.Contains 只适合小切片。
2 用泛型替代 interface{} 时强制约束 有些场景 interface{} 更灵活——泛型不是万能药。
3 comparable 误以为包含所有可比类型 slice/map/func 不能用 ==,不能用 comparable。
4 类型 switch + 泛型混用——设计味道 泛型消除了 switch——如果还在 switch,说明约束太宽。
5 滥用泛型把简单代码搞复杂 "泛型是工具,不是目标"——如果普通函数已经够用,别加类型参数。

# 16.12 思考题

  1. slices 的设计哲学:slices.Sort 只接受 cmp.Ordered 类型——为什么不让 Sort 接受一个 comparator 参数(像 SortFunc)作为默认?哪个更好?

  2. 泛型 vs 代码生成:Java 的泛型是"类型擦除"(编译后只有一套代码),C++ 模板是"代码膨胀"(每种类型生成一套)。Go 的泛型用 GC shape stenciling——和两者有什么不同?

  3. comparable 的边界:struct { a int; b []int } 包含切片字段——能用 comparable 约束吗?为什么?

  4. ~T 的陷阱:~int 约束下——自定义类型 type Celsius int 和一个函数 func F[T ~int](a, b T) T { return a + b } ——调用 F(Celsius(0), Celsius(100)) 返回什么类型?

  5. 何时不泛型:你已经写了一个 Max 函数只用于 int。同事说"何不改成泛型支持所有 Ordered 类型"——什么情况下你应该拒绝?什么情况下应该同意?


# 16.13 训练题

训练 1:用 slices 和 maps 重构以下代码(去掉所有 for 循环):

// 旧代码
func process(users []User) ([]string, map[string]User) {
    names := make([]string, 0, len(users))
    m := make(map[string]User)
    for _, u := range users {
        names = append(names, u.Name)
        m[u.Email] = u
    }
    sort.Strings(names)
    return names, m
}
1
2
3
4
5
6
7
8
9
10
11

训练 2:写一个泛型函数 Filter[T any](slice []T, predicate func(T) bool) []T——过滤切片中满足条件的元素。然后用它过滤 []int(保留偶数)和 []string(保留长度 > 3 的)。

训练 3:实现一个泛型的并发安全 Map 包装器 type SafeMap[K comparable, V any] struct { mu sync.RWMutex; data map[K]V }——实现 Get/Set/Delete/Len 方法。


# 16.14 推荐阅读

  • 入门卷:第 5 章 复合类型——slice/map 基础
  • 入门卷:第 14 章 sync 包——Mutex/RWMutex
  • 卷三:24.泛型与类型约束——泛型编译器实现、GC shape stenciling 源码
  • Go Generics Tutorial (opens new window)
  • Robert Griesemer & Ian Lance Taylor: Generics in Go (opens new window)
  • slices 包文档 (opens new window) | maps 包文档 (opens new window) | cmp 包文档 (opens new window)
上次更新: 2026/06/14, 15:49:50
IO和文件
工程化与模块

← IO和文件 工程化与模块→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式