Go 语言网络编程实战
wxk1991 Lv5

Go 语言网络编程实战

Go 很适合写网络程序。标准库已经提供 TCP、UDP、HTTP、超时、连接池等能力,很多服务不需要一上来就引入复杂框架。

这篇文章从 TCP echo server 开始,讲几个真实项目里必须注意的点。


一、最小 TCP 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
listener, err := net.Listen("tcp", "127.0.0.1:9000")
if err != nil {
log.Fatal(err)
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
log.Println("accept:", err)
continue
}

go handleConn(conn)
}

每个连接用一个 goroutine 处理,这是 Go 网络编程最常见的写法。


二、处理连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func handleConn(conn net.Conn) {
defer conn.Close()

buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err == io.EOF {
return
}
if err != nil {
log.Println("read:", err)
return
}

if _, err := conn.Write(buf[:n]); err != nil {
log.Println("write:", err)
return
}
}
}

这个服务会把客户端发来的内容原样写回去。看起来简单,但已经包含网络服务的核心:连接、读、写、关闭、错误处理。


三、TCP 是字节流,不是消息流

不要假设一次 Read 就是一条完整消息。TCP 可能半包,也可能粘包。

如果你的协议是一行一条消息,可以用 bufio.Reader

1
2
3
4
5
6
7
8
9
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}

fmt.Print("message:", line)
}

如果是二进制协议,通常会用固定长度头部记录 body 长度。核心原则是:协议必须有明确边界。


四、必须设置超时

没有超时的连接很容易被慢客户端拖住。可以设置读写 deadline:

1
2
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf)

写入也可以设置:

1
2
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
_, err := conn.Write(data)

超时不是可选项。只要面对外部网络,就应该默认考虑。


五、HTTP 客户端不要裸用默认配置

很多人会直接写:

1
resp, err := http.Get(url)

这在 demo 里可以,生产代码最好显式配置超时:

1
2
3
4
5
client := &http.Client{
Timeout: 5 * time.Second,
}

resp, err := client.Get(url)

如果是服务端调用下游接口,应该优先使用带 context 的请求:

1
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

这样请求取消、服务关闭、链路超时都能传递下去。


六、限制并发连接

goroutine 很轻量,但不是免费。连接过多时,要限制并发或使用连接池。

一个简单的限制方式:

1
2
3
4
5
6
7
sem := make(chan struct{}, 1000)

sem <- struct{}{}
go func() {
defer func() { <-sem }()
handleConn(conn)
}()

真实服务还需要配合监控,观察连接数、错误率、请求耗时和超时数量。


七、实践建议

Go 网络编程的重点不在“能不能连上”,而在连接生命周期是否可控。

协议边界、超时、取消、并发限制、错误日志,这几个点处理好,服务就不容易被异常网络拖垮。标准库已经足够强,先把这些基础写稳,比盲目堆框架更重要。