net.Dial 是 Go 中建立 TCP 连接最直接方式,但默认无超时且行为受系统影响;需用 net.Dialer 精确控制超时、KeepAlive 等参数,并妥善处理粘包、重连与资源清理。
net.Dial 建立基础 TCP 连接Go 中最直接的 TCP 客户端创建方式就是调用 net.Dial,它封装了底层 socket 创建、连接等逻辑,返回一个 net.Conn 接口实例。默认使用阻塞模式,连接失败会立即返回 error。
常见错误现象:调用后卡住几秒才返回 dial tcp 127.0.0.1:8080: i/o timeout —— 这是系统级连接超时(通常 30 秒),不是 Go 控制的。
"tcp"(IPv4)或 "tcp4"/"tcp6"
"host:port",不带协议头;"localhost:8080" 和 "127.0.0.1:8080" 行为可能不同(涉及 DNS 解析和 dual-stack)net.Dial 默认行为,应改用 net.DialTimeout 或更灵活的 net.Dialer
conn, err := net.Dial("tcp", "127.0.0.1:8080", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
, = conn.Write([]byte("HELLO\n"))
buf := make([]byte, 128)
n, _ := conn.Read(buf)
log.Printf("received: %s", buf[:n])
net.Dialer 精确控制连接行为当需要设置超时、绑定本地地址、禁用

net.Dialer 是必选项。它把连接参数从函数参数中解耦出来,也便于测试 mock。
容易踩的坑:Dialer.Timeout 只控制「建立连接阶段」超时,不影响后续读写;KeepAlive 设为 0 会关闭 OS 层心跳,但某些服务端仍可能因中间设备(如 NAT)断连。
Dialer.Timeout:连接建立最大等待时间(推荐设为 3–5 秒)Dialer.KeepAlive:TCP keep-alive 探测间隔(如 30 * time.Second),设为 0 则禁用Dialer.LocalAddr:指定本地绑定地址(例如多网卡场景下选特定出口 IP)Dialer.Resolver:可替换默认 DNS 解析器,用于测试或定制解析逻辑dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
conn, err := dialer.Dial("tcp", "example.com:443")
if err != nil {
log.Fatal(err)
}TCP 连接在生产环境大概率会断开:服务端重启、网络抖动、防火墙超时、对方主动 close。Go 的 net.Conn 一旦关闭或出错,不可恢复,必须新建连接。
关键点:不要在 Read 或 Write 出错后继续使用该 conn;也不要对已关闭连接调用 Close()(会 panic);重连前务必检查 error 是否属于临时性错误(net.ErrClosed、io.EOF、syscall.ECONNREFUSED 等)。
errors.Is(err, io.EOF) 或 errors.Is(err, net.ErrClosed) 判断是否可重试net.OpError + Temporary() == true)和永久错误(如 DNS 解析失败)TCP 是字节流协议,conn.Write() 和 conn.Read() 不保证一次调用对应一“条”业务消息。客户端发 3 次 Write,服务端一次 Read 可能拿到全部数据;也可能一次 Write 被拆成多次 Read 返回 —— 这就是粘包/半包问题。
除非协议本身是「行分隔」(如 HTTP/1.1 的 \r\n)或「定长包」,否则必须自行处理消息边界。常见做法是加长度头(4 字节 big-endian 表示 payload 长度)或特殊分隔符。
Read 会填满传入的 []byte 缓冲区;总是检查返回的 n 值Write 可能只写部分,需循环调用或使用 io.WriteString/bufio.Writer
bufio.Reader + ReadString('\n') 或 ReadBytes('\n') 处理行协议真正难的不是连上,而是连上之后怎么稳住、怎么识别消息、怎么安全清理资源 —— 这些细节不写进日志,就只能靠连接断了再看报错。