Skip to content

第 3 章:组织 — 当一个变成一千个

从一段让你警觉的代码开始

下面这段代码,8 门语言都能写,且看起来都差不多:

有一批订单,找出所有已支付且金额超过 100 的,按金额排序,取前 5 个。

你在 Python 里可能写成一行的链式调用。你在 Java 里可能用 Stream。你在 Go 里可能写一个 for 循环加手动过滤。

问题是:为什么 Go 不能像 Python 那样一行写完?这不是 "谁更现代" 的问题。这背后是语言对 "数据组织" 这件事完全不同的分工——哪些能力内置,哪些能力交给程序员,哪些交给生态。


3.1 语法基础:8 种语言如何容纳"很多个订单"

Python

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

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

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++

cpp
#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

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

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

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

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 }

语法速查:核心容器

概念PythonTypeScriptJavaC++RustGoSwiftKotlin
动态数组listArray / []ArrayList / List.ofvectorVecsliceArrayList / MutableList
字典dictMap / RecordHashMap / Map.ofunordered_mapHashMapmapDictionaryMap / MutableMap
集合setSetHashSet / Set.ofunordered_setHashSet无(用 map[T]boolSetSet
不可变容器tuple, frozensetreadonly T[](编译期)List.of(), Map.of()const vector&默认不可变(通过借用)无(靠约定)let 声明listOf(), mapOf()
推导式/快速构建推导式 [x for x in...]map/filterStream<algorithm> + lambda迭代器链for 循环(没有快捷)map/filtermap/filter

3.2 深度对照一:要改原数据,还是生成新的?

同一个任务——"过滤出已支付的订单"——在 8 门语言里有两种哲学。

就地修改派(Mutate in place)

python
# Python
paid_orders = []
for o in orders:
    if o.paid:
        paid_orders.append(o)
go
// Go
var paidOrders []Order
for _, o := range orders {
    if o.Paid {
        paidOrders = append(paidOrders, o)
    }
}
java
// Java:传统写法
List<Order> paidOrders = new ArrayList<>();
for (Order o : orders) {
    if (o.paid()) paidOrders.add(o);
}

这些写法都创建一个新容器,然后往里添加。

转换派(Transform)

python
# Python 推导式
paid_orders = [o for o in orders if o.paid]
kotlin
// Kotlin
val paidOrders = orders.filter { it.paid }
rust
// Rust
let paid_orders: Vec<&Order> = orders.iter().filter(|o| o.paid).collect();
javascript
// JavaScript
const paidOrders = orders.filter(o => o.paid);

这些写法描述"我想要什么",而不是"怎么一步步造出来"。

真正的分歧在哪里

不在于"有没有 filter 函数"。Java 也有 Stream filter,C++ 也有 std::copy_if。真正的分歧在:

就地修改容易产生"这个容器之后还能不能改"的意外,而转换模式天然倾向不可变数据流。

一门语言如果社区文化倾向于 "默认不可变"(Kotlin、Swift、Rust),它的集合操作就会自然地返回新容器而不是修改原容器。一门语言如果文化容忍甚至鼓励就地修改(Go、C++),它的代码风格就会在 "我现在改的是什么" 这件事上更容易出现误解。


3.3 深度对照二:遍历的方式,就是语言的"记忆负担"

方式一:下标循环(最底层,需要你管理索引)

python
# Python(不推荐)
for i in range(len(orders)):
    print(orders[i].order_id)
go
// Go(标准风格之一)
for i := 0; i < len(orders); i++ {
    fmt.Println(orders[i].OrderID)
}
java
// Java(不推荐用于遍历,但有时需要下标)
for (int i = 0; i < orders.size(); i++) {
    System.out.println(orders.get(i).orderId());
}

下标循环是最原始的形式。你需要管理一个索引变量,你需要在循环体内用索引访问,你需要在脑子里同时持有 "循环到第几个了" 和 "当前元素是什么" 两件事。这是最重的认知负担。

方式二:for-each(最普遍的范式,你只关心"当前元素")

python
for order in orders:
    print(order.order_id)
javascript
for (const order of orders) {
    console.log(order.orderId);
}
java
for (var order : orders) {
    System.out.println(order.orderId());
}
cpp
for (const auto& order : orders) {
    std::cout << order.order_id << "\n";
}
rust
for order in &orders {
    println!("{}", order.order_id);
}
go
for _, order := range orders {
    fmt.Println(order.OrderID)
}
swift
for order in orders {
    print(order.orderId)
}
kotlin
for (order in orders) {
    println(order.orderId)
}

几乎现代语言都支持 for-each(或称 range-based for)。

方式三:带索引的遍历

python
for i, order in enumerate(orders):     # Python
    print(f"{i}: {order.order_id}")
go
for i, order := range orders {         // Go
    fmt.Printf("%d: %s\n", i, order.OrderID)
}
kotlin
for ((i, order) in orders.withIndex()) { // Kotlin
    println("$i: ${order.orderId}")
}
rust
for (i, order) in orders.iter().enumerate() { // Rust
    println!("{}: {}", i, order.order_id);
}
swift
for (i, order) in orders.enumerated() {  // Swift
    print("\(i): \(order.orderId)")
}

关键观察:不是8门语言都提供相同的遍历抽象

Go 的 range 非常朴素——只有 range slicerange maprange 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)填补了空白:

go
// 使用 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:把"切片"当成理所当然

python
first_three = orders[:3]  # Python:优雅
java
// Java:没有切片语法,需要:
List<Order> firstThree = orders.subList(0, Math.min(3, orders.size()));
// 注意:subList 返回的是视图,不是副本!修改 subList 会影响原 list!

subList 返回的是原 list 的视图,不是独立副本——这是一个在 Java 面试中反复被问到的陷阱。

陷阱二:JavaScript → Go:对象的"键"只能是字符串或 Symbol

javascript
// JavaScript
const m = new Map();
m.set(123, "value");       // 键可以是数字
m.set({id: 1}, "value");   // 键可以是对象
go
// 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
// 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
# 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() 不是 "收集结果",是 "分配新内存"

rust
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,这个信号不存在。


如果你只用一门语言,你不会学到这个:

最能塑造你编程习惯的,不是你会用的容器类型,而是你用不上的那些。