标准库与泛型
# 第 16 章 标准库与泛型
Go 1.18 引入泛型,1.21 引入
slices/maps/cmp三大标准库——Go 终于"现代化"了。 关键词:slices/maps/cmp、泛型语法、类型约束、comparable、~T、cmp.Ordered
# 目录介绍
- 16.1 本章学习目标
- 16.2 高频标准库速览
- 16.3
slices包(Go 1.21+) - 16.4
maps包(Go 1.21+) - 16.5
cmp包:通用比较 - 16.6 泛型语法
- 16.7 泛型类型 vs 泛型函数
- 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
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("超时")
}
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)
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))
})
}
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) // 错误
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)
}
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 行。
思考题:
strings.Builder和+拼字符串有什么区别?什么时候该用 Builder?json.Unmarshalvsjson.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
})
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
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 容量
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])
}
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 行核心逻辑。
思考题:
slices.Delete删除元素后——原底层数组的内存会释放吗?被删除位置的旧值还能被 GC 吗?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
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}
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))]
}
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 循环收集 + 排序的代码,变成了两行链式调用。
思考题:
maps.Keys返回的切片——每次调用都分配新切片吗?连续两次调用的结果顺序一致吗?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
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", // 硬编码默认值
)
2
3
4
5
6
7
8
9
10
11
12
13
思考题:
cmp.Compare(a, b)和a - b(int 减法)有什么区别?为什么 int 减法不是安全的比较?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"})
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())
}
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
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、函数——这些不能用 == 比较
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)) // ✅
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
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](顺序不定)
}
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 的标准模式——不占额外内存。
思考题:
comparable约束允许 struct——但如果 struct 里有 slice 字段,能通过编译吗?~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
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
}
}
}
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
思考题:
- 泛型类型的每个类型参数实例化后——是独立的类型吗?
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
}
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 的合法 keycontainer/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 思考题
slices的设计哲学:slices.Sort只接受cmp.Ordered类型——为什么不让 Sort 接受一个 comparator 参数(像 SortFunc)作为默认?哪个更好?泛型 vs 代码生成:Java 的泛型是"类型擦除"(编译后只有一套代码),C++ 模板是"代码膨胀"(每种类型生成一套)。Go 的泛型用 GC shape stenciling——和两者有什么不同?
comparable的边界:struct{ a int; b []int }包含切片字段——能用comparable约束吗?为什么?~T的陷阱:~int约束下——自定义类型type Celsius int和一个函数func F[T ~int](a, b T) T { return a + b }——调用F(Celsius(0), Celsius(100))返回什么类型?何时不泛型:你已经写了一个
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
}
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)