MENU

Catalog

Go 网络编程入门到实践:`net.Conn` 和 `net.Listener`

April 5, 2026 • Go

如果你有 Java 后端经验,那么你一定接触过:

  • ServerSocket:服务端监听端口
  • Socket:客户端和服务端之间的连接
  • InputStream/OutputStream:读写数据

在 Go 里,与之最接近的就是:

  • net.Listener:负责监听,类似 Java 的 ServerSocket
  • net.Conn:代表一条连接,类似 Java 的 Socket

但 Go 的网络编程风格和 Java 有一些重要区别:

  1. 接口优先:Go 的 net.Connnet.Listener 是接口,不是你直接 new 出来的具体类。
  2. goroutine 天然适合一连接一协程:Go 处理并发连接比 Java 线程模型更轻量。
  3. 超时控制更直接:通过 SetDeadline 等方法控制读写超时。
  4. 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 的:

  • Socket
  • socket.getInputStream() + socket.getOutputStream()
  • socket.close()
  • socket.setSoTimeout()(但 Go 更灵活)

职责总结

  • 超时控制
  • 关闭连接

1.3 ListenerConn 的关系

可以理解成两阶段:

  1. Listener 负责“门口接客”
  2. 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

适用于什么场景?

  • 学习 ListenerConn 的基本关系
  • 做一个连接探测服务
  • 验证端口监听是否正常

要注意什么?

  1. Accept() 是阻塞的

    • 没有连接时会一直等
  2. 监听器要关闭

    • defer listener.Close()
  3. 连接也要关闭

    • 否则会泄漏文件描述符

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 客户端
  • 内部服务调用
  • 自定义协议客户端

要注意什么?

  1. Dial 可能失败:

    • 目标地址不可达
    • 端口没开
    • 服务端拒绝连接
  2. 连接成功后一定要 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 的差异

  1. goroutine 更轻量

    • Java 里一连接一线程成本较高
    • Go 里一连接一 goroutine 很常见
  2. 接口抽象更强

    • Java 通常直接用 ServerSocket/Socket
    • Go 常面向 net.Listener/net.Conn 编程
  3. 错误处理风格不同

    • Go 大量 if err != nil
    • 需要显式处理 Accept/Read/Write 的错误

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()
  • connhandleConndefer 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 正确思路:定义消息边界

常见方案:

  1. 固定长度协议
  2. 分隔符协议
  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') 会持续读取,直到读到分隔符。


适用于什么场景?

  • 文本协议
  • 日志流协议
  • 简单命令协议

要注意什么?

  1. 消息体里不能随便包含换行符,除非做转义
  2. 超长行要防止内存膨胀
  3. 二进制协议不适合这种方式

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 场景

要注意什么?

  1. 长度一定要校验

    • 防止恶意请求传一个超大长度导致内存爆炸
  2. 协议要统一大小端
  3. 不能盲信客户端输入

例如加长度保护:

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 协议服务骨架
  • 连接管理学习样例

要注意什么?

  1. buf 是复用的,写业务时不要把 buf[:n] 直接交给异步任务长期持有
  2. 如果协议不是“收多少回多少”,要自己做消息 framing
  3. 这里只是演示,生产里还要加日志、限流、监控

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 8080

13.2 客户端连接不上

常见原因:

  • 服务端没启动
  • 地址写错
  • 端口写错
  • 防火墙拦截
  • 容器/宿主机端口未映射

先确认:

telnet 127.0.0.1 8080
nc -vz 127.0.0.1 8080

13.3 Read 一直阻塞

常见原因:

  • 对方根本没发数据
  • 协议边界没处理好
  • 等待换行符但对方没发 \n
  • 长度字段读错了

解决思路:

  1. 先加 SetReadDeadline
  2. 打印每次收到的字节数
  3. 用抓包工具看原始数据
  4. 检查协议编码/解码是否一致

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 客户端

  • 优先使用带超时的 DialTimeoutDialer
  • 成功连接后 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 有输入流输出流
  • ServerSocket accept 一个 socket

Go 则更统一:

  • Conn 本身就能 Read/Write/Close
  • 它实现的是一组标准接口
  • 很多工具都能直接围绕 io.Reader/io.Writer 工作

这会让 Go 网络代码和文件、缓冲区、压缩流等处理方式非常一致。


16.2 从“线程昂贵”切换到“goroutine 默认可用”

Java 里你会谨慎考虑一连接一线程。
Go 里通常可以先用“一连接一 goroutine”,然后再根据压测优化。


16.3 从“面向一次读取”切换到“面向协议边界”

这是最关键的:

不是 Read() 完就得到一条请求,
而是要先定义“怎么从字节流中切出一条完整消息”。

谁掌握了这点,谁才算真正入门 TCP 编程。


十七、总结

net.Listenernet.Conn 是 Go 网络编程最基础也最重要的两个概念:

  • net.Listener:负责监听端口、接受新连接
    类比 Java 的 ServerSocket
  • net.Conn:代表一条具体连接,负责读写数据、设置超时、关闭连接
    类比 Java 的 Socket

你需要重点掌握的核心点是:

  1. 服务端流程

    • net.Listen
    • 循环 Accept
    • 每个连接交给 goroutine 处理
  2. 客户端流程

    • net.Dial / net.DialTimeout
    • 获取 Conn
    • 进行读写
  3. 正确读写

    • Read 读的是字节流,不是消息
    • Write 不保证和对端 Read 一一对应
    • 使用协议边界解决半包/粘包
  4. 工程上必须做的事

    • defer Close()
    • 设置超时
    • 正确处理错误
    • 限制并发
    • 防止长度攻击和资源泄漏

十八、进一步学习建议

如果你已经理解了本文内容,建议继续往下学:

第一阶段:继续夯实基础

  • bufio.Reader / bufio.Writer
  • io.ReadFull
  • encoding/binary
  • context 在网络请求中的使用

第二阶段:进入工程实践

  • 自定义 TCP 协议设计
  • 长连接心跳机制
  • 连接池
  • 优雅关闭(graceful shutdown)
  • TLS:crypto/tls

第三阶段:理解更高层抽象

  • net/http 的底层思路
  • Go 如何基于 net.Conn 封装 HTTP/RPC
  • gRPC、WebSocket、本地 RPC 框架

如果你愿意,我下一步还可以继续为你写一篇配套文章,例如:

  1. 《Go TCP 服务器实战:从 net.Conn 到自定义协议》
  2. 《Go 中如何优雅关闭 TCP 服务》
  3. 《Go 里半包/粘包的完整解决方案》
  4. 《从 Java NIO 到 Go netpoll:网络模型对比》

如果你想,我也可以直接继续写第 2 篇。