如果你有 Java 后端经验,那么你一定接触过:
ServerSocket:服务端监听端口Socket:客户端和服务端之间的连接InputStream/OutputStream:读写数据
在 Go 里,与之最接近的就是:
net.Listener:负责监听,类似 Java 的ServerSocketnet.Conn:代表一条连接,类似 Java 的Socket
但 Go 的网络编程风格和 Java 有一些重要区别:
- 接口优先:Go 的
net.Conn、net.Listener是接口,不是你直接 new 出来的具体类。 - goroutine 天然适合一连接一协程:Go 处理并发连接比 Java 线程模型更轻量。
- 超时控制更直接:通过
SetDeadline等方法控制读写超时。 - I/O 模型更统一:
Conn本身就实现了io.Reader/io.Writer/io.Closer,使用上非常自然。
一、先建立整体认知
1.1 net.Listener 是什么?
net.Listener 的职责是:
- 在某个网络地址上监听,比如
:8080 - 等待客户端连接
- 每来一个连接,返回一个
net.Conn
你可以把它理解为:
- Java 里的
ServerSocket - 但在 Go 里它是一个监听器接口
核心方法只有几个:
type Listener interface {
Accept() (Conn, error)
Close() error
Addr() Addr
}职责总结
- 监听端口
- 接收新连接
- 关闭监听器
1.2 net.Conn 是什么?
net.Conn 表示一条已经建立好的网络连接。
它的职责是:
- 从连接中读取数据
- 向连接中写入数据
- 设置读写超时
- 关闭连接
接口定义核心如下:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}你可以把它类比为 Java 的什么?
很像 Java 的:
Socketsocket.getInputStream()+socket.getOutputStream()socket.close()socket.setSoTimeout()(但 Go 更灵活)
职责总结
- 读
- 写
- 超时控制
- 关闭连接
1.3 Listener 和 Conn 的关系
可以理解成两阶段:
Listener负责“门口接客”Conn负责“和某个客人持续对话”
1.4 一张图看懂服务端流程
flowchart TD
A[net.Listen] --> B[得到 net.Listener]
B --> C[Accept 阻塞等待连接]
C --> D[返回 net.Conn]
D --> E[读取请求 Read]
E --> F[处理业务]
F --> G[写回响应 Write]
G --> H[Close 关闭连接]二、最基础的用法
2.1 服务端:监听并接受连接
先看一个最小可运行的 TCP 服务端。
package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("server listening on :8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
fmt.Println("new connection from", conn.RemoteAddr())
conn.Close()
}
}这段代码在做什么?
net.Listen("tcp", ":8080"):监听本机 8080 端口listener.Accept():阻塞等待客户端连接- 有客户端连上来后,返回一个
conn - 打印远端地址,然后关闭连接
为什么这么写?
这是 Go TCP 服务端最基本的骨架:
- 先监听
- 然后循环
Accept - 每个连接都会变成一个
net.Conn
适用于什么场景?
- 学习
Listener和Conn的基本关系 - 做一个连接探测服务
- 验证端口监听是否正常
要注意什么?
Accept()是阻塞的- 没有连接时会一直等
监听器要关闭
defer listener.Close()
连接也要关闭
- 否则会泄漏文件描述符
2.2 客户端:建立连接
对应的客户端代码如下:
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Println("connected to", conn.RemoteAddr())
}这段代码在做什么?
net.Dial("tcp", "127.0.0.1:8080"):主动连接服务端- 成功后得到一个
net.Conn - 打印远端地址
为什么这么写?
因为 Go 客户端建立 TCP 连接的最常见入口就是 net.Dial
适用于什么场景?
- TCP 客户端
- 内部服务调用
- 自定义协议客户端
要注意什么?
Dial可能失败:- 目标地址不可达
- 端口没开
- 服务端拒绝连接
- 连接成功后一定要
Close()
三、read / write / close 的正确使用
3.1 最简单的数据收发
服务端
package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read error:", err)
return
}
fmt.Println("received:", string(buf[:n]))
_, err = conn.Write([]byte("hello client"))
if err != nil {
fmt.Println("write error:", err)
return
}
}客户端
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
_, err = conn.Write([]byte("hello server"))
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Println("response:", string(buf[:n]))
}这段代码在做什么?
- 客户端发
"hello server" - 服务端读取后打印
- 服务端回写
"hello client" - 客户端读取响应并打印
为什么这么写?
这是最小“请求-响应”模型。
适用于什么场景?
- 学习最基础的 TCP 读写
- 实现非常简单的自定义协议
要注意什么?
这里虽然简单,但真实工程里不能假设一次 Read 就能读到完整请求,也不能假设一次 Write 就一定发完全部数据。这是 TCP 最重要的坑之一,后面会重点讲。
3.2 Read 到底意味着什么?
Java 开发者经常容易误解的一点:
conn.Read(buf) 不等于“读一条消息”,它只是“从 TCP 字节流里读一段当前可读的数据”。TCP 是字节流协议,没有消息边界。
所以:
- 一次
Write("hello") - 对方可能一次
Read读到"he" - 再下一次
Read才读到"llo"
也可能连续两次写:
Write("hello")Write("world")
对方一次 Read 就读到 "helloworld"
这就是常说的:
- 半包:一条消息没读完整
- 粘包:多条消息读到一起了
3.3 正确理解 Close
conn.Close() 表示关闭整个连接。
常见效果:
- 本端不能再读写
- 对端再读时可能会收到 EOF 或错误
对于初学者来说,一个原则够用了:
谁创建连接,谁负责关闭;服务端处理完一个连接后一般要关闭;客户端不用时要关闭。
四、服务端监听与 Accept 流程
4.1 标准服务端模型
Go 中最经典的 TCP 服务端结构:
listener, err := net.Listen("tcp", ":8080")
if err != nil {
return err
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
// 记录日志,继续或退出
continue
}
go handleConn(conn)
}4.2 这和 Java 有什么相似与不同?
相似点
和 Java 经典写法很像:
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> handle(socket)).start();
}Go 的差异
goroutine 更轻量
- Java 里一连接一线程成本较高
- Go 里一连接一 goroutine 很常见
接口抽象更强
- Java 通常直接用
ServerSocket/Socket - Go 常面向
net.Listener/net.Conn编程
- Java 通常直接用
错误处理风格不同
- Go 大量
if err != nil - 需要显式处理
Accept/Read/Write的错误
- Go 大量
4.3 Accept 出错要怎么处理?
很多人会写成:
conn, err := listener.Accept()
if err != nil {
panic(err)
}这在 demo 中可以,在生产里不合适。
更合理的是:
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConn(conn)
}为什么?
Accept 出错不一定意味着服务彻底不可用,可能只是暂时问题:
- 文件描述符耗尽
- 短暂网络异常
- listener 被关闭
要根据错误类型决定:
- 可恢复:记录日志,继续
- 不可恢复:退出循环
五、客户端建立连接流程
5.1 net.Dial
最常用:
conn, err := net.Dial("tcp", "127.0.0.1:8080")含义:
- 使用 TCP
- 连接到目标地址
- 成功返回
net.Conn
5.2 带超时的连接:net.DialTimeout
工程上更推荐:
conn, err := net.DialTimeout("tcp", "127.0.0.1:8080", 3*time.Second)
if err != nil {
return
}
defer conn.Close()为什么要这样写?
如果网络异常,裸 Dial 可能等很久。
在生产环境里,连接超时必须可控。
这和 Java 里的:
socket.connect(address, timeout)很像。
5.3 更灵活的方式:net.Dialer
dialer := net.Dialer{
Timeout: 3 * time.Second,
}
conn, err := dialer.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()适用于什么场景?
- 要配置连接超时
- 要自定义本地地址、keepalive 等参数
- 要在工程里统一封装连接逻辑
六、超时控制:非常重要
Go 网络编程里,超时控制是避免连接卡死的关键。
6.1 读超时
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf)意思是:
- 5 秒内读不到数据,就超时返回错误
6.2 写超时
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
_, err := conn.Write(data)意思是:
- 5 秒内写不出去,就超时返回错误
6.3 总超时
conn.SetDeadline(time.Now().Add(5 * time.Second))表示:
- 对之后的读写都生效
- 到时间后读写都会超时
6.4 示例:防止连接一直阻塞
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read error:", err)
return
}
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
_, err = conn.Write([]byte("ok:" + string(buf[:n])))
if err != nil {
fmt.Println("write error:", err)
return
}
}这段代码在做什么?
- 读之前设置读超时
- 写之前设置写超时
- 避免恶意客户端或网络异常把协程卡死
为什么这么写?
如果不加超时,下面这些情况会让 goroutine 长时间挂住:
- 客户端连上后不发数据
- 对端接收缓慢,导致写阻塞
- 网络异常但连接没及时感知
要注意什么?
Deadline 是绝对时间点,不是持续时长。
如果你想“每次操作都给 10 秒”,要每次读写前重新设置。
七、并发处理:Go 的强项
7.1 一连接一 goroutine
最常见写法:
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConn(conn)
}为什么这是 Go 的常规姿势?
因为 goroutine 非常轻量,Go runtime 会帮你调度。
对于 Java 开发者来说,可以理解为:
- Java 传统上“一连接一线程”成本高
- Go 中“一连接一 goroutine”通常是默认选项
7.2 但并发不是无限的
这不代表你可以无限开 goroutine。工程上仍要考虑:
- 连接数上限
- 单机内存
- 文件描述符限制
- 慢连接攻击
例如可以加一个信号量限制并发:
var sem = make(chan struct{}, 1000)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
sem <- struct{}{}
go func(c net.Conn) {
defer func() { <-sem }()
handleConn(c)
}(conn)
}这段代码在做什么?
- 最多允许 1000 个连接处理协程同时运行
- 超过后会阻塞,形成背压
适用于什么场景?
- 面向公网服务
- 要防止突发流量压垮机器
- 要保护下游资源
八、资源释放:不要让连接泄漏
8.1 最常见原则
服务端
listener创建后要defer listener.Close()conn在handleConn里defer conn.Close()
客户端
Dial成功后defer conn.Close()
8.2 一个正确示例
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
fmt.Println("connection closed or read error:", err)
return
}
_, err = conn.Write(buf[:n])
if err != nil {
fmt.Println("write error:", err)
return
}
}
}为什么这样写?
- 无论中途哪一步 return,连接都会被释放
- 避免文件描述符泄漏
Java 开发者可以怎么理解?
类似:
try (Socket socket = ...) {
...
}Go 没有 try-with-resources,但 defer conn.Close() 就是最接近的习惯用法。
九、半包/粘包问题:TCP 编程的核心难点
这是最重要的实践问题之一。
9.1 为什么会有半包/粘包?
因为 TCP 是字节流,不是消息队列。
它只保证:
- 数据顺序
- 数据可靠
但不保证消息边界。
所以应用层必须自己定义协议边界。
9.2 错误示范:以为一次 Read 就是一条消息
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
msg := string(buf[:n])这只适合非常简单的 demo,不适合正式协议。
9.3 正确思路:定义消息边界
常见方案:
- 固定长度协议
- 分隔符协议
- 长度字段 + 消息体(最常用)
9.4 方案一:按换行符分隔
比如每条消息都以 \n 结尾。
服务端
package main
import (
"bufio"
"fmt"
"net"
)
func handleConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("read error:", err)
return
}
fmt.Print("received:", line)
_, err = conn.Write([]byte("ok\n"))
if err != nil {
fmt.Println("write error:", err)
return
}
}
}这段代码在做什么?
- 用
bufio.Reader包装连接 - 每次按
\n读取一整条消息 - 解决半包/粘包
为什么这么写?
因为 ReadString('\n') 会持续读取,直到读到分隔符。
适用于什么场景?
- 文本协议
- 日志流协议
- 简单命令协议
要注意什么?
- 消息体里不能随便包含换行符,除非做转义
- 超长行要防止内存膨胀
- 二进制协议不适合这种方式
9.5 方案二:长度字段 + 消息体
工程里最常见。
协议格式示例:
- 前 4 个字节:消息体长度
- 后面 N 个字节:消息体内容
读取示例
package main
import (
"encoding/binary"
"fmt"
"io"
"net"
)
func handleConn(conn net.Conn) {
defer conn.Close()
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
fmt.Println("read header error:", err)
return
}
length := binary.BigEndian.Uint32(header)
body := make([]byte, length)
if _, err := io.ReadFull(conn, body); err != nil {
fmt.Println("read body error:", err)
return
}
fmt.Println("received:", string(body))
}这段代码在做什么?
- 先精确读取 4 字节长度头
- 再按长度精确读取消息体
io.ReadFull保证“没读满就继续读”
为什么这么写?
这是解决半包问题的关键方式。
比起自己循环 Read 拼装,io.ReadFull 更稳妥。
适用于什么场景?
- 二进制协议
- RPC 协议
- 网关、IM、游戏服务等高频 TCP 场景
要注意什么?
长度一定要校验
- 防止恶意请求传一个超大长度导致内存爆炸
- 协议要统一大小端
- 不能盲信客户端输入
例如加长度保护:
if length > 1024*1024 {
fmt.Println("message too large")
return
}十、Read / Write 的几个关键细节
10.1 Read 返回 io.EOF 是什么意思?
通常表示:
- 对端已经关闭连接
- 已经没有更多数据可读
例如:
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("peer closed")
} else {
fmt.Println("read error:", err)
}
return
}10.2 Write 不一定一次写完
这一点很多人忽略。
安全起见,工程里对于大块数据,最好自己确保完整写出:
func writeFull(conn net.Conn, data []byte) error {
for len(data) > 0 {
n, err := conn.Write(data)
if err != nil {
return err
}
data = data[n:]
}
return nil
}为什么这么写?
因为 Write 返回的 n 可能小于 len(data)。
虽然在很多简单 TCP 场景下你不容易遇到,但正确认知非常重要。
10.3 不要忽略返回值 n
错误写法:
conn.Read(buf)
fmt.Println(string(buf))问题:
- buf 里可能有脏数据
- 只有前
n个字节是有效数据
正确写法:
n, err := conn.Read(buf)
if err != nil {
return
}
fmt.Println(string(buf[:n]))十一、常见误区与踩坑点
11.1 误区一:一次 Read 就是一条完整消息
错。
TCP 没有消息边界。必须自己做协议拆包。
11.2 误区二:一次 Write 对方就会一次 Read 收到
错。
发送和接收次数没有一一对应关系。
11.3 误区三:不设置超时也没事
错。
生产里很容易被慢连接、僵尸连接拖死。
11.4 误区四:Accept 出错就 panic
错。
生产服务应当分类处理错误,尽量保证服务可恢复。
11.5 误区五:忘记关闭连接
错。
会造成:
- 文件描述符泄漏
- goroutine 泄漏
- 内存上涨
- 服务越来越慢
11.6 误区六:多个 goroutine 同时随意读同一个连接
通常不推荐。
虽然并发写在某些情况下可行,但工程上最好遵循:
- 一个 goroutine 负责读
- 一个 goroutine 负责写
- 或者统一串行处理
否则协议状态会变得很难维护。
十二、工程实践:一个简单 Echo Server
下面写一个稍微像样一点的回声服务,带:
- 并发处理
- 读超时
- 正确资源释放
- EOF 处理
12.1 服务端代码
package main
import (
"fmt"
"io"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("echo server listening on :8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
fmt.Println("client connected:", conn.RemoteAddr())
buf := make([]byte, 1024)
for {
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("client disconnected:", conn.RemoteAddr())
} else {
fmt.Println("read error:", err)
}
return
}
msg := buf[:n]
fmt.Printf("received from %s: %s\n", conn.RemoteAddr(), string(msg))
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
_, err = conn.Write(msg)
if err != nil {
fmt.Println("write error:", err)
return
}
}
}这段代码在做什么?
- 服务端监听 8080
- 每接入一个客户端,启动一个 goroutine
- 循环读取客户端消息
- 把原消息原样写回去
为什么这么写?
这是 TCP 服务端最常见的模板之一:
Accept循环负责接入handleConn负责单连接生命周期- 每次读写前都设置超时
defer conn.Close()确保连接释放
适用于什么场景?
- Echo 服务
- 自定义 TCP 协议服务骨架
- 连接管理学习样例
要注意什么?
buf是复用的,写业务时不要把buf[:n]直接交给异步任务长期持有- 如果协议不是“收多少回多少”,要自己做消息 framing
- 这里只是演示,生产里还要加日志、限流、监控
12.2 客户端代码
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
input := bufio.NewReader(os.Stdin)
for {
fmt.Print("input: ")
line, err := input.ReadString('\n')
if err != nil {
panic(err)
}
_, err = conn.Write([]byte(line))
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Println("echo:", string(buf[:n]))
}
}十三、排错思路:连不上、读不到、写失败怎么办?
这是工程里最有价值的部分之一。
13.1 服务端启动失败
看 net.Listen 报错:
- 端口被占用
- 权限不足
- 地址不合法
排查:
lsof -i :8080
netstat -an | grep 808013.2 客户端连接不上
常见原因:
- 服务端没启动
- 地址写错
- 端口写错
- 防火墙拦截
- 容器/宿主机端口未映射
先确认:
telnet 127.0.0.1 8080
nc -vz 127.0.0.1 808013.3 Read 一直阻塞
常见原因:
- 对方根本没发数据
- 协议边界没处理好
- 等待换行符但对方没发
\n - 长度字段读错了
解决思路:
- 先加
SetReadDeadline - 打印每次收到的字节数
- 用抓包工具看原始数据
- 检查协议编码/解码是否一致
13.4 Write 失败
常见原因:
- 对端已经关闭
- 网络中断
- 写超时
- 消息过大导致缓冲拥塞
处理方式:
- 记录对端地址
- 记录写入长度
- 区分临时错误和连接关闭错误
13.5 goroutine 越来越多
通常意味着:
- 连接没有正确关闭
Read永久阻塞- 后台任务未退出
排查思路:
- 看 goroutine profile
- 看连接数
- 看是否设置了 deadline
- 检查是否所有错误路径都 return/close
十四、最佳实践清单
给你一份工程上很实用的 checklist。
14.1 服务端
- 用
net.Listen创建监听器 defer listener.Close()for { Accept }持续接收连接- 每个连接单独处理,通常
go handleConn(conn) handleConn开头就defer conn.Close()- 对读写设置超时
- 设计清晰的协议边界
- 校验消息长度,防止内存攻击
- 不要忽略
Read/Write返回值 - 记录关键日志:远端地址、错误、耗时、字节数
14.2 客户端
- 优先使用带超时的
DialTimeout或Dialer - 成功连接后
defer conn.Close() - 如果是长连接,要处理重连策略
- 写请求后不要盲等,要有读超时
- 明确请求和响应的协议边界
14.3 协议设计
- 文本协议可用换行分隔
- 二进制协议优先用“长度头 + body”
- 长度字段必须有限制
- 明确编码格式:UTF-8 / JSON / protobuf / 自定义二进制
- 提前考虑版本兼容
十五、连接建立与数据读写流程图
15.1 连接建立流程
sequenceDiagram
participant Client
participant ServerListener
participant ServerConn
Client->>ServerListener: Dial 发起连接
ServerListener->>ServerListener: Accept 阻塞等待
ServerListener-->>ServerConn: 返回一个新的 net.Conn
Client-->>Client: 获得 net.Conn
ServerConn-->>ServerConn: 开始读写15.2 数据读写流程
sequenceDiagram
participant ClientConn
participant ServerConn
ClientConn->>ServerConn: Write(bytes)
ServerConn->>ServerConn: Read(buf) 读取字节流
ServerConn->>ServerConn: 解析协议边界
ServerConn->>ServerConn: 处理业务
ServerConn->>ClientConn: Write(response)
ClientConn->>ClientConn: Read(buf)十六、从 Java 到 Go,你最需要转换的思维
如果你是 Java 后端工程师,学习这里时最重要的心智转换有三点:
16.1 从“对象 API”切换到“接口 + io 风格”
Java 常想:
Socket有输入流输出流ServerSocketaccept 一个 socket
Go 则更统一:
Conn本身就能Read/Write/Close- 它实现的是一组标准接口
- 很多工具都能直接围绕
io.Reader/io.Writer工作
这会让 Go 网络代码和文件、缓冲区、压缩流等处理方式非常一致。
16.2 从“线程昂贵”切换到“goroutine 默认可用”
Java 里你会谨慎考虑一连接一线程。
Go 里通常可以先用“一连接一 goroutine”,然后再根据压测优化。
16.3 从“面向一次读取”切换到“面向协议边界”
这是最关键的:
不是Read()完就得到一条请求,
而是要先定义“怎么从字节流中切出一条完整消息”。
谁掌握了这点,谁才算真正入门 TCP 编程。
十七、总结
net.Listener 和 net.Conn 是 Go 网络编程最基础也最重要的两个概念:
net.Listener:负责监听端口、接受新连接
类比 Java 的ServerSocketnet.Conn:代表一条具体连接,负责读写数据、设置超时、关闭连接
类比 Java 的Socket
你需要重点掌握的核心点是:
服务端流程
net.Listen- 循环
Accept - 每个连接交给 goroutine 处理
客户端流程
net.Dial/net.DialTimeout- 获取
Conn - 进行读写
正确读写
Read读的是字节流,不是消息Write不保证和对端Read一一对应- 使用协议边界解决半包/粘包
工程上必须做的事
defer Close()- 设置超时
- 正确处理错误
- 限制并发
- 防止长度攻击和资源泄漏
十八、进一步学习建议
如果你已经理解了本文内容,建议继续往下学:
第一阶段:继续夯实基础
bufio.Reader/bufio.Writerio.ReadFullencoding/binarycontext在网络请求中的使用
第二阶段:进入工程实践
- 自定义 TCP 协议设计
- 长连接心跳机制
- 连接池
- 优雅关闭(graceful shutdown)
- TLS:
crypto/tls
第三阶段:理解更高层抽象
net/http的底层思路- Go 如何基于
net.Conn封装 HTTP/RPC - gRPC、WebSocket、本地 RPC 框架
如果你愿意,我下一步还可以继续为你写一篇配套文章,例如:
- 《Go TCP 服务器实战:从 net.Conn 到自定义协议》
- 《Go 中如何优雅关闭 TCP 服务》
- 《Go 里半包/粘包的完整解决方案》
- 《从 Java NIO 到 Go netpoll:网络模型对比》
如果你想,我也可以直接继续写第 2 篇。