当你在浏览器输入一个网址,真正发生了什么?
打开浏览器,输入:
https://example.com
按下回车。几百毫秒之后,一张网页出现在屏幕上。对于绝大多数人来说,这个动作简单得近乎没有存在感。但从计算机系统的角度看,这其实是一场跨越多个系统层次的协作。
你的键盘产生输入,浏览器解析 URL,系统尝试找到域名对应的 IP 地址,网络协议栈准备数据。可能需要建立 TCP 连接;如果是 HTTPS,还需要建立 TLS 会话并验证服务器身份。HTTP 请求被封装进网络数据包,数据经过操作系统、网卡、家庭路由器、运营商网络、互联网骨干网络、中间路由设备,最终抵达服务器所在的网络。
服务器收到数据后,再把它交给自己的网络协议栈、Web Server、应用程序。应用程序开始执行:可能查询数据库,可能访问缓存,可能调用其他服务,可能读取文件,然后生成 HTTP 响应。响应再次经过网络返回浏览器。最后,浏览器把 HTML、CSS、JavaScript、图片、字体等资源组织起来,交给渲染引擎。
你看到的,不是一台计算机完成的结果,而是一整套分层系统完成的结果。
互联网并不是“浏览器连接服务器”这么简单。
真正重要的问题是:浏览器为什么能够找到服务器?服务器为什么能够知道请求来自谁?数据为什么能够跨越这么多网络设备最终抵达目标?TCP、TLS、HTTP 分别解决什么问题?当网络发生丢包、延迟、拥塞时,系统又是如何继续工作的?
而更重要的是:
为什么今天的 Web 是这样设计的?
理解这些问题,才算真正开始进入网络编程。
一、先不要学 Go:先理解互联网到底解决了什么问题
互联网最本质的问题其实很简单:
两台不认识的计算机,如何交换数据?
假设你的电脑是:
Client
服务器是:
Server
最开始,我们可以非常粗暴地想:
Client ----------------------> Server
"Hello"
看起来没有什么复杂的。但这个模型马上会遇到问题。服务器在哪里?如果服务器地址是:
192.168.1.10
那互联网里的几十亿设备怎么知道它在哪里?如果网络中间有设备丢掉了数据怎么办?如果数据被别人偷看怎么办?如果客户端一次发送 1 KB,服务器只收到 500 字节怎么办?如果服务器同时面对一万个客户端怎么办?如果客户端突然断网怎么办?如果一个服务器有多个网站怎么办?
如果一个网页包含:
index.html
style.css
app.js
logo.png
avatar.jpg
font.woff2
浏览器又该如何知道这些资源从哪里获取?
所以互联网真正解决的,从来不是单纯的“把数据从 A 发给 B”,而是:
在一个不可靠、规模巨大、设备类型复杂、网络质量不断变化的环境中,让不同计算机能够可靠地交换信息。
这也是网络协议存在的原因。协议不是为了增加复杂度,恰恰相反,协议是在控制复杂度。
二、为什么不能让所有程序直接通信?
假设我们没有任何协议,浏览器自己规定一套格式:
HELLO|URL|DATA
另一个程序又规定:
REQUEST@URL@DATA
第三个程序可能使用:
<request>
<url>...</url>
</request>
那么互联网很快就会变成一片混乱。所以计算机网络采取了一个非常重要的思想:
分层。
一个系统不要试图一次解决所有问题,而是让每一层只解决自己负责的问题。可以把一次 Web 请求粗略理解成:
应用层
│
│ HTTP
▼
传输层
│
│ TCP / QUIC
▼
网络层
│
│ IP
▼
链路层
│
│ Ethernet / Wi-Fi
▼
物理介质
│
▼
电信号 / 无线信号
这意味着:HTTP 不需要知道电缆是怎么传输比特的,TCP 不需要知道 HTTP 请求到底是网页、JSON 还是图片,IP 不需要知道上面传的是 HTTP 还是其他协议。每层处理自己的问题,于是复杂系统被拆成了一组可以组合的系统。
这套思想后来影响了整个软件工程。微服务、模块化、操作系统、编译器、网络协议,本质上都在不断重复这个模式:
把一个巨大的复杂问题拆成多个边界清晰的问题。
三、第一步:浏览器为什么知道 example.com 在哪里?
你输入的是:
https://example.com
但计算机真正进行网络通信时,需要的是 IP 地址。例如:
93.184.216.34
这里存在一个巨大的认知差异:人类喜欢名字,机器更容易使用地址。DNS 就是解决这个问题的系统。DNS,全称 Domain Name System,是一个分层、分布式的命名系统,用来把人类可读的域名映射到 IP 地址等资源。于是:
example.com
│
│ DNS
▼
93.184.216.34
这一步经常被理解成“DNS = 域名转 IP”,但这个理解还不够。DNS 真正有价值的地方,是它把:
“服务的名字”与“服务当前位于哪里”
进行了分离。这是一种极其重要的架构思想。
四、为什么不能直接写死 IP?
假设我们直接把 example.com 写成 93.184.216.34,看起来更直接。但现实系统很快会崩掉。因为服务器会扩容,可能从 1 台变成 10 台,再变成 100 台,甚至不同区域拥有不同服务器。
于是 example.com 并不一定只对应一个 IP,它更像:
example.com
│
├── IP A
├── IP B
├── IP C
└── IP D
DNS 由此成为整个互联网基础设施里非常重要的一层。这也是为什么域名不仅仅是一个“好记的名字”,它实际上是一层:
位置抽象。
应用不需要知道服务器的物理位置,只需要知道服务名称。这种思想在后面的服务发现、微服务、API Gateway、Kubernetes Service、Service Mesh 中还会再次出现。所以网络知识学到最后,你会发现:DNS 并不只是一个协议,它体现的是一种现代系统设计思想:
名称与位置解耦。
五、找到服务器以后,下一步是什么?
假设 DNS 已经给我们:
93.184.216.34
现在问题变成:我的数据怎么到这台机器?这时候 IP 出场。可以先把 IP 理解成:
互联网世界里的寻址系统。
数据被切分成网络层能够处理的数据包,然后经过一个又一个网络节点:
Browser
│
▼
Operating System
│
▼
Wi-Fi / Ethernet
│
▼
Home Router
│
▼
ISP
│
▼
Backbone Network
│
▼
Router
│
▼
Data Center Network
│
▼
Server
这里有一个很重要的事实:
互联网不是一条从浏览器直接连到服务器的线路。
而是一系列互联网络组成的网络。每一个路由设备都在做同一个判断:“这个包下一步应该往哪里走?”因此:
A → B → C → D → Server
并不是一个永久固定的物理路径。网络状况发生变化时,路径可能改变。这就是为什么 IP 解决的是“往哪里走”,而不是“最终一定能完整地把数据交付给应用程序”。后一个问题需要更高的协议来解决。
六、IP 能把包送到服务器,但它保证不了什么?
假设浏览器发出去:
1
2
3
4
5
网络中发生:
1 ✅
2 ✅
3 ❌
4 ✅
5 ✅
服务器收到:
1
2
4
5
IP 不负责解决“第 3 个数据哪里去了”,这就是传输层的问题。
七、TCP:互联网为什么还能“可靠”工作?
TCP 是 Internet 中最重要的传输协议之一。它负责为应用程序提供可靠的字节流传输机制,并定义了连接建立、数据传输、重传等行为。现代 TCP 的标准规范已经由 RFC 9293 统一整理。
可以简单理解成:
HTTP
│
▼
TCP
│
▼
IP
HTTP 关心的是“我要请求什么资源”,TCP 关心的是“这些数据怎么可靠地送过去”。这两个问题完全不同。
八、TCP 的核心不是“三次握手”,而是可靠传输
网络教程最喜欢讲 TCP 三次握手,然后背:
SYN
SYN + ACK
ACK
这当然没有错。但如果只记住“三次握手”,实际上几乎没有理解 TCP。TCP 真正解决的是:
在一个可能丢包、乱序、延迟、重复的数据网络之上,为应用提供可靠、有序的字节流。
例如应用程序发送:
ABCDEFGHIJ
网络中可能发生:
A B C D
↓
丢失
E F G
↓
延迟
H I J
TCP 在两端维护状态,通过序列号、确认、重传、流量控制、拥塞控制等机制,让应用层最终看到的是一个连续的数据流。于是应用程序不需要自己处理“第 17 个包在哪里”“第 18 个包有没有重复”“刚才是不是丢包”“网络是不是拥塞”,这些复杂性被封装在 TCP 里面。这就是分层设计真正的价值:
应用程序把网络传输的复杂性下沉给传输层。
九、但 TCP 也不是万能的
TCP 的可靠性是有成本的。它需要维护:
Sequence Number
ACK
Retransmission
Receive Window
Congestion Window
Connection State
这些状态都会产生开销。尤其是在现代互联网环境中,一个页面可能同时涉及很多资源。于是新的问题出现:如果一个 TCP 连接中的某部分数据出现问题,会发生什么?
在 HTTP/2 中,多路请求可以共享一个 TCP 连接,这样连接数量减少了。但 TCP 自己的可靠字节流语义仍然存在,这意味着某些情况下,一个丢失的 TCP 数据包会影响该连接上其他正在等待的数据。
HTTP/3 采用了不同的路径:
HTTP/3
│
▼
QUIC
│
▼
UDP
│
▼
IP
HTTP/3 是把 HTTP 语义映射到 QUIC 之上,而 QUIC 提供多流、每流流控、低延迟建立和连接迁移等能力。这不是“UDP 比 TCP 高级”,真正的变化是:
一些原本属于 TCP 的传输控制能力,被重新组织到了 QUIC 中。
这是网络工程非常典型的演进方式:不是抛弃旧问题,而是重新划分边界。
十、现在回到浏览器:我们真正访问的是 HTTPS
现实世界中的浏览器访问,大量使用的是 HTTPS,而不是裸 HTTP。HTTPS 的核心思想是:
HTTP
+
TLS
TLS 的目标是让客户端和服务器之间的通信具备机密性、完整性,并帮助完成身份认证。TLS 1.3 的标准定义在 RFC 8446 中。所以:
HTTP
↓
TLS
↓
TCP
↓
IP
可以先这么理解。
十一、为什么需要 TLS?
假设你在浏览器里登录:
username = alice
password = 123456
如果数据直接裸奔:
Internet
─────────────────────
username=alice
password=123456
中间任何能够观察网络流量的设备,都可能看到数据。所以需要加密。最终你看到的是类似:
Client
│
│ encrypted data
▼
Internet
│
▼
Server
中间节点看到的是密文,而不是你的 HTTP 内容。但 TLS 不只是加密。真正完整的问题是:
我怎么知道对面的服务器真的是我想访问的服务器?
这就是数字证书、证书链和身份认证等机制发挥作用的地方。
十二、一次 HTTPS 请求,究竟经历了什么?
现在把前面的知识串起来。假设你输入:
https://example.com
可以建立一个非常简化的模型:
① 浏览器解析 URL
https://example.com/
│
▼
② DNS 查询
example.com
│
▼
IP 地址
③ 建立传输连接
TCP / QUIC
│
▼
④ 建立加密会话
TLS
│
▼
⑤ 发送 HTTP 请求
GET /
Host: example.com
│
▼
⑥ 服务器处理
│
▼
⑦ HTTP Response
200 OK
...
这才是浏览器打开网页的基本骨架。
十三、HTTP 到底是什么?
HTTP 是应用层协议,它解决的问题非常具体:
客户端和服务器之间,如何表达请求与响应。
例如:
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
服务器可能返回:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234
再加上真正的数据:
<!doctype html>
<html>
...
</html>
所以 HTTP 并不负责 DNS、路由、TCP 可靠性、电缆传输、网卡驱动。HTTP 只需要定义“请求长什么样”以及“响应长什么样”。这就是应用层协议的边界。
十四、HTTP 为什么没有直接把互联网设计完?
因为 HTTP 不应该负责所有事情。这就是软件架构里的一个重要原则:
职责边界。
HTTP 负责资源、请求、响应;TCP 负责可靠传输;IP 负责寻址与路由;TLS 负责安全通信;DNS 负责名称解析;浏览器负责资源加载、页面执行与渲染;服务器应用负责业务逻辑;数据库负责数据持久化。最终形成一张非常复杂但边界清晰的系统。
15. HTTP 本身也一直在进化
Web 最初非常简单。早期 HTTP 的目标只是让浏览器能够获取超文本,但网页后来越来越复杂。一个页面可能包含 HTML、CSS、JavaScript、Image、Video、Font、JSON、WebSocket 等资源。浏览器与服务器之间的通信压力越来越大,因此 HTTP 一直在演进。可以粗略理解成:
HTTP/0.9
↓
HTTP/1.0
↓
HTTP/1.1
↓
HTTP/2
↓
HTTP/3
HTTP/1.1 的消息结构非常直观,但随着 Web 复杂度增加,连接复用、多路复用、头部压缩等需求越来越明显。HTTP/2 引入了二进制分帧和多路复用。而 HTTP/3 则把 HTTP 语义映射到了 QUIC 上——RFC 9114 明确规定 HTTP/3 使用 QUIC 作为传输层,并利用其流和流控能力。
因此,HTTP/3 并不是“HTTP 4 的重新发明”,而是:
重新思考 HTTP 与传输层之间的边界。
十六、为什么 HTTP/3 会使用 UDP?
这是网络学习里最容易出现误解的地方。很多人会说“UDP 更快,所以 HTTP/3 用 UDP”,这句话太简单。真正的原因是:
QUIC 希望在用户空间重新构建现代传输协议所需要的机制,同时利用 UDP 作为底层数据报承载。
QUIC 本身提供流、流量控制、低延迟连接建立、丢包检测、连接迁移、加密能力。因此更准确的关系是:
HTTP/3
↓
QUIC
↓
UDP
↓
IP
而不是 HTTP/3 直接跳到 UDP。它们之间还隔着一个非常重要的传输协议:QUIC。
十七、服务器收到 HTTP 请求以后,发生了什么?
现在终于可以进入服务器。例如:
GET /users/123 HTTP/1.1
Host: example.com
数据到达服务器之后,并不会直接跑进 func main() 中。中间还隔着操作系统网络栈。可以粗略理解成:
Network Card
│
▼
Linux Kernel
│
▼
TCP / UDP
│
▼
Socket
│
▼
Go Runtime / net package
│
▼
net/http
│
▼
Handler
│
▼
Business Logic
这个结构非常重要,因为它解释了:
网络编程为什么从来不仅仅是“写一个 HTTP Handler”。
你写的业务代码其实处在整个网络栈的最上层。
十八、Go 是怎么进入这个体系的?
这时候 Go 才真正出现。Go 标准库提供了 net 处理网络基础能力,以及 net/http 处理 HTTP。所以一个最简单的 Go HTTP 服务器可以写成:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go 1.26.5")
})
log.Println("server listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
使用 Go 1.26.5 运行,然后浏览器访问 http://127.0.0.1:8080/。浏览器发起 GET /,Go 程序返回:
Hello from Go 1.26.5
看起来只有几十行代码,但实际上,这几十行代码站在了非常厚的系统之上。
十九、这就是 Go 最厉害的地方之一
你没有亲自实现 TCP、DNS、HTTP parser、socket、epoll、线程池,但是你可以直接写:
http.HandleFunc(...)
这并不意味着 Go“把网络变简单了”,真正发生的是:
Go 把复杂性封装到了标准库、Runtime 和操作系统接口之后。
与此同时,它又没有把底层彻底隐藏。你依然可以继续向下:
net/http
↓
net
↓
Syscall
↓
Kernel
因此 Go 很适合作为服务器语言,因为它在工程效率和系统控制能力之间提供了一个很有价值的平衡。
二十、Go 的 HTTP 服务器为什么能同时处理很多请求?
例如:
http.HandleFunc("/", handler)
你没有写 ThreadPool,也没有自己创建 pthread,但服务器依然可以同时处理大量请求。这是因为 Go 的并发模型和 Runtime。典型的 Go 网络服务器会大量使用 Goroutine。可以简化理解成:
Incoming Request
│
├───────────────┐
│ │
▼ ▼
Goroutine A Goroutine B
│ │
▼ ▼
Handler Handler
这并不意味着“一请求 = 一操作系统线程”。Go Runtime 会负责 Goroutine 的调度。所以:
大量请求
↓
大量 Goroutine
↓
Go Scheduler
↓
有限数量 OS Threads
↓
CPU
这正是前面并发章节中 G-P-M 模型真正开始与网络结合的地方。
二十一、网络 IO 与 Goroutine 为什么能结合得很好?
这里进入 Go 网络编程非常关键的一层:netpoll。
网络程序最浪费时间的东西,通常不是 CPU,而是等待。比如等待客户端发送数据、等待服务器读取数据、等待网络数据到达等。假设一个线程正在等待网络:
Thread
│
▼
recv()
│
│ waiting...
│
│
│
如果每一个连接都占用一个操作系统线程,那么连接数量一大,系统就会越来越昂贵。现代操作系统提供了高效的 I/O 多路复用机制。Linux 上典型的是 epoll,macOS/BSD 家族常见的是 kqueue。Go Runtime 把这些系统能力封装进了自己的网络轮询机制。所以可以粗略理解为:
Go Program
│
▼
net/http
│
▼
net
│
▼
netpoll
│
├──── epoll
├──── kqueue
└──── 其他平台机制
│
▼
Operating System
最终形成:
Goroutine + Scheduler + Netpoll + OS Kernel
这一整套组合。这才是 Go 网络服务器性能的重要基础。
二十二、所以“Go 能处理百万连接”到底是什么意思?
这里必须非常谨慎。“百万连接”不是写一行 Go 代码然后自动获得百万并发。真实世界远比这复杂。连接数量受到内存、文件描述符、CPU、带宽、内核参数、网络设备、Goroutine 数量、业务逻辑、数据库、缓存、锁竞争、GC 等大量因素影响。
Go 能做的,是提供非常合适的基础模型。例如:
大量连接
│
▼
网络事件
│
▼
Netpoll
│
▼
Goroutine 唤醒
│
▼
Scheduler
│
▼
Handler
所以真正值得学习的不是“Go 可以百万连接”,而是:
为什么一种编程语言能够把大量并发 I/O 映射到相对有限的系统线程和 CPU 资源上。
这才是系统工程问题。
二十三、一个 HTTP 请求真正穿过了多少层?
现在重新看一次:
浏览器
│
│ URL
▼
DNS
│
│ IP
▼
TCP / QUIC
│
▼
TLS
│
▼
HTTP
│
▼
网卡
│
▼
操作系统内核
│
▼
网络设备
│
▼
互联网
│
▼
服务器
│
▼
网卡
│
▼
操作系统
│
▼
Socket
│
▼
Go Runtime
│
▼
net/http
│
▼
Handler
│
▼
业务系统
│
├── Cache
├── Database
├── RPC
└── File
于是你会发现:
一行 http.Get() 背后,可能隐藏着整个互联网。
二十四、浏览器拿到 HTML 以后,事情还没有结束
这是很多初学者容易忽略的地方。服务器返回:
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
</head>
<body>
<img src="logo.png">
</body>
</html>
浏览器发现 style.css、app.js、logo.png 又要发请求。于是:
GET /
↓
HTML
↓
发现 CSS
↓
GET /style.css
发现 JS
↓
GET /app.js
发现 Image
↓
GET /logo.png
所以一个“打开网页”往往不是一次网络请求,而是一组请求。这也是为什么现代浏览器需要连接复用、HTTP/2 多路复用、HTTP/3、缓存、CDN、压缩、资源预加载等大量技术。Web 的复杂度,就是这样一点点增长起来的。
二十五、CDN 为什么会出现?
假设服务器在美国,用户在东京。如果所有内容都必须从东京到美国往返,延迟自然会增加。所以现代系统会增加 CDN Node,形成:
User
│
▼
Nearby CDN
│
├── Cache Hit
│
└── Cache Miss
│
▼
Origin Server
这又是一个和 DNS 类似的思想:用户不关心真正的源服务器在哪里。而系统通过 DNS、路由、CDN、Anycast、负载均衡等方式,把请求导向更合适的位置。你会发现:整个互联网一直在做同一件事情:
隐藏复杂性。
二十六、反向代理又是什么?
真实生产环境里,浏览器通常不会直接连接 Go 应用。更常见的结构是:
Browser
│
▼
Internet
│
▼
Load Balancer
│
▼
Reverse Proxy
│
├──── Go Server A
├──── Go Server B
└──── Go Server C
为什么?因为业务服务器不应该承担所有边缘工作。反向代理可以负责 TLS、连接管理、限流、路由、压缩、缓存、负载均衡。Go 应用则专注业务逻辑。于是:
HTTP 服务器只是整个互联网系统中的一个组件。
不是全部。
二十七、Go 在这套体系里到底站在哪里?
现在终于可以给 Go 定位:
Browser
│
▼
Internet
│
▼
TCP / QUIC / TLS
│
▼
HTTP Layer
│
▼
Go net/http
│
▼
Go Runtime
│
▼
OS Kernel
│
▼
Hardware
Go 并没有创造 TCP、IP、HTTP、DNS、TLS。Go 真正做的是:
把复杂的系统能力组织成一套开发者可以高效使用的工程工具。
这也是为什么到了 Day 61,你已经不能只把 Go 当作一门语法简单的语言。现在应该开始把 Go 放回它真正的位置:
系统软件和网络软件的工程工具。
二十八、和 C、C++、Rust、Java 比,Go 到底强在哪里?
没有一种语言在所有场景都最好。
C
C 给你极高的控制能力,你可以非常直接地接近 Memory、CPU、Syscall、Socket、Kernel。但代价是开发成本高、内存安全问题多、并发代码复杂、工程维护成本高。
C++
C++ 在 C 的基础上提供了大量抽象能力,优势非常强大,但复杂度也非常高。尤其是在大型工程里,Templates、Object Lifetime、ABI、Build System、Toolchain 都会增加认知负担。
Rust
Rust 非常强调内存安全与接近零成本抽象,对于系统软件、网络基础设施、高性能组件、安全关键代码非常有价值。但它把大量复杂性提前交给编译器和类型系统,这意味着学习成本通常比 Go 更高。
Java
Java 最大的优势之一是巨大生态、JVM、成熟企业工具链,特别适合企业应用、大型业务系统、长期维护系统。但它的运行时模型与 Go 不同。
Go
Go 的设计目标非常不同。它并不追求让语言本身拥有最多功能,而更关注简单语法、快速编译、清晰工程结构、高效并发、成熟网络库、可部署性、可维护性。因此 Go 的核心价值从来不是“性能绝对第一”,而是:
在现代服务器开发中,把系统复杂度控制在工程团队能够长期承受的范围内。
二十九、性能真正应该看什么?
网络程序性能不能只看 QPS。至少应该同时看 Throughput、Latency、CPU、Memory、Connections、GC、Network I/O、System Calls。
例如 QPS = 100,000 听起来很厉害,但是如果 P99 = 5s,那系统一样可能不可用。反过来 P99 = 5ms 但只能处理 500 QPS,同样可能不满足业务需求。所以真正的工程问题是:
在一定资源预算下,系统能否稳定地处理目标负载?
这才是性能。
三十、Cache Miss 为什么也会影响网络服务器?
假设一个请求需要访问 CPU Cache 和 Memory。CPU Cache 的速度远高于主内存,所以 Cache Hit 和 Cache Miss 可能产生明显差异。网络服务器又是高度并发的,如果多个线程或者 Goroutine 频繁访问共享数据,可能产生 Cache Miss、False Sharing、Lock Contention、Memory Traffic。
因此高性能网络服务不是“把代码写得漂亮”就够了。你最终必须理解 CPU、Cache、Memory、Scheduler、Kernel、Network 这些层如何共同作用。
三十一、系统调用也不是免费的
假设服务器频繁进行 read()、write()、send()、recv(),每次进入内核都存在成本。可以简单理解成:
User Space
│
│ syscall
▼
Kernel Space
│
│ work
▼
Return
所以高性能服务器一直在思考:能不能减少系统调用次数?例如批处理、Buffer、零拷贝思想、连接复用、多路复用、内核旁路等,本质上都在试图减少不必要的成本。
三十二、真正优秀的网络程序,优化的不是某一行代码
它优化的是整个路径:
Browser
↓
DNS
↓
Network
↓
TLS
↓
HTTP
↓
Kernel
↓
Socket
↓
Runtime
↓
Scheduler
↓
Handler
↓
Database
任何一层成为瓶颈——CPU、Memory、Network、Kernel、Lock、Database——最终都可能拖慢整个系统。所以系统性能分析真正应该问的是“瓶颈在哪里”,而不是“哪一行代码最快”。
三十三、Go 1.26.5 在这里有什么意义?
本文虽然讨论的是互联网基础,但最终我们要用 Go 实现网络系统,所以版本必须明确。本文以:
go1.26.5
为基准。Go 1.26 是 2026 年 2 月发布的大版本,之后继续发布了多个维护版本;官方发布历史显示,Go 1.26.5 于 2026 年 7 月 7 日发布,包含针对 crypto/tls、os、编译器、Runtime、go 命令以及 net、syscall 等组件的安全修复和 bug 修复。Go 1.26 本身还带来了新的 Runtime 与工具链变化,例如 Green Tea GC 默认启用,以及部分编译器和运行时性能改进。
不过对于 Day 61 来说,一个非常重要的原则是:
不要因为 Go 版本升级,就把网络基础学习变成 API 版本学习。
TCP 仍然是 TCP,DNS 仍然是 DNS,HTTP 的分层思想也没有改变。版本变化真正影响的是:实现这些概念时,Go 的标准库、Runtime 和工具链今天是什么样子。
三十四、用 Go 观察一次 HTTP 请求
我们可以先写一个简单的服务器:
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method)
fmt.Println("url:", r.URL.String())
fmt.Println("host:", r.Host)
fmt.Println("remote:", r.RemoteAddr)
fmt.Fprintln(w, "hello")
}
func main() {
http.HandleFunc("/", handler)
log.Println("listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
浏览器访问 http://127.0.0.1:8080/,服务器可能打印:
method: GET
url: /
host: 127.0.0.1:8080
remote: 127.0.0.1:xxxxx
这时候你应该开始意识到:GET、/、Host、RemoteAddr 都不是 Go 创造出来的。Go 只是把 HTTP 世界映射到了程序模型。也就是说:
Internet Protocol
↓
Go API
↓
Go Program
这是非常重要的认知。
三十五、再写一个 Go HTTP Client
Go 不只能够创建服务器,也能发起 HTTP 请求:
package main
import (
"fmt"
"log"
"net/http"
"io"
)
func main() {
resp, err := http.Get("https://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println("status:", resp.Status)
fmt.Println(string(body))
}
这个程序看起来非常简单。但 http.Get(...) 背后可能涉及 URL 解析、DNS 查询、TCP 建连、TLS 握手、HTTP 请求、Socket 操作、Runtime 调度、Kernel 系统调用。这就是为什么网络编程值得单独拿出一个阶段来学习。
三十六、为什么网络编程必须从“请求的生命历程”开始学习?
因为网络 API 很容易让人产生错觉。例如 http.Get(url) 可能让你以为“发送 HTTP 请求就是调用一个函数”。但真正的事实是:
Function Call
↓
HTTP Client
↓
Connection
↓
TLS
↓
Socket
↓
Kernel
↓
Network
↓
Remote Kernel
↓
Remote Socket
↓
HTTP Server
↓
Application
所以真正成熟的网络工程师,遇到问题时不会只看 http.Get(...),而会问:DNS 是不是慢?TCP 建连是不是慢?TLS 握手是不是慢?服务端是不是没及时接受连接?内核是不是存在队列压力?Handler 是不是阻塞?数据库是不是慢?网络是不是丢包?
这就是:
从 API 思维走向系统思维。
三十七、互联网最大的工程思想:每一层只做自己的事情
从今天开始,你应该慢慢建立一张这样的地图:
DNS
│
└── 我是谁?
IP
│
└── 我去哪里?
TCP / QUIC
│
└── 怎么传?
TLS
│
└── 怎么安全地传?
HTTP
│
└── 我们要交换什么?
Go net/http
│
└── 程序怎么处理?
Go Runtime
│
└── 并发任务怎么运行?
OS Kernel
│
└── 网络设备怎么工作?
Hardware
│
└── 最终怎么发送出去?
这张地图比记住几十个 API 更重要。
三十八、从这里开始,你已经进入真正的后端世界
Day 1 学的是“什么是程序”,Day 10 开始认识“为什么 Go 值得学习”,Day 30 完成 Go 语言基础,Day 45 进入软件工程,Day 46 开始并发。到了今天 Day 61:
我们终于把 Go 放到了互联网里。
接下来,Go 不再只是 var、func、struct、interface,而开始变成 Server、Socket、Connection、HTTP、TCP、Concurrency、Runtime、Network。这也是 Go 真正擅长的领域开始出现的地方。
三十九、Go 为什么在服务器领域拥有如此强的生命力?
因为现代服务器面对的核心问题,恰好是 Go 非常关注的问题:很多并发请求、很多网络连接、大量 I/O、快速部署、长期维护、团队协作、系统可观测性。Go 用 Goroutine、Channel、Context、net、net/http、Runtime 构建出了一套相对完整的服务器工程模型。
但是一定要注意:
这不意味着 Go 在所有服务器问题上都最优。
极致性能底层组件可能更偏向 C、C++、Rust;大量历史企业系统依然可能使用 Java、C#;某些 CPU 密集型任务也可能更适合其他方案。Go 的优势不是“天下第一”,而是:
在现代网络服务中,以较低的工程复杂度获得足够高的性能与并发能力。
这才是客观的评价。
四十、从今天开始,不要再把“网站”理解成一个页面
一个网站,真正可能是:
Browser
│
▼
DNS
│
▼
CDN
│
▼
Load Balancer
│
▼
Reverse Proxy
│
▼
API Gateway
│
├──────────────┐
▼ ▼
Go Service A Go Service B
│ │
▼ ▼
Redis Database
│
▼
Message Queue
│
▼
Other Services
而这还只是应用层。下面还存在 TLS、TCP/QUIC、IP、Network、Kernel、NIC、Hardware。所以现代互联网不是一个浏览器加一个服务器,而是一座巨大的分布式系统。
四十一、真正值得思考的问题
理解互联网之后,一个更有意思的问题会出现:如果今天重新设计互联网,我们还会按照几十年前的方式设计吗?
事实上,答案已经正在发生。我们已经看到:
HTTP/1.1
↓
HTTP/2
↓
HTTP/3
TCP
↓
QUIC
Static Server
↓
CDN
Single Server
↓
Load Balancer
Monolith
↓
Microservices
Machine IP
↓
Service Discovery
整个互联网几十年来一直在变化。但有一些东西几乎没有变化:分层、抽象、解耦、隐藏复杂性。这可能才是互联网留给软件工程最重要的遗产之一。
四十二、而 Go 真正要解决的,也不是“写网页”
到了这里,我们终于可以重新理解 Go。Go 并不是专门为了写网页,而是非常适合:
构建长期运行的网络系统。
比如 HTTP Server、API Gateway、Reverse Proxy、RPC Service、Message Service、Storage Service、Control Plane、Cloud Service、Container Runtime、Infrastructure Software。这也是为什么网络会成为 Go 学习路线后半段的核心。
四十三、未来的网络程序会变成什么样?
未来的服务器不会只是 Client → Server,而会越来越像:
Client
↓
Edge
↓
CDN
↓
Gateway
↓
Service Mesh
↓
Microservices
↓
Cache
↓
Database
↓
Distributed Storage
↓
AI Service
协议也不会停留在 TCP + HTTP/1.1,而会继续向 HTTP/2、HTTP/3、QUIC 以及下一代传输与安全技术演进。所以学习网络的真正价值,并不是为了知道 http.Get() 怎么调用,而是为了知道:
一条数据从一个程序走向另一个程序时,整个系统究竟发生了什么。
结语:当你输入一个网址的时候,互联网其实已经开始“工作”了
下次你在浏览器输入 https://example.com,不要再把它理解成“浏览器打开了一个网页”。试着把整个过程在脑子里重新播放一遍:
URL
↓
DNS
↓
IP
↓
TCP / QUIC
↓
TLS
↓
HTTP
↓
Network
↓
Router
↓
Server
↓
Kernel
↓
Socket
↓
Go Runtime
↓
net/http
↓
Handler
↓
Business Logic
↓
Database
↓
Response
↓
Browser
↓
Render
你看到的那一张网页,只是整个系统最终露在你眼前的一小部分。真正值得学习的,是网页背后的那座机器。
因为当你开始理解——数据究竟是怎样从一台计算机,穿过一个由无数机器组成的全球网络,最终抵达另一台计算机的——你学习的就已经不再只是 Go。你开始学习的是:
现代计算机系统究竟是如何彼此协作的。
而这,才是网络编程真正的起点。
下一天,我们将不再停留在“互联网是什么”。我们会进一步把一条连接拆开:
TCP 到底是怎么工作的?
从一次连接的建立开始,看到 SYN、ACK、序列号、确认、重传、流量控制与拥塞控制。到了那里,你会第一次真正看到:一个看似简单的 conn.Read(),下面究竟藏着多少系统设计。
如果你对这类从底层协议到工程实践的完整拆解感兴趣,欢迎常来云栈社区逛逛,那里有不少同样在啃网络与系统方向的开发者,大家一起交流、沉淀、避坑。