第 3 章:组织 — 当一个变成一千个
从一段让你警觉的代码开始
下面这段代码,8 门语言都能写,且看起来都差不多:
有一批订单,找出所有已支付且金额超过 100 的,按金额排序,取前 5 个。
你在 Python 里可能写成一行的链式调用。你在 Java 里可能用 Stream。你在 Go 里可能写一个 for 循环加手动过滤。
问题是:为什么 Go 不能像 Python 那样一行写完?这不是 "谁更现代" 的问题。这背后是语言对 "数据组织" 这件事完全不同的分工——哪些能力内置,哪些能力交给程序员,哪些交给生态。
3.1 语法基础:8 种语言如何容纳"很多个订单"
Python
# 列表:有序,可重复,最常用
orders: list[Order] = []
orders.append(Order("A1001", 99.0, True))
orders.append(Order("A1002", 150.0, False))
# 字典:按键查找
orders_by_id: dict[str, Order] = {}
orders_by_id["A1001"] = Order("A1001", 99.0, True)
# 集合:不重复
seen_ids: set[str] = {"A1001", "A1002"}
# 不可变序列
from typing import Sequence
tags: Sequence[str] = ("urgent", "vip") # 元组是不可变序列
# 推导式——Python 最标志性的容器语法
paid_ids = [o.order_id for o in orders if o.paid]
amounts_by_id = {o.order_id: o.amount for o in orders}
unique_amounts = {o.amount for o in orders} # 集合推导式JavaScript / TypeScript
// 数组:JavaScript 的主力容器
const orders: Order[] = [];
orders.push(new Order("A1001", 99.0, true));
// 对象字面量当字典用
const ordersById: Record<string, Order> = {};
ordersById["A1001"] = new Order("A1001", 99.0, true);
// Map:真正的字典(键可以是任意类型)
const ordersMap = new Map<string, Order>();
ordersMap.set("A1001", new Order("A1001", 99.0, true));
// Set
const seenIds = new Set<string>(["A1001", "A1002"]);
// 函数式数组方法
const paidIds = orders.filter(o => o.paid).map(o => o.orderId);
const total = orders.reduce((sum, o) => sum + o.amount, 0);Java
// List:最常用的有序容器
List<Order> orders = new ArrayList<>();
orders.add(new Order("A1001", 99.0, true, null));
// 不可变列表(Java 9+)
List<Order> ordersImmutable = List.of(
new Order("A1001", 99.0, true, null),
new Order("A1002", 150.0, false, null)
);
// Map
Map<String, Order> ordersById = new HashMap<>();
ordersById.put("A1001", new Order("A1001", 99.0, true, null));
// Stream:Java 的函数式容器操作
List<String> paidIds = orders.stream()
.filter(Order::paid)
.map(Order::orderId)
.toList();
Map<String, Double> amountsById = orders.stream()
.collect(Collectors.toMap(Order::orderId, Order::amount));C++
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
// vector:最常用的动态数组
std::vector<Order> orders;
orders.push_back(Order("A1001", 99.0, true));
orders.emplace_back("A1002", 150.0, false); // 原地构造
// unordered_map:哈希表
std::unordered_map<std::string, Order> orders_by_id;
orders_by_id["A1001"] = Order("A1001", 99.0, true);
// set
std::unordered_set<std::string> seen_ids;
// algorithm + lambda:C++ 的函数式操作
std::vector<Order> paid_orders;
std::copy_if(orders.begin(), orders.end(), std::back_inserter(paid_orders),
[](const Order& o) { return o.paid; });
std::sort(paid_orders.begin(), paid_orders.end(),
[](const Order& a, const Order& b) { return a.amount > b.amount; });Rust
// Vec:Rust 的动态数组
let mut orders: Vec<Order> = Vec::new();
orders.push(Order::new("A1001".into(), 99.0, true, None).unwrap());
// 用 vec! 宏创建
let orders = vec![
Order::new("A1001".into(), 99.0, true, None).unwrap(),
Order::new("A1002".into(), 150.0, false, None).unwrap(),
];
// HashMap
use std::collections::HashMap;
let mut orders_by_id: HashMap<String, Order> = HashMap::new();
orders_by_id.insert("A1001".into(), orders[0].clone());
// HashSet
use std::collections::HashSet;
let seen_ids: HashSet<&str> = ["A1001", "A1002"].iter().cloned().collect();
// 迭代器链:Rust 的函数式操作
let paid_ids: Vec<&str> = orders.iter()
.filter(|o| o.paid)
.map(|o| o.order_id.as_str())
.collect();Go
// slice:Go 最核心的容器
var orders []Order
orders = append(orders, Order{OrderID: "A1001", Amount: 99.0, Paid: true})
// 带初始容量的 slice
orders := make([]Order, 0, 100) // len=0, cap=100
// map
ordersByID := make(map[string]Order)
ordersByID["A1001"] = Order{OrderID: "A1001", Amount: 99.0, Paid: true}
// Go 没有 Set 类型,用 map[T]bool 或 map[T]struct{} 模拟
seenIDs := make(map[string]bool)
seenIDs["A1001"] = true
// for 循环 + 条件:Go 风格的 "过滤和映射"
var paidIDs []string
for _, o := range orders {
if o.Paid {
paidIDs = append(paidIDs, o.OrderID)
}
}Swift
// Array
var orders: [Order] = []
orders.append(Order(orderId: "A1001", amount: 99.0, paid: true))
// Dictionary
var ordersByID: [String: Order] = [:]
ordersByID["A1001"] = Order(orderId: "A1001", amount: 99.0, paid: true)
// Set
var seenIDs: Set<String> = ["A1001", "A1002"]
// 函数式链式操作
let paidIDs = orders
.filter { $0.paid }
.map { $0.orderId }
let grouped = Dictionary(grouping: orders, by: { $0.paid })Kotlin
// List
val orders = mutableListOf<Order>()
orders.add(Order("A1001", 99.0, true))
// 不可变 List(推荐)
val ordersImmutable = listOf(
Order("A1001", 99.0, true),
Order("A1002", 150.0, false)
)
// Map
val ordersByID = mapOf(
"A1001" to Order("A1001", 99.0, true)
)
// Set
val seenIDs = setOf("A1001", "A1002")
// 函数式链式操作
val paidIDs = orders
.filter { it.paid }
.map { it.orderId }
val amountsByID = orders.associate { it.orderId to it.amount }语法速查:核心容器
| 概念 | Python | TypeScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| 动态数组 | list | Array / [] | ArrayList / List.of | vector | Vec | slice | Array | List / MutableList |
| 字典 | dict | Map / Record | HashMap / Map.of | unordered_map | HashMap | map | Dictionary | Map / MutableMap |
| 集合 | set | Set | HashSet / Set.of | unordered_set | HashSet | 无(用 map[T]bool) | Set | Set |
| 不可变容器 | tuple, frozenset | readonly T[](编译期) | List.of(), Map.of() | const vector& | 默认不可变(通过借用) | 无(靠约定) | let 声明 | listOf(), mapOf() |
| 推导式/快速构建 | 推导式 [x for x in...] | map/filter 链 | Stream | <algorithm> + lambda | 迭代器链 | for 循环(没有快捷) | map/filter 链 | map/filter 链 |
3.2 深度对照一:要改原数据,还是生成新的?
同一个任务——"过滤出已支付的订单"——在 8 门语言里有两种哲学。
就地修改派(Mutate in place)
# Python
paid_orders = []
for o in orders:
if o.paid:
paid_orders.append(o)// Go
var paidOrders []Order
for _, o := range orders {
if o.Paid {
paidOrders = append(paidOrders, o)
}
}// Java:传统写法
List<Order> paidOrders = new ArrayList<>();
for (Order o : orders) {
if (o.paid()) paidOrders.add(o);
}这些写法都创建一个新容器,然后往里添加。
转换派(Transform)
# Python 推导式
paid_orders = [o for o in orders if o.paid]// Kotlin
val paidOrders = orders.filter { it.paid }// Rust
let paid_orders: Vec<&Order> = orders.iter().filter(|o| o.paid).collect();// JavaScript
const paidOrders = orders.filter(o => o.paid);这些写法描述"我想要什么",而不是"怎么一步步造出来"。
真正的分歧在哪里
不在于"有没有 filter 函数"。Java 也有 Stream filter,C++ 也有 std::copy_if。真正的分歧在:
就地修改容易产生"这个容器之后还能不能改"的意外,而转换模式天然倾向不可变数据流。
一门语言如果社区文化倾向于 "默认不可变"(Kotlin、Swift、Rust),它的集合操作就会自然地返回新容器而不是修改原容器。一门语言如果文化容忍甚至鼓励就地修改(Go、C++),它的代码风格就会在 "我现在改的是什么" 这件事上更容易出现误解。
3.3 深度对照二:遍历的方式,就是语言的"记忆负担"
方式一:下标循环(最底层,需要你管理索引)
# Python(不推荐)
for i in range(len(orders)):
print(orders[i].order_id)// Go(标准风格之一)
for i := 0; i < len(orders); i++ {
fmt.Println(orders[i].OrderID)
}// Java(不推荐用于遍历,但有时需要下标)
for (int i = 0; i < orders.size(); i++) {
System.out.println(orders.get(i).orderId());
}下标循环是最原始的形式。你需要管理一个索引变量,你需要在循环体内用索引访问,你需要在脑子里同时持有 "循环到第几个了" 和 "当前元素是什么" 两件事。这是最重的认知负担。
方式二:for-each(最普遍的范式,你只关心"当前元素")
for order in orders:
print(order.order_id)for (const order of orders) {
console.log(order.orderId);
}for (var order : orders) {
System.out.println(order.orderId());
}for (const auto& order : orders) {
std::cout << order.order_id << "\n";
}for order in &orders {
println!("{}", order.order_id);
}for _, order := range orders {
fmt.Println(order.OrderID)
}for order in orders {
print(order.orderId)
}for (order in orders) {
println(order.orderId)
}几乎现代语言都支持 for-each(或称 range-based for)。
方式三:带索引的遍历
for i, order in enumerate(orders): # Python
print(f"{i}: {order.order_id}")for i, order := range orders { // Go
fmt.Printf("%d: %s\n", i, order.OrderID)
}for ((i, order) in orders.withIndex()) { // Kotlin
println("$i: ${order.orderId}")
}for (i, order) in orders.iter().enumerate() { // Rust
println!("{}: {}", i, order.order_id);
}for (i, order) in orders.enumerated() { // Swift
print("\(i): \(order.orderId)")
}关键观察:不是8门语言都提供相同的遍历抽象
Go 的 range 非常朴素——只有 range slice、range map、range channel。没有反向遍历、没有步长遍历、没有惰性过滤。你需要自己写。
Rust 的迭代器则是一个极其丰富的抽象层——.enumerate()、.skip(n)、.take(n)、.step_by(n)、.rev()、.chain() 可以堆叠组合,而且是零成本抽象(编译器会优化成和手写循环一样的机器码)。
Kotlin 的集合扩展函数可能是最多的之一——.filterIndexed()、.mapNotNull()、.associate()、.groupBy()、.partition()、.windowed()。当你需要的东西 Kotlin 标准库里有,它就是一行。当没有,你就回到 for 循环。
3.4 深度对照三:Go 为什么没有 filter/map/reduce
8 门语言中有 7 门提供了某种形式的函数式链式操作。Go 是唯一的例外。
这不是 Go 团队忘了。这是刻意的设计选择。
以下是 Rob Pike 在 2014 年的原话大意:
我们刻意不在 Go 标准库中加入 filter/map/reduce。不是因为它们没用,而是因为它们在 Go 里写起来不够自然——需要闭包和泛型的配合,而这两个东西我们在 Go 1.0 中刻意保留。
即便 Go 1.18 引入了泛型,标准库中仍然没有 filter/map。社区库(如 samber/lo)填补了空白:
// 使用 samber/lo 库
paidIDs := lo.Map(
lo.Filter(orders, func(o Order, _ int) bool { return o.Paid }),
func(o Order, _ int) string { return o.OrderID },
)但即便有了泛型和社区库,Go 风格的函数式代码仍然比 Kotlin、Rust、Python 啰嗦。这不是技术做不到,而是 Go 始终选择 "显式 for 循环" 作为默认——因为对 Go 的目标读者(大型团队中经验参差的工程师)来说,for 循环一眼就能看懂,没有魔法。
这个选择揭示了 Go 和 Kotlin/Rust/Swift 之间的根本分歧:一门语言应该鼓励表达力,还是应该限制表达力以换取一致性?
3.5 边界探索:跨语言容器陷阱
陷阱一:Python → Java:把"切片"当成理所当然
first_three = orders[:3] # Python:优雅// Java:没有切片语法,需要:
List<Order> firstThree = orders.subList(0, Math.min(3, orders.size()));
// 注意:subList 返回的是视图,不是副本!修改 subList 会影响原 list!subList 返回的是原 list 的视图,不是独立副本——这是一个在 Java 面试中反复被问到的陷阱。
陷阱二:JavaScript → Go:对象的"键"只能是字符串或 Symbol
// JavaScript
const m = new Map();
m.set(123, "value"); // 键可以是数字
m.set({id: 1}, "value"); // 键可以是对象// Go:map 的键必须是可以比较的类型(comparable)
m := make(map[int]string) // 键是 int,可以
// m := make(map[Order]string) // 如果 Order 包含 slice/map/函数字段,编译错误Go 的 map 键必须支持 == 比较。包含 slice、map、function 字段的 struct 不能当 map key。
陷阱三:Swift → Python:值类型 vs 引用类型的容器行为
// Swift:Array 是值类型
var a = [1, 2, 3]
var b = a // 复制!b 是独立副本
b.append(4)
print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]# Python:list 是引用类型
a = [1, 2, 3]
b = a # b 指向同一个 list!
b.append(4)
print(a) # [1, 2, 3, 4] —— a 也被改了这可能是 Swift 和 Python 之间最容易踩的容器陷阱。Swift 的 Array 是值类型(赋值即复制,写时复制优化),而 Python 的 list 是引用类型(赋值共享同一份数据)。
陷阱四:Rust → 任何 GC 语言:collect() 不是 "收集结果",是 "分配新内存"
let paid: Vec<&Order> = orders.iter().filter(|o| o.paid).collect();Rust 程序员对 collect() 有身体记忆——它分配新内存。在 GC 语言里你不怎么想内存从哪来,但在 Rust 里,每一次 collect() 都是一次显式的堆分配。这让 Rust 程序员天然地更注意容器的分配成本。
3.6 洞察:容器不是"装东西的",它是"关系的可见化"
如果你只用一门语言操作集合,你不会发现:
你选的容器类型,是在替你向未来的读者承诺 "你可以怎么访问这些数据"。
- 你选 List/Array:你在说 "顺序重要"
- 你选 Map/Dict:你在说 "按键快速查找重要"
- 你选 Set:你在说 "唯一性重要"
- 你选不可变容器:你在说 "这份关系整理好后,不希望有人偷偷改掉"
但当你在 8 门语言之间切换时,你还会发现另一层东西:
同样的容器语义,在不同语言里承受着完全不同的认知代价。
Python 的 [o for o in orders if o.paid] 和 Go 的五行 for 循环做的是同一件事。区别不在于结果,而在于:阅读 Python 版本的人花 0.5 秒,阅读 Go 版本的人花 5 秒。这 4.5 秒的差,乘以代码库中有多少处这类操作,乘以团队有多少人,乘以要维护多少年——就是语言选择带来的隐性工程成本。
当然,反过来说也成立: Go 的五行 for 循环,任何一个会写 Go 的初级工程师一眼能看懂。Python 的推导式,嵌套两层之后可读性迅速下降。这不是说哪个更好——而是说,表达力和理解成本之间,永远有一个平衡。
本章小结
容器不是桶,是关系的形式化。 选 List 还是 Map 还是 Set,你在选择 "这段数据关系以什么方式被其他人检索"。
遍历的语法差异,是认知负担的差异。 下标循环 > for-each > 函数式链——从左到右,每升一级,你少在脑子里记一件事。
Go 没有 filter/map 不是因为落后。 是因为它对 "可读性" 和 "表达力" 的天平,往另一端压了更多筹码。
不可变容器和可变容器不只是性能差异。 当你在 Kotlin 里用
listOf(),你在给未来的维护者发信号:"这个集合形成后就不会改了,你可以放心读。" 当你在 Go 里用[]Order,这个信号不存在。
如果你只用一门语言,你不会学到这个:
最能塑造你编程习惯的,不是你会用的容器类型,而是你用不上的那些。