Go 后端面试复盘:语言、并发与服务实践 · 17. TCP、HTTP 与网络编程至18. 序列化:JSON 与 Protobuf
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
17. TCP、HTTP 与网络编程
17.1 TCP 是什么?
TCP 是面向连接、可靠、有序的字节流协议。它提供:
- 序号与确认。
- 校验与超时重传。
- 流量控制。
- 拥塞控制。
- 按序交付。
TCP 不保留应用消息边界。一次 Write 不一定对应一次 Read。
17.2 什么是 TCP 粘包和拆包?
“粘包”本质是应用层误以为 TCP 有消息边界。
解决方法:
- 固定长度。
- 分隔符。
- 长度前缀。
- 自描述协议。
长度前缀示例:
[4 字节长度][消息体][4 字节长度][消息体]读取时必须处理半包,不要假设一次 Read 返回完整消息。
17.3 net.Conn 需要设置哪些超时?
根据协议设置:
SetReadDeadlineSetWriteDeadlineSetDeadline
否则对端不发送或网络异常时,goroutine 可能永久阻塞。长连接需要按心跳、空闲策略动态刷新 deadline。
17.4 HTTP/1.1、HTTP/2、HTTP/3 的核心区别?
| 协议 | 传输基础 | 主要特点 |
|---|---|---|
| HTTP/1.1 | TCP | 持久连接、管线化实践受限,多连接并发 |
| HTTP/2 | TCP | 二进制分帧、单连接多路复用、头部压缩 |
| HTTP/3 | QUIC/UDP | 传输层多路复用,降低 TCP 队头阻塞影响 |
单个 HTTP/2 TCP 连接丢包时仍会影响该连接上的数据传输;HTTP/3 的 QUIC 流之间隔离更好。
17.5 Go HTTP Client 为什么应该复用?
http.Client 和其 Transport 管理连接池,可以安全并发使用。每次请求创建新 Client/Transport 会损失连接复用,并可能造成连接、端口和 goroutine 压力。复用可减少 TCP/TLS 握手与慢启动成本,还要配合空闲超时、最大连接数和服务端策略。
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
},
}参数必须根据目标主机数量、并发和延迟调优。
17.6 只设置 http.Client.Timeout 够吗?
它是整个请求生命周期的总超时,适合许多普通调用,但精细系统还会考虑:
- 建连超时。
- TLS 握手超时。
- 等待响应头超时。
- Expect Continue 超时。
- 单次请求 context deadline。
- 读取响应体时限和最大体积。
既不能完全不设超时,也不能让各层超时相互矛盾。
17.7 为什么必须关闭 HTTP Response Body?
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()关闭 Body 让资源及时释放。若希望连接复用,通常还要把响应体读取到 EOF;但对于巨大或无用响应体,应限制读取量或直接放弃复用,不能无限读。
需要注意:非 2xx 状态通常仍然是成功的 HTTP 往返,Do 不一定返回 error,业务必须检查 StatusCode。
17.8 HTTP Server 应设置哪些超时?
不要只用默认 http.ListenAndServe 暴露到不可信网络。通常需要评估:
ReadHeaderTimeoutReadTimeoutWriteTimeoutIdleTimeout- 最大 Header 大小
不同类型服务,尤其流式响应和 WebSocket,对 WriteTimeout 的要求不同。
17.9 如何优雅关闭 HTTP Server?
流程:
- 收到 SIGTERM/SIGINT。
- 停止接收新流量或先从服务发现摘除。
- 调用
Server.Shutdown(ctx)。 - 等待在途请求、后台 worker 和资源清理。
- 超时后执行兜底退出。
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown: %v", err)
}Shutdown 不会自动等待你自己启动的所有后台 goroutine,需要统一生命周期管理。
17.10 WebSocket 的关键注意点是什么?
- 它是长连接,需要读写 deadline 和心跳。
- 通常一个连接只能有受控的单读者和单写者,具体遵循所用库约束。
- 写操作应序列化,避免并发写破坏帧。
- 设置消息大小上限。
- 处理对端关闭帧。
- 建立发送队列上限和慢客户端踢除策略。
- 连接断开时取消关联业务 goroutine。
18. 序列化:JSON 与 Protobuf
18.1 encoding/json 如何处理导出字段?
默认只处理导出字段。未导出字段即使有 tag 也不会被正常编码。
type User struct {
ID int64 `json:"id"`
name string `json:"name"` // 不会导出
}18.2 omitempty 有什么坑?
它会省略被认为是空值的字段,例如 false、0、空字符串、nil 指针、长度为 0 的部分集合。
如果业务必须区分:
- 未提供。
- 明确提供 0/false。
可以使用指针、自定义可选类型或协议层 presence 语义。
18.3 JSON 数字为什么可能丢精度?
解码到 any 时,数字默认常表示为 float64。大于 JavaScript 安全整数范围的 int64 在跨语言链路中容易丢精度。
方案:
- 解码到明确结构体字段
int64。 Decoder.UseNumber()。- 对前端协议把超大整数编码成字符串。
- 使用 Protobuf 等有明确整数类型的协议。
18.4 Decoder 和 Unmarshal 如何选择?
json.Unmarshal:已有完整字节切片。json.Decoder:从流读取,可以连续解码多个值。
严格 API 可使用 DisallowUnknownFields,但要考虑前后端兼容策略。
对不可信输入应限制 body 大小,避免先无上限 ReadAll。
18.5 Protobuf 的优势是什么?
- 二进制体积通常较小。
- 编解码速度通常较快。
- 有 schema 和代码生成。
- 支持跨语言。
- 字段编号支持演进。
- 与 gRPC 配合自然。
缺点:
- 人眼不可直接阅读。
- 需要生成代码和 schema 管理。
- 调试工具链复杂一些。
18.6 Protobuf 如何保证兼容性?
核心规则:
- 已发布字段号不能改变语义。
- 删除字段后应保留编号和名称,避免未来复用。
- 新增字段通常是兼容的。
- 不要随意修改字段类型。
- enum 的 0 值应表示 unknown/unspecified。
- 区分默认值和“未设置”时使用合适的 presence 能力。
18.7 为什么不能把数据库模型直接当 API 模型?
- 数据库结构和外部协议演进节奏不同。
- 容易泄露内部字段。
- 可空语义不同。
- API 需要版本兼容,DB 需要存储优化。
- 业务领域对象可能需要额外不变量。
建议区分:
- transport DTO / Proto message。
- domain model。
- persistence model。
是否严格三层映射要根据项目规模权衡。
继续阅读
- 排错
- 排错
- 排错