一、打孔卡:当编程意味着体力劳动
纸上的0和1
在计算机科学的黎明时期,"编程"这个词的意思和今天完全不同。它不是坐在舒适的椅子上敲键盘,而是站在一台打孔机前,用物理的方式在纸板卡片上打洞。
打孔卡(punch card)的历史比计算机本身还要悠久。1801年,法国织布工约瑟夫·雅卡尔(Joseph Jacquard)发明了一种用穿孔卡片控制织布机图案的系统。每一张卡片上的孔洞模式对应织布机的一种编织动作。通过更换卡片序列,同一台织布机可以织出不同的花纹。
这个看似简单的发明蕴含着一个深刻的洞察:指令可以被外部化为物理形式,并按照顺序自动执行。这正是"程序"的本质。
雅卡尔的发明在当时引发了巨大的社会动荡。法国里昂的丝绸工人们担心自动化会夺走他们的工作,多次暴动捣毁织布机。这段历史与两百年后人们对人工智能取代工作的恐惧如出一辙。技术进步的车轮从未停歇,而人类对它的焦虑也从未改变。
IBM与打孔卡帝国
到了20世纪中期,打孔卡成了数据处理的通用媒介。IBM(国际商业机器公司)围绕打孔卡建立了一个庞大的商业帝国。IBM的打孔卡有80列,每列有12个可能的打孔位置。一张卡片可以存储80个字符--大约相当于一条推文的长度。
IBM的创始人托马斯·沃森(Thomas Watson)有一句著名的格言:"全世界只需要大约五台计算机。"这句话后来被证明是历史上最糟糕的技术预测之一,但它反映了当时人们对计算机的理解--它们是庞然大物,只有最大的机构才需要。
程序员的工作流程是这样的:
- 在纸上写出程序的逻辑
- 用铅笔在编码纸上填写每一行指令
- 把编码纸交给打孔员,打孔员把指令翻译成打孔卡上的孔洞
- 把一叠打孔卡交给操作员,操作员把它们喂进计算机
- 等待几小时甚至几天,拿到打印出来的结果
- 发现结果不对,检查是哪张卡片打错了,重新来过
"在打孔卡时代,调试程序意味着在几千张卡片中找到打错孔的那一张。"--佚名程序员
这种工作方式有一个意想不到的好处:它迫使你在写代码之前想清楚所有逻辑。因为你没有机会快速迭代--每一次修改都需要重新打孔,可能意味着半天的延迟。所以你必须在脑子里把程序完整地运行一遍,确保没有明显的错误,才敢把卡片送去打孔。
这种"一次写对"的文化,在后来的高级语言时代几乎完全消失了。今天,一个程序员可以写一行代码、运行、看到错误、修改、再运行--整个循环只需要几秒钟。这种快速迭代的能力极大地提高了生产力,但也导致了一种"先写再改"的编程风格,有时候会让代码的质量下降。
打孔卡时代还留下了一个有趣的文化遗产:FORTRAN语言的语法限制。早期的FORTRAN严格依赖打孔卡的80列格式,每一列都有特定的含义--第1-5列是行号,第6列是续行标记,第7-72列是代码。这种格式限制影响了几代程序员的编码习惯,甚至今天一些老程序员仍然习惯性地把代码限制在72列以内。
二、汇编语言:给机器一个昵称
从机器码到助记符
计算机的CPU只认识一种语言--机器码,也就是一串串0和1的二进制数字。比如,要把一个数字加到寄存器里,你可能需要写这样的东西:
10111000 00000101 00000000
这意味着什么?对人类来说,这完全无法理解。但对CPU来说,这就是一条清晰的指令:"把数字5放到AX寄存器里。"
1940年代末,程序员们开始为这些二进制数字起昵称。他们把10111000叫做MOV(移动),把00000011叫做ADD(加法)。这就是汇编语言的诞生。
MOV AX, 5
ADD AX, 3
; 现在AX寄存器里的值是8
汇编语言并没有改变计算机的工作方式--它仍然需要被翻译成机器码才能执行。但它让程序员可以用人类可读的符号来编写指令,然后再用一个叫"汇编器"(assembler)的工具把这些符号翻译回机器码。
汇编的哲学:你就是机器
汇编语言的核心哲学是一一对应--每一条汇编指令都直接对应一条机器指令。你写的每一个符号,都有一个明确的、物理的含义。
这种透明性是一把双刃剑。好处是你完全知道计算机在做什么--没有隐藏的行为,没有意外的副作用。坏处是你必须关心计算机的每一个细节--哪个寄存器存了什么值、内存地址是多少、栈指针指向哪里。
一个经典的汇编趣事发生在1960年代的MIT。黑客文化(hacker culture)的先驱们在PDP-1小型机上编写了第一个电子游戏《太空大战》(Spacewar!)。整个游戏用汇编语言写成,代码量约9KB--以今天的标准来看微不足道,但在当时,这是一群天才程序员花了数百小时的心血结晶。
汇编语言的美妙之处在于它的诚实--它不会对你撒谎,也不会帮你隐瞒任何东西。你写的每一行代码,都会被原封不动地执行。
今天仍然有人用汇编语言编程。在嵌入式系统、操作系统内核、驱动程序、以及对性能有极端要求的场景(如游戏引擎的热路径),汇编仍然是不可替代的。Linux内核中大约有2%的代码是汇编。
但对绝大多数程序员来说,汇编语言就像手动挡汽车--你理解它的原理很好,但日常工作中你更愿意用自动挡。
不同架构的汇编差异
值得一提的是,汇编语言并不是一种统一的语言--它与具体的处理器架构绑定。x86架构的汇编和ARM架构的汇编完全不同:
| 特性 | x86汇编 | ARM汇编 |
|---|---|---|
| 寄存器数量 | 较少(通用寄存器约8个) | 较多(通用寄存器16个) |
| 指令长度 | 可变长度(1-15字节) | 固定长度(4字节) |
| 设计哲学 | CISC(复杂指令集) | RISC(精简指令集) |
| 代表应用 | 桌面PC、服务器 | 手机、嵌入式设备 |
这意味着一个精通x86汇编的程序员并不能直接读懂ARM汇编--他们需要重新学习一套完全不同的指令集。这也是为什么高级编程语言如此重要:它们屏蔽了底层硬件的差异,让程序员可以用同一套代码在不同的处理器上运行。
三、Fortran:科学计算的解放者
约翰·巴库斯的赌注
1954年,IBM的程序员约翰·巴库斯(John Backus)向管理层提出了一个大胆的建议:创造一种用接近数学公式的语言来编写程序的系统。当时,绝大多数人认为这是不可能的--计算机怎么能理解数学公式?
IBM的管理层给了巴库斯一个团队和几年时间。1957年,Fortran(Formula Translation,公式翻译)诞生了。
巴库斯本人是一个极具传奇色彩的人物。年轻时他在哥伦比亚大学学习化学,但因为成绩太差被开除。后来他发现自己真正的天赋在数学和逻辑上,于是加入了IBM。他后来回忆说,发明Fortran的动机部分来自他对手工编写汇编代码的厌恶--他觉得这种重复性的劳动应该由机器来完成。
Fortran的第一个杀手级应用是科学计算。在此之前,科学家们想要用计算机做数值计算,必须自己手写汇编代码--这通常需要几个月的时间。Fortran让他们可以用这样的代码来表达数学公式:
PROGRAM HELLO
IMPLICIT NONE
REAL :: x, y, result
x = 3.14159
y = 2.71828
result = SIN(x) + COS(y)
PRINT *, 'Result:', result
END PROGRAM HELLO
这段代码的可读性在今天看来仍然很好。一个没有学过Fortran的数学家也能大致理解它在做什么。
Fortran的性能遗产
Fortran的一个惊人事实是:在某些数值计算领域,Fortran编译器生成的代码在今天仍然比C编译器更快。
原因在于Fortran的语言设计。Fortran从一开始就禁止了指针别名(pointer aliasing)--编译器可以假定两个不同的数组不会重叠在内存中。这个假设让编译器可以进行更激进的优化。
C语言没有这个限制--两个指针可能指向同一块内存。这意味着C编译器在优化数组操作时必须更加保守,因为它不能确定修改一个数组不会影响另一个。
"Fortran是这样一种语言:它告诉你'你不能做这些事情',但作为交换,它承诺'你让它做的事情,它会做得非常快'。"
Fortran至今仍在使用。NASA、核物理实验室、气象预报系统--这些对数值精度和计算速度有极端要求的领域,仍然大量依赖Fortran代码。据估计,全球仍在运行的Fortran代码有数百亿行。2004年,NASA的气候模拟系统完全用Fortran编写,运行在超级计算机上,模拟地球大气层的每一个网格点--这种规模的计算对性能的要求是极其苛刻的,而Fortran的编译器优化在这种场景下展现出了无可比拟的优势。
Fortran制造的新问题
Fortran解决了"如何让科学家更容易地编程"的问题,但它也制造了新问题:
- 缺乏结构化控制流:早期的Fortran大量使用
GOTO语句,导致代码像一碗面条一样难以理解 - 没有内存管理概念:变量的生命周期完全由编译器决定,程序员无法控制
- 类型系统很弱:你可以把一个整数赋给一个字符串变量,编译器不会抱怨
这些问题将在后来的语言中被逐一解决,但每解决一个问题,又会带来新的复杂性。
1968年,荷兰计算机科学家艾兹格·戴克斯特拉(Edsger Dijkstra)发表了一封著名的信--《GOTO语句被认为是有害的》(Go To Statement Considered Harmful)。这封信直接推动了"结构化编程"运动,也间接催生了后来的Pascal、C等语言的设计理念。戴克斯特拉的观点是:GOTO语句让程序的控制流变得不可追踪,是软件质量的最大敌人。
四、C语言:系统编程的瑞士军刀
贝尔实验室的副产品
1969年,贝尔实验室的肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)开始编写一个叫Unix的操作系统。他们最初用汇编语言来写,但很快发现汇编太痛苦了--每次换一台新电脑,就得重写所有代码。
他们需要一种语言,既有汇编语言的底层控制能力,又有高级语言的可移植性。于是C语言诞生了。
#include <stdio.h>
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += numbers[i];
}
printf("Sum: %d\n", sum);
return 0;
}
C语言的设计哲学可以用一句话概括:信任程序员。
它给你指针、给你直接的内存访问、给你位操作--它假设你知道自己在做什么。如果你写出了一段越界访问内存的代码,C语言不会阻止你。它会在编译时安静地接受你的代码,在运行时默默地执行,然后在某个不可预测的时刻崩溃。
丹尼斯·里奇曾这样描述C语言的设计理念:"C语言是一种'信任你'的语言。它给你所有的自由,也给你所有的责任。"这种哲学在当时是革命性的--之前的高级语言(如Pascal)倾向于限制程序员的行为来防止错误,而C选择了相反的方向。
C语言的另一个深远影响是它的语法范式。C语言引入的花括号{}代码块、if-else条件语句、for和while循环、switch-case分支--这些语法结构被后来的几十种语言直接继承。当你写Java、JavaScript、Go、Rust、Swift时,你实际上在使用C语言在1970年代奠定的语法框架。这种影响力之广泛,在编程语言史上只有极少数语言能够匹敌。
C的指针:强大而危险
指针是C语言最核心的概念,也是它最危险的特性。
int x = 42;
int *p = &x; // p指向x的地址
*p = 100; // 通过指针修改x的值
// 现在x == 100
指针让C程序员可以精确地控制内存布局--这在操作系统和嵌入式系统中是不可或缺的。但指针也是一把双刃剑:
- 空指针解引用:试图访问地址为0的内存 → 程序崩溃
- 悬垂指针:指向已经被释放的内存 → 不可预测的行为
- 缓冲区溢出:写入超过数组边界的内存 → 安全漏洞
- 内存泄漏:分配了内存但忘记释放 → 程序越来越慢
"C语言给了你足够的绳子来上吊。"--这句话被归因于多位程序员
缓冲区溢出的历史上最臭名昭著的案例之一是1988年的莫里斯蠕虫(Morris Worm)。这是互联网历史上第一个大规模的蠕虫病毒,它利用了Unix系统中fingerd程序的一个缓冲区溢出漏洞。这个漏洞本质上就是C代码没有正确检查输入长度--一个典型的C语言安全问题。莫里斯蠕虫感染了当时互联网上约10%的计算机,直接催生了CERT(计算机紧急响应小组)的成立。
C的遗产
C语言的影响无处不在。以下这些语言都直接或间接受到C的影响:
- C++:C的超集,添加了面向对象特性
- Java:语法借鉴C,但去掉了指针
- JavaScript:语法借鉴Java(间接来自C),但语义完全不同
- Go:由C的设计者之一罗布·派克(Rob Pike)参与设计
- Rust:试图在保持C的性能的同时解决C的安全问题
操作系统领域仍然是C的天下。Linux内核主要是C写的。Windows内核主要是C写的。macOS和iOS的底层也是C写的。C语言在50多年后仍然没有被取代--这在技术领域是极其罕见的。
值得一提的是,C语言的标准化过程也充满了戏剧性。1989年,ANSI发布了C89标准。1999年,C99标准发布,添加了变长数组、long long类型等特性。但C语言的标准化一直落后于实际使用--很多编译器厂商(特别是微软)对新标准的支持非常缓慢,导致程序员经常需要面对不同编译器之间的兼容性问题。这种"标准与实现之间的鸿沟"是C语言社区长期存在的痛点。
五、C++:复杂性的军备竞赛
本贾尼的野心
1979年,贝尔实验室的本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)开始设计一种新语言,他最初叫它"C with Classes"(带类的C)。他的目标是在不牺牲C的性能的前提下,添加对大型软件项目的支持。
C++在1985年正式发布。它在C的基础上添加了:
- 类和对象:面向对象编程的基础
- 继承和多态:代码复用和接口抽象
- 模板:泛型编程
- 异常处理:错误处理的结构化方式
- 命名空间:避免名字冲突
- 标准模板库(STL):通用数据结构和算法
#include <iostream>
#include <vector>
#include <algorithm>
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
for (auto& animal : animals) {
animal->speak();
}
return 0;
}
复杂性的深渊
C++的问题在于它太强大了。它支持多种编程范式--面向对象、泛型、函数式、过程式--而且这些范式可以自由组合。结果是,同一个问题可以用几十种不同的方式来解决,而且每种方式都有自己的陷阱。
C++标准委员会在语言的演进过程中不断添加新特性。C++11添加了lambda表达式、智能指针、移动语义。C++14、C++17、C++20继续添加更多特性。到C++23,这个语言已经有了数百个关键字和数千条规则。
"在C++中,有太多的方式可以射击自己的脚。"--本贾尼·斯特劳斯特卢普自己
一个经典的例子是C++的"最令人困惑的解析"(most vexing parse):
// 这不是在创建一个Widget对象
// 这是在声明一个函数
Widget w(Widget());
上面这行代码看起来像是创建了一个Widget对象w,用另一个Widget临时对象初始化。但实际上,C++编译器会把它解析为一个函数声明--一个名为w的函数,它接受一个返回Widget的函数作为参数,返回一个Widget。
这种歧义是C++复杂性的缩影。你写的代码看起来是一个意思,但编译器的理解完全不同--而且编译器是对的。
C++在游戏行业的统治
尽管C++的复杂性饱受批评,但它在游戏行业中几乎是不可替代的。虚幻引擎(Unreal Engine)--世界上使用最广泛的游戏引擎之一--完全用C++编写。游戏行业选择C++的原因很明确:性能。
一个现代3A游戏需要在每秒内渲染60帧甚至更多帧画面。每一帧都需要处理数百万个多边形的变换、光照计算、物理模拟、AI决策。在这种场景下,垃圾回收带来的任何暂停都是不可接受的。C++的手动内存管理和零开销抽象让它成为游戏开发的首选。
"游戏开发者不怕C++的复杂性--他们怕的是60fps的目标被打断哪怕一毫秒。"
六、Java:一次编写,到处运行
失败的开始
Java的故事始于一次失败。1991年,Sun Microsystems的詹姆斯·高斯林(James Gosling)开始设计一种叫Oak的语言,目标是用在交互式电视的机顶盒上。但这个市场没有发展起来,Oak项目差点被砍掉。
转机出现在1995年--互联网开始爆发。Sun的团队意识到,Oak的"跨平台"特性正好是互联网所需要的。他们把Oak改名为Java(据说是因为团队成员喜欢喝Java咖啡),并把它定位为Web应用的编程语言。
Java的核心卖点是"Write Once, Run Anywhere"(一次编写,到处运行)。它通过在操作系统和程序之间插入一个叫Java虚拟机(JVM)的中间层来实现这一点。程序员写Java代码,编译成字节码(bytecode),然后由JVM在任何操作系统上执行。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
}
}
垃圾回收:解放与代价
Java最重大的设计决策之一是自动垃圾回收(Garbage Collection,GC)。
在C和C++中,程序员必须手动管理内存--分配内存、使用内存、释放内存。如果你忘了释放,就会内存泄漏。如果你释放了还在使用的内存,就会悬垂指针。这两种错误是C/C++程序中最常见也最难调试的bug。
Java说:"不要手动管理内存了,我来帮你做。"JVM会自动追踪哪些对象还在使用,哪些已经没有引用了,然后自动回收后者的内存。
这极大地降低了编程的心智负担。但它也有代价:
- GC暂停(GC pause):垃圾回收器在回收内存时会暂停程序的执行。对于延迟敏感的应用(如交易系统、游戏引擎),这是不可接受的
- 内存开销:GC需要额外的内存来追踪对象状态
- 不可预测性:你不知道GC什么时候会运行,这意味着你的程序在某个时刻可能会突然变慢
"Java让你永远不会忘记释放内存,因为它根本不让你释放内存。"
Java的垃圾回收器经历了多次重大升级。从最初的简单"标记-清除"算法,到分代收集器,再到G1(Garbage First)收集器,最后到ZGC和Shenandoah这些超低延迟收集器--每一次升级都是在尝试解决GC暂停时间过长的问题。ZGC的设计目标是将GC暂停时间控制在10毫秒以内,这对于大多数应用场景来说已经是不可感知的了。
Java的统治
Java在企业级软件开发中建立了近乎垄断的地位。原因包括:
- 跨平台:企业有各种操作系统,Java的"一次编写到处运行"解决了兼容性问题
- 生态系统:Spring、Hibernate、Maven、Gradle--Java的工具链极其丰富
- 人才市场:Java是全球大学教得最多的编程语言之一
- 稳定性:Java的向后兼容性非常好,10年前写的代码在今天的JVM上仍然能运行
但Java也因为它的冗长而被程序员们嘲笑。一个简单的"Hello World"在Java中需要写一个类、一个main方法、一个System.out.println调用。这种仪式感在大型项目中是有意义的,但在写小工具时就显得非常繁琐。
2017年,Java 9引入了模块系统(JPMS),试图解决大型项目中的依赖管理问题。但这个特性被广泛批评为过于复杂,很多开发者选择忽略它。这种"为了大型企业项目而设计,却让小项目变得更复杂"的矛盾,是Java社区长期的痛点。
Java在安卓开发领域的命运也值得一说。2008年,谷歌选择Java作为安卓应用开发的官方语言--这个决定让Java在移动时代继续保持了庞大的用户基数。但2017年,谷歌宣布Kotlin成为安卓开发的"一级语言",并在2019年进一步将其列为首选语言。Kotlin由JetBrains公司开发,它保留了Java的JVM生态优势,但语法更简洁、更安全(空指针安全、不可变性优先)。到2025年,大多数新的安卓项目都使用Kotlin而非Java。Java在移动端的统治地位正在被它自己的"后代"所取代。
七、Python:简洁的代价
Guido的圣诞节项目
1989年的圣诞节,荷兰程序员吉多·范罗苏姆(Guido van Rossum)觉得无聊,决定写一个新的脚本语言来打发时间。他以英国喜剧团体Monty Python命名了这个语言--Python。
Python的设计哲学写在它的"禅"(The Zen of Python)中:
import this
运行这段代码会输出19条设计原则,其中最重要的几条是:
- 优美胜于丑陋(Beautiful is better than ugly)
- 明了胜于晦涩(Explicit is better than implicit)
- 简洁胜于复杂(Simple is better than complex)
- 可读性很重要(Readability counts)
Python的代码风格和C++形成了鲜明的对比。同样一个"读取文件并统计行数"的功能:
with open('file.txt') as f:
line_count = sum(1 for line in f)
print(f'Lines: {line_count}')
// C++
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ifstream file("file.txt");
std::string line;
int count = 0;
while (std::getline(file, line)) {
count++;
}
std::cout << "Lines: " << count << std::endl;
return 0;
}
Python用了3行,C++用了10行。这种简洁性让Python成了初学者的首选语言和快速原型开发的利器。
动态类型:灵活还是混乱?
Python是动态类型语言--你不需要声明变量的类型,类型在运行时才确定。
x = 42 # x是整数
x = "hello" # 现在x是字符串了
x = [1, 2, 3] # 现在x是列表了
这种灵活性在小项目中非常方便。但在大型项目中,它会导致一类特殊的bug--类型错误要到运行时才会发现。
def add(a, b):
return a + b
result = add(3, 4) # 正常,返回7
result = add("3", 4) # 运行时报错:字符串和整数不能相加
result = add(3, "4") # 运行时报错
为了解决这个问题,Python 3.5引入了类型提示(type hints):
def add(a: int, b: int) -> int:
return a + b
但类型提示是可选的--Python解释器不会强制检查它们。这导致了一个有趣的局面:Python同时拥有动态类型和静态类型的能力,但两种风格的代码混在一起时,反而可能比纯粹的动态类型更让人困惑。
全局解释器锁(GIL):Python的阿喀琉斯之踵
Python有一个臭名昭著的设计决策--全局解释器锁(Global Interpreter Lock,GIL)。GIL确保在任何时刻,只有一个线程在执行Python字节码。
这意味着Python无法真正利用多核CPU进行并行计算。即使你的电脑有16个核心,Python的多线程程序也只能用一个。
GIL的存在有历史原因--它大大简化了CPython(Python的标准实现)的内存管理。但它也成了Python在高性能计算领域的天花板。
"GIL就像Python给自己戴的一副手铐。它让Python的实现更简单了,但也让Python的并行能力变简单了--简单到几乎不存在。"
2023年,Python指导委员会接受了PEP 703,计划在CPython中逐步移除GIL。这个过程可能需要数年时间,但它标志着Python社区终于决定直面这个历史遗留问题。
在GIL被移除之前,Python程序员通常用多进程(multiprocessing)来绕过多线程的限制。每个进程有自己的Python解释器和GIL,因此可以真正并行执行。但进程间的通信比线程间慢得多,内存也无法直接共享。这种"用多进程代替多线程"的方案是Python社区的一个独特的工程权衡。
八、JavaScript:互联网的通用语言
十天造就的语言
1995年5月,网景公司(Netscape)的布兰登·艾奇(Brendan Eich)花了十天时间设计了JavaScript。这个事实至今仍然让人难以置信--世界上使用最广泛的编程语言,是在十天内设计出来的。
JavaScript最初的目标很简单:让网页能够做一些交互效果--比如在你把鼠标移到按钮上时改变颜色。它被设计为一种"简单的小脚本语言",面向的是设计师和业余程序员,而不是专业的软件工程师。
但JavaScript有一个关键的宿命:它被内嵌在了每一个网页浏览器中。这意味着无论你喜不喜欢JavaScript,只要你想在浏览器中运行代码,你就必须用它。
布兰登·艾奇后来回忆说,十天的设计时间充满了政治压力。网景公司与Sun Microsystems达成了一项协议,新语言将被命名为"JavaScript"--借Java的名气来营销。但实际上JavaScript和Java几乎没有关系。这种命名上的误导困扰了开发者社区几十年。
从玩具到全栈
JavaScript在最初的十年里一直被专业程序员看不起--它被认为是"一种给业余爱好者用的玩具语言"。但2008年发生了两件改变历史的事:
- Google Chrome发布,内置了V8 JavaScript引擎。V8使用了即时编译(JIT)技术,把JavaScript的执行速度提高了10-100倍
- Node.js在2009年发布,让JavaScript可以在服务器端运行
Node.js的发布意味着JavaScript不再局限于浏览器。一个程序员可以用同一种语言编写前端和后端代码。这催生了"全栈JavaScript"的概念。
// Express.js - 一个简单的Web服务器
const express = require('express');
const app = express();
app.get('/api/users', (req, res) => {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
res.json(users);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
JavaScript的怪癖
JavaScript有很多让人困惑的设计决策。最著名的一个:
[] + [] // 结果是 ""
[] + {} // 结果是 "[object Object]"
{} + [] // 结果是 0
"b" + "a" + +"a" + "a" // 结果是 "baNaNa"
最后一个例子中,+"a"试图把字符串"a"转换为数字,结果是NaN(Not a Number)。所以"b" + "a" + NaN + "a"变成了"baNaNa"--没错,JavaScript在输出一个香蕉。
这些怪癖源于JavaScript的隐式类型转换机制。当运算符两边的类型不匹配时,JavaScript会自动尝试把它们转换为同一类型--但转换规则极其复杂,而且经常产生反直觉的结果。
"JavaScript是一门你不得不使用的语言,而不是一门你选择使用的语言。"--很多人
TypeScript的救赎
2012年,微软的安德斯·海尔斯伯格(Anders Hejlsberg,也是C#和Turbo Pascal的设计者)发布了TypeScript。TypeScript是JavaScript的超集--任何合法的JavaScript代码都是合法的TypeScript代码,但TypeScript添加了静态类型系统。
interface User {
id: number;
name: string;
email?: string; // 可选属性
}
function greet(user: User): string {
return `Hello, ${user.name}!`;
}
// 正确
greet({ id: 1, name: "Alice" });
// 编译时报错:缺少name属性
greet({ id: 2 });
TypeScript的出现让JavaScript生态系统变得更加健壮。到2025年,大多数新的JavaScript项目都使用TypeScript。GitHub上TypeScript的使用量已经超过JavaScript本身。
TypeScript的成功故事也揭示了一个重要的语言设计原则:渐进式采纳。TypeScript不要求你一次性把所有代码都改成类型安全的--你可以从一个纯JavaScript项目开始,逐步添加类型注解。这种"渐进式"策略大大降低了迁移成本,让TypeScript能够在不破坏现有生态的情况下快速增长。相比之下,Python 2到Python 3的迁移就因为不兼容性问题而拖延了十多年。安德斯·海尔斯伯格在设计TypeScript时明确吸取了这个教训--他把"与现有JavaScript代码的完美兼容"作为最高优先级的设计目标。
九、Go:少即是多
三个老程序员的反叛
2007年,Google的三位资深工程师--罗布·派克(Rob Pike)、肯·汤普森(Ken Thompson,Unix和C的共同创造者)和罗伯特·格里泽默(Robert Griesemer)--对当时主流编程语言的复杂性感到不满。
C++太复杂。Java太冗长。Python太慢。JavaScript太混乱。他们想要一种简单的、快速的、并发友好的语言。
Go语言就这样诞生了。它的设计哲学可以用一个词概括:简单。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d\n", id)
}(i)
}
wg.Wait()
}
Go语言的设计过程中有一个著名的故事。罗布·派克曾经在一个周末用Google的内部构建系统编译一个C++项目,等了45分钟才完成。他愤怒地站起来说:"这太荒谬了。我们需要一种编译速度快到你不需要等待的语言。"Go语言的编译速度后来成了它最大的卖点之一--一个大型Go项目通常只需要几秒钟就能编译完成。
Goroutine:并发编程的简化
Go语言最强大的特性是goroutine--一种极其轻量级的并发执行单元。
在传统语言中,创建一个线程通常需要分配1-8MB的栈空间。这意味着你最多只能同时运行几千个线程。但一个goroutine只需要2KB的初始栈空间,而且可以动态增长。你可以在一台普通的服务器上运行数十万个goroutine。
Go的并发模型基于C.A.R. Hoare的通信顺序进程(CSP)理论。核心思想是:不要通过共享内存来通信,而要通过通信来共享内存。
ch := make(chan string)
go func() {
ch <- "hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
Channel(通道)是goroutine之间通信的管道。发送方和接收方通过channel交换数据,而不需要共享变量和锁。这种模型大大减少了并发编程中最常见的bug--竞态条件(race condition)。
Go的争议:泛型缺失
Go语言长期拒绝添加泛型(generics)--这让很多程序员感到愤怒。没有泛型意味着你不能写出类型安全的通用数据结构。比如,如果你想写一个通用的"最大值"函数,在Go 1.18之前你只能写:
Go在云原生领域的崛起速度令人瞩目。Docker--容器技术的开创者--用Go编写。Kubernetes--容器编排的事实标准--也用Go编写。Terraform、Prometheus、Grafana--这些定义了现代基础设施的工具全部是Go的产物。Go语言在云计算领域的统治地位甚至催生了一个说法:"云原生就是Go原生"。这种生态优势反过来又吸引了更多开发者学习Go,形成了一个正向循环。
// 没有泛型:需要为每种类型写一个函数
func maxInt(a, b int) int {
if a > b { return a }
return b
}
func maxFloat64(a, b float64) float64 {
if a > b { return a }
return b
}
或者使用interface{}(空接口)和类型断言,但那样就失去了类型安全。
Go的核心团队坚持认为,泛型会增加语言的复杂性,违背了"简单"的设计哲学。这个争论持续了十多年,最终Go 1.18在2022年引入了泛型--但设计得非常克制,远没有C++或Java的模板/泛型那么强大。
十、Rust:安全与性能的终极平衡
Mozilla的实验
2006年,Mozilla的工程师格雷登·霍尔(Graydon Hoare)开始设计一种新的系统编程语言。他的目标是解决C/C++中最严重的问题--内存安全。
C/C++中的内存安全问题(缓冲区溢出、空指针解引用、悬垂指针、数据竞争)是计算机安全领域最大的噩梦。微软的安全工程师在2019年透露,微软产品中约70%的安全漏洞都是内存安全问题。谷歌的Chromium项目也报告了类似的比例--超过70%的严重安全漏洞是内存安全问题。
Rust的核心创新是所有权系统(ownership system)。它用一组编译时规则来保证内存安全,而不需要垃圾回收:
- 每个值都有一个所有者
- 同一时刻只能有一个所有者
- 当所有者离开作用域时,值会被自动释放
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1的所有权转移给了s2
// println!("{}", s1); // 编译错误!s1已经不再有效
println!("{}", s2); // 正常工作
}
借用检查器:编译时的守护者
Rust最具争议的特性是它的借用检查器(borrow checker)。它在编译时强制执行一套严格的规则:
- 你可以有任意数量的不可变引用(读取)
- 或者只有一个可变引用(写入)
- 但不能同时有两者
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0]; // 不可变借用
// data.push(4); // 编译错误!不能在借用期间修改
println!("{}", first);
// first的借用在这里结束
data.push(4); // 现在可以了
}
这个规则看起来很严格,但它消除了数据竞争--并发编程中最难调试的bug类型。数据竞争发生在两个线程同时访问同一块内存,且至少一个在写入。Rust在编译时就阻止了这种情况。
"与借用检查器搏斗"
Rust程序员有一个自嘲的说法:"你不是在写Rust,你是在和借用检查器搏斗。"确实,初学者经常会花大量时间来满足借用检查器的要求。
但随着经验的增长,你会发现借用检查器其实在教你一种更好的编程思维。它迫使你在写代码之前思考数据的生命周期和所有权关系--这些问题在C/C++中也会存在,只是编译器不会帮你检查。
"Rust编译器是你最严厉的老师,但它教你的每一课都是为了你好。"
Rust的学习曲线之陡峭在编程语言中是出了名的。一个有经验的C++程序员转学Rust,通常需要3-6个月才能真正"开窍"--理解借用检查器的逻辑,并在写代码时自然地遵循它的规则。很多初学者在这个过程中感到沮丧,甚至放弃。但那些坚持下来的人几乎无一例外地表示:Rust改变了他们对编程的理解,即使他们后来回到其他语言工作。
Rust的崛起
Rust从2015年1.0版本发布到2025年,用了十年时间从一个小众实验语言成长为系统编程领域的重要力量:
Rust在安全性方面的优势已经开始产生实际的经济效益。2024年,谷歌发布了一份内部报告,称将部分C++代码迁移到Rust后,内存安全漏洞减少了三分之二。美国白宫网络安全办公室也在2024年发表了一份备忘录,建议关键基础设施的软件开发应逐步从C/C++迁移到内存安全的语言--Rust是被提及最多的候选语言。这是历史上第一次,一个国家的政府层面对编程语言的选择发表正式建议。
- Linux内核从6.1版本开始正式支持Rust
- Android的部分底层组件用Rust重写
- AWS、Google、Microsoft都在关键基础设施中使用Rust
- Stack Overflow开发者调查中,Rust连续多年被评为"最受喜爱的编程语言"
use std::collections::HashMap;
fn word_count(text: &str) -> HashMap<&str, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
let count = counts.entry(word).or_insert(0);
*count += 1;
}
counts
}
fn main() {
let text = "hello world hello rust hello world";
let counts = word_count(text);
for (word, count) in &counts {
println!("{}: {}", word, count);
}
}
十一、语言设计的永恒争论
静态类型 vs 动态类型
编程语言社区最持久的争论之一是:类型应该在编译时检查还是运行时确定?
静态类型阵营(C、Java、Go、Rust、TypeScript)认为:
- 类型错误应该在编译时发现,而不是在生产环境中
- 类型信息就是文档,IDE可以提供更好的自动补全和重构
- 编译器的优化机会更多
动态类型阵营(Python、Ruby、JavaScript、Lua)认为:
- 类型声明增加了代码的冗长度
- 快速迭代比类型安全更重要
- 测试可以弥补类型检查的缺失
历史趋势表明,两种阵营正在相互靠拢。动态类型语言开始添加类型提示(Python的type hints、TypeScript),静态类型语言开始支持类型推断(Rust、Go、Kotlin),让你不必总是显式声明类型。
垃圾回收 vs 手动管理 vs 所有权
内存管理是编程语言设计中最关键的决策之一:
| 方案 | 代表语言 | 优点 | 缺点 |
|---|---|---|---|
| 手动管理 | C, C++ | 最大控制,零开销 | 容易出错,安全漏洞 |
| 垃圾回收 | Java, Python, Go | 简单安全 | GC暂停,内存开销 |
| 所有权系统 | Rust | 安全且零开销 | 学习曲线陡峭 |
编译 vs 解释
- 编译型(C、C++、Rust、Go):源代码先被翻译成机器码,然后执行。速度快,但需要编译步骤
- 解释型(Python、Ruby、JavaScript):源代码被逐行解释执行。启动快,但运行速度慢
- JIT编译(Java、C#、现代JavaScript):先编译成中间码,运行时再编译成机器码。兼顾灵活性和性能
值得注意的是,这种分类的界限正在变得模糊。Python的标准实现CPython实际上会先编译成字节码再执行,所以它既是"解释型"也是"编译型"。V8引擎对JavaScript使用了多层JIT编译策略--先用快速但优化程度低的编译器执行热点代码,然后用慢速但高度优化的编译器重新编译最频繁执行的代码路径。
面向对象 vs 函数式
面向对象编程(OOP)强调封装、继承、多态。函数式编程(FP)强调不可变数据、纯函数、高阶函数。
这两种范式之间的争论已经持续了几十年。现实是,大多数现代语言都支持两种范式的混合。你可以用Java写纯函数式的代码,也可以用Haskell模拟面向对象的行为。
"最好的编程语言不是那种有最多特性的语言,而是那种让你最容易表达想法的语言。"
函数式编程在过去十年中经历了一次显著的复兴。这在很大程度上归功于并发编程的需求--不可变数据天然是线程安全的,这使得函数式风格在多核时代变得格外有吸引力。Haskell、Erlang、Clojure等纯函数式语言虽然没有成为主流,但它们的核心思想--不可变性、纯函数、模式匹配--已经被主流语言广泛吸收。
十二、下一个十年
编程语言的进化没有终点。每一代语言都在解决上一代留下的问题,同时也创造新的问题:
- Fortran解决了"科学家不需要写汇编"的问题,但引入了
GOTO面条代码 - C解决了"需要一种可移植的系统语言"的问题,但引入了内存安全噩梦
- Java解决了"跨平台和内存管理"的问题,但引入了冗长和GC暂停
- Python解决了"编程应该简单"的问题,但引入了性能和并发的瓶颈
- Rust解决了"安全和性能不可兼得"的问题,但引入了陡峭的学习曲线
没有完美的编程语言,只有适合特定场景的权衡。
如果你要写操作系统内核或嵌入式驱动,选C或Rust。如果你要快速做一个原型或数据分析,选Python。如果你要构建大规模分布式后端,选Go或Java。如果你要做Web前端,选TypeScript。
但更重要的是:不要被任何一种语言绑定。编程语言是工具,不是信仰。最好的程序员不是精通一种语言的人,而是理解编程本质、能够快速学习任何新语言的人。
正如艾伦·佩利斯(Alan Perlis)所说:
"一种不能影响你思考编程方式的语言,不值得了解。"
展望未来,AI辅助编程正在改变语言的生态。GitHub Copilot和类似的工具已经能够根据注释自动生成代码。这可能会降低学习新语言的门槛,也可能改变语言设计的方向--如果AI能帮你写代码,那么语言的"易写性"就不再那么重要,而"易读性"和"安全性"会变得更加关键。
另一个值得关注的趋势是领域特定语言(DSL)的兴起。SQL用于数据库查询、HTML用于网页结构、正则表达式用于文本匹配--这些DSL证明了"为特定任务设计专用语言"的力量。未来可能会有更多针对AI模型训练、量子计算、区块链等新兴领域的专用语言出现。
回顾整个编程语言的进化史,我们会发现一个反复出现的模式:每一次“革命”都不是对过去的完全否定,而是对过去的继承与超越。Fortran继承了汇编的计算模型,但用数学语法包装了它。C继承了汇编的底层控制能力,但添加了可移植性。Java继承了C++的语法,但用垃圾回收解决了内存管理问题。Python继承了多种语言的优点,但用简洁性统一了它们。Rust继承了C的性能哲学,但用所有权系统解决了安全问题。没有哪种语言是从真空中诞生的——它们都站在前人的肩膀上,试图看得更远一点。这就是编程语言进化的本质:不是替代,而是叠加。
如果这篇文章让你想学一门新语言,那就去学吧。但记住:学习编程语言的语法只需要几天,理解它的设计哲学需要几年,而真正掌握一门语言需要写几十万行代码。