Skip to content

Go 后端面试复盘:语言、并发与服务实践 · 17. TCP、HTTP 与网络编程至18. 序列化:JSON 与 Protobuf ​

返回分篇目录

前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。

17. TCP、HTTP 与网络编程 ​

17.1 TCP 是什么? ​

TCP 是面向连接、可靠、有序的字节流协议。它提供:

  • 序号与确认。
  • 校验与超时重传。
  • 流量控制。
  • 拥塞控制。
  • 按序交付。

TCP 不保留应用消息边界。一次 Write 不一定对应一次 Read。

17.2 什么是 TCP 粘包和拆包? ​

“粘包”本质是应用层误以为 TCP 有消息边界。

解决方法:

  • 固定长度。
  • 分隔符。
  • 长度前缀。
  • 自描述协议。

长度前缀示例:

text
[4 字节长度][消息体][4 字节长度][消息体]

读取时必须处理半包,不要假设一次 Read 返回完整消息。

17.3 net.Conn 需要设置哪些超时? ​

根据协议设置:

  • SetReadDeadline
  • SetWriteDeadline
  • SetDeadline

否则对端不发送或网络异常时,goroutine 可能永久阻塞。长连接需要按心跳、空闲策略动态刷新 deadline。

17.4 HTTP/1.1、HTTP/2、HTTP/3 的核心区别? ​

协议传输基础主要特点
HTTP/1.1TCP持久连接、管线化实践受限,多连接并发
HTTP/2TCP二进制分帧、单连接多路复用、头部压缩
HTTP/3QUIC/UDP传输层多路复用,降低 TCP 队头阻塞影响

单个 HTTP/2 TCP 连接丢包时仍会影响该连接上的数据传输;HTTP/3 的 QUIC 流之间隔离更好。

17.5 Go HTTP Client 为什么应该复用? ​

http.Client 和其 Transport 管理连接池,可以安全并发使用。每次请求创建新 Client/Transport 会损失连接复用,并可能造成连接、端口和 goroutine 压力。复用可减少 TCP/TLS 握手与慢启动成本,还要配合空闲超时、最大连接数和服务端策略。

go
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? ​

go
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 暴露到不可信网络。通常需要评估:

  • ReadHeaderTimeout
  • ReadTimeout
  • WriteTimeout
  • IdleTimeout
  • 最大 Header 大小

不同类型服务,尤其流式响应和 WebSocket,对 WriteTimeout 的要求不同。

17.9 如何优雅关闭 HTTP Server? ​

流程:

  1. 收到 SIGTERM/SIGINT。
  2. 停止接收新流量或先从服务发现摘除。
  3. 调用 Server.Shutdown(ctx)。
  4. 等待在途请求、后台 worker 和资源清理。
  5. 超时后执行兜底退出。
go
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 也不会被正常编码。

go
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。

是否严格三层映射要根据项目规模权衡。


继续阅读