Go语言实战:基于TCP的局域网聊天程序源码解析
简介这是一份面向Go语言初学者与网络编程爱好者的局域网聊天程序源码基于控制台实现适合用来理解TCP通信、并发处理与客户端/服务端交互的基本流程。资源包共6个文件以4个Go源码文件为主另含服务端与客户端各一个可执行文件压缩包约3.72MB结构精简便于直接运行与二次开发。程序已实现基础聊天功能读者可在此基础上扩展私聊、群组、文件传输或消息持久化等特性。目前已有221人学习下载说明其作为练手项目具备一定参考价值。通过阅读服务端与客户端的代码组织方式能够掌握Go中goroutine、channel及网络IO的实际用法适合作为课程设计、自学练手或面试准备的小型实战案例。1. 一个终端窗口就能聊起来的局域网工具你有没有遇到过这种场景会议室里几台电脑要临时传个文件、对个接口结果微信登录不上、网盘限速、U 盘又找不到。这时候如果有个纯命令行的小工具敲一行地址就能连上隔壁工位的机器直接开聊那才叫顺手。这个用 Go 实现的基于控制台的局域网聊天程序干的就是这件事——它不依赖任何中心服务器不依赖外网只要两台机器在同一个网段就能通过 TCP 直连互发消息。整个项目编译出来就是一个可执行文件扔到 Windows、Linux、macOS 上都能跑特别适合内网环境下的快速沟通、调试协作或者拿来当学习 Go 网络编程和并发模型的练手项目。如果你正在找一份能直接跑起来、代码量不大但结构完整的 Go 实战源码这个项目值得你花半小时拆一拆。2. 项目架构拆解从 main 到连接管理的三层结构2.1 整体目录与模块划分这个项目没有引入任何第三方框架标准库就是它的全部依赖。常见的目录组织方式是这样lan-chat/ ├── main.go // 程序入口解析命令行参数启动服务或连接 ├── server.go // 服务端逻辑监听端口、接受连接、广播消息 ├── client.go // 客户端逻辑连接服务端、发送消息、接收消息 ├── message.go // 消息结构体定义与编解码 └── go.mod // 模块定义这种拆法很朴素但职责清晰。main.go只负责判断当前是「开房间」还是「进房间」server.go管连接池和消息分发client.go管单机上的输入输出循环message.go定义传输格式。我一般会把这种结构叫做「三文件骨架」——入口、服务端、客户端各一个文件再加一个协议定义对于千行以内的项目来说足够用也方便二次开发时快速定位。2.2 消息协议设计定长头 变长体局域网聊天最怕的就是粘包。TCP 是字节流你发两条消息对面可能一次全收到也可能分两次收到半条。所以必须自己定一个边界。这个项目采用的是最常见的「长度前缀」方案// message.go package main import ( encoding/binary io ) // Message 定义一条聊天消息 type Message struct { Nickname string // 发送者昵称 Content string // 消息正文 } // Encode 把消息编码成字节流4 字节长度 昵称长度 昵称 内容 func (m *Message) Encode() []byte { nickLen : len(m.Nickname) contentLen : len(m.Content) // 总长度 4(昵称长度字段) nickLen contentLen total : 4 nickLen contentLen buf : make([]byte, 4total) binary.BigEndian.PutUint32(buf[0:4], uint32(total)) binary.BigEndian.PutUint32(buf[4:8], uint32(nickLen)) copy(buf[8:8nickLen], m.Nickname) copy(buf[8nickLen:], m.Content) return buf } // Decode 从连接中读取一条完整消息 func Decode(r io.Reader) (*Message, error) { lenBuf : make([]byte, 4) if _, err : io.ReadFull(r, lenBuf); err ! nil { return nil, err } total : binary.BigEndian.ReadUint32(lenBuf) body : make([]byte, total) if _, err : io.ReadFull(r, body); err ! nil { return nil, err } nickLen : binary.BigEndian.Uint32(body[0:4]) nickname : string(body[4 : 4nickLen]) content : string(body[4nickLen:]) return Message{Nickname: nickname, Content: content}, nil }这里有几个关键点。第一长度字段用BigEndian写入网络字节序统一跨平台不会翻车。第二io.ReadFull保证读满指定字节数才返回避免半包。第三昵称长度单独用一个字段而不是用分隔符去切这样昵称里带空格或冒号也不会解析错。参数上total是消息体总长度不包含最外层那 4 字节nickLen是昵称的字节长度不是字符数中文昵称要注意 UTF-8 编码下 1 个汉字占 3 字节。2.3 服务端连接管理map 读写锁服务端要维护所有在线连接并在收到消息时广播给其他人。Go 里最直接的做法是用map[net.Conn]string存连接和昵称但 map 不是并发安全的多个 goroutine 同时读写会 panic。所以必须加锁// server.go package main import ( fmt net sync ) type Server struct { listener net.Listener clients map[net.Conn]string // 连接 - 昵称 mu sync.RWMutex // 保护 clients 的读写锁 } func NewServer(addr string) (*Server, error) { ln, err : net.Listen(tcp, addr) if err ! nil { return nil, err } return Server{ listener: ln, clients: make(map[net.Conn]string), }, nil } func (s *Server) Start() { for { conn, err : s.listener.Accept() if err ! nil { fmt.Println(accept error:, err) continue } go s.handleConn(conn) // 每个连接一个 goroutine } } func (s *Server) handleConn(conn net.Conn) { defer func() { s.mu.Lock() delete(s.clients, conn) s.mu.Unlock() conn.Close() }() // 第一条消息当作昵称注册 msg, err : Decode(conn) if err ! nil { return } s.mu.Lock() s.clients[conn] msg.Nickname s.mu.Unlock() s.broadcast(fmt.Sprintf([系统] %s 加入了聊天室, msg.Nickname), conn) for { msg, err : Decode(conn) if err ! nil { s.broadcast(fmt.Sprintf([系统] %s 离开了聊天室, s.getNick(conn)), conn) return } s.broadcast(fmt.Sprintf(%s: %s, msg.Nickname, msg.Content), conn) } } func (s *Server) broadcast(text string, exclude net.Conn) { s.mu.RLock() defer s.mu.RUnlock() out : (Message{Nickname: server, Content: text}).Encode() for c : range s.clients { if c exclude { continue } c.Write(out) } } func (s *Server) getNick(conn net.Conn) string { s.mu.RLock() defer s.mu.RUnlock() return s.clients[conn] }这段代码是整个项目的核心。handleConn里每个连接独立一个 goroutine互不阻塞。broadcast用读锁因为只遍历不修改delete和写入用写锁。注意broadcast里对每个连接直接Write如果某个客户端网络卡住这里会阻塞整个广播循环。生产环境常见做法是给每个连接配一个带缓冲的发送 channel由单独的写 goroutine 消费但作为学习项目直接写也能跑只是客户端一多会感觉到延迟。2.4 客户端双 goroutine一个收、一个发客户端要同时处理「用户键盘输入」和「服务端消息到达」两件事单线程做不了所以拆成两个 goroutine// client.go package main import ( bufio fmt net os ) func StartClient(addr, nickname string) error { conn, err : net.Dial(tcp, addr) if err ! nil { return err } defer conn.Close() // 先发昵称完成注册 reg : Message{Nickname: nickname, Content: } if _, err : conn.Write(reg.Encode()); err ! nil { return err } // 收消息 goroutine go func() { for { msg, err : Decode(conn) if err ! nil { fmt.Println(连接已断开) os.Exit(0) } fmt.Printf(\r%s\n , msg.Content) } }() // 主 goroutine 读键盘输入并发送 scanner : bufio.NewScanner(os.Stdin) fmt.Print( ) for scanner.Scan() { text : scanner.Text() if text /quit { return nil } msg : Message{Nickname: nickname, Content: text} if _, err : conn.Write(msg.Encode()); err ! nil { return err } fmt.Print( ) } return nil }收消息的 goroutine 里用\r回到行首再打印是为了不让服务端消息和当前输入行叠在一起。/quit是自定义的退出指令不走网络直接关连接。这里有个小细节bufio.NewScanner默认单行最大 64KB如果粘贴超长文本会被截断需要调scanner.Buffer加大上限。3. 跑起来编译、启动服务端、多端接入的完整步骤3.1 环境准备与编译先确认本机装了 Go 1.18 以上版本然后进入项目目录# 初始化模块如果源码里没有 go.mod go mod init lan-chat # 编译当前平台可执行文件 go build -o lan-chat . # 交叉编译 Windows 版本在 Linux/macOS 上 GOOSwindows GOARCHamd64 go build -o lan-chat.exe .编译完只有一个二进制文件没有任何运行时依赖。GOOS和GOARCH是交叉编译的关键参数常见组合还有GOOSlinux GOARCHarm64给树莓派用。如果编译报错找不到包先执行go mod tidy拉齐依赖——虽然这个项目只用标准库但模块声明缺失时也会报错。3.2 启动服务端并确认监听服务端需要指定监听地址和端口。常见做法是监听0.0.0.0表示所有网卡都接受连接# 启动服务端监听 8888 端口 ./lan-chat -modeserver -addr0.0.0.0:8888启动后终端会打印类似listening on 0.0.0.0:8888的日志。这时候在另一台机器上先用telnet或nc测一下端口通不通# 在另一台机器上测试端口连通性 nc -zv 192.168.1.100 8888如果显示succeeded或open说明网络层没问题。如果卡住或拒绝先检查服务端机器防火墙有没有放行 8888 端口。Linux 上常见做法是用ufw allow 8888/tcp或firewall-cmd --add-port8888/tcpWindows 则要在「高级安全 Windows Defender 防火墙」里新建入站规则。3.3 客户端接入与多开测试客户端启动时传入服务端 IP 和自己的昵称# 客户端 A ./lan-chat -modeclient -addr192.168.1.100:8888 -nick张三 # 客户端 B同一台机器再开一个终端 ./lan-chat -modeclient -addr192.168.1.100:8888 -nick李四两个客户端都连上后在 A 里输入「接口文档发我一下」B 的终端会立刻显示张三: 接口文档发我一下。服务端终端也会打印加入和离开的系统消息。如果想模拟多人同一台机器开多个终端窗口即可每个窗口一个昵称。注意昵称不要重复否则广播时无法区分——虽然代码没做去重校验但实际使用中重名会让对话变得混乱。3.4 参数说明与常用配置参数含义默认值示例-mode运行模式server 或 clientclient-modeserver-addr服务端监听地址或客户端连接地址127.0.0.1:8888-addr0.0.0.0:8888-nick客户端昵称匿名-nick张三这些参数在main.go里用flag包解析改默认值直接改flag.String的第二个参数即可。如果想让服务端启动时自动打印本机所有网卡 IP方便别人连接可以在Start之前加一段遍历net.Interfaces()的逻辑这是我在多个内网工具里都会加的小功能能省去用户自己查 IP 的麻烦。4. 避坑与排查粘包、并发写、防火墙这三类问题最常遇到4.1 消息显示不全或两条消息粘在一起现象客户端偶尔只收到半句话或者两条消息连成一行显示。原因TCP 是流式协议没有消息边界。如果接收端直接用conn.Read读固定大小缓冲区就会把两条消息读进同一个 buffer或者一条消息分两次读完。解决严格按Decode里的逻辑先读 4 字节长度再按长度读满消息体。不要用bufio.Reader.ReadString(\n)这种按分隔符读的方式因为消息内容里可能包含换行符。如果自己改造协议记住一个原则长度字段本身必须是定长的否则长度字段自己也会粘包。4.2 并发写同一个连接导致数据错乱现象多个 goroutine 同时向一个net.Conn写数据接收端解析出的消息内容错位或乱码。原因net.Conn的Write方法在 Go 里是并发安全的但「编码 写入」这个组合操作不是原子的。两个 goroutine 同时调用Encode再Write字节流可能交错。解决给每个连接配一个sync.Mutex写之前加锁或者更优雅的做法是每个连接一个发送 channel单独一个 goroutine 负责从 channel 取数据并写入连接。这个项目里服务端广播时是单 goroutine 顺序写所以没暴露这个问题但一旦你加了「私聊」功能多个 goroutine 同时写同一个连接就会翻车。4.3 局域网内其他机器连不上现象本机客户端能连上隔壁机器连不上提示connection refused或超时。原因三种可能——服务端监听在127.0.0.1而不是0.0.0.0服务端机器防火墙拦截了入站连接两台机器不在同一网段。解决先确认启动参数是-addr0.0.0.0:8888然后检查防火墙规则最后用ping和ipconfig/ifconfig确认双方 IP 前三段一致。如果公司网络做了 VLAN 隔离即使 IP 看起来在同一网段也可能不通这种情况需要找网络管理员确认不是代码能解决的。4.4 客户端退出后服务端没有清理连接现象客户端直接关窗口或 CtrlC 后服务端clientsmap 里还留着那条连接广播时向已关闭的连接写入会报错。原因客户端异常退出时没有发送关闭通知服务端的Decode会返回io.EOF但如果没有正确处理这个错误并执行delete连接就会泄漏。解决在handleConn里用defer确保无论正常还是异常退出都执行delete(s.clients, conn)和conn.Close()。另外广播时忽略写入错误不要因为一个死连接影响其他客户端。我一般还会加一个心跳机制每隔 30 秒向所有连接发一个空消息连续三次写失败就主动清理这样能处理拔网线这种极端情况。4.5 中文昵称或消息乱码现象英文聊天正常一发中文就显示成乱码或问号。原因Windows 终端默认编码可能是 GBK而 Go 字符串是 UTF-8直接输出到终端时编码不匹配。解决在 Windows 上运行前执行chcp 65001切换到 UTF-8 代码页。或者在代码里引入golang.org/x/text/encoding/simplifiedchinese做转码但这会增加依赖。更简单的做法是文档里注明「Windows 用户请先执行 chcp 65001」这是我在多个控制台工具里踩过的坑写清楚能省掉大量反馈。5. 二次开发加私聊、传文件、改协议的三条改造路线拿到这份源码后最值得动手改的方向有三个我按难度从低到高排一下。第一条路线加私聊指令。现在的广播是「一人说话全房间听」改造成支持昵称 消息的私聊只需要在服务端broadcast之前判断消息内容是否以开头解析出目标昵称然后只向对应连接写入。核心改动在handleConn的消息处理循环里加一个分支// 伪代码示意私聊分支 if strings.HasPrefix(msg.Content, ) { parts : strings.SplitN(msg.Content, , 2) target : strings.TrimPrefix(parts[0], ) s.sendTo(target, fmt.Sprintf([私聊] %s: %s, msg.Nickname, parts[1])) } else { s.broadcast(...) }sendTo方法需要遍历clients找到昵称匹配的连接找不到就回一条「用户不在线」。这里要注意锁的粒度——查找时用读锁找到后释放锁再写入避免持锁写网络导致其他操作阻塞。第二条路线加文件传输。文件传输的本质是把文件切成块每块当作一条消息发送接收端按序号拼接。可以在Message结构体里加一个Type字段区分「文本」和「文件块」再加FileID、Seq、Total三个字段。发送端读文件按 32KB 分块接收端收到所有块后按FileID合并写入磁盘。这个改造会显著增加协议复杂度建议先把Message的编解码改成用encoding/gob或 JSON省去手动处理字段的麻烦。但要注意JSON 编码后的长度不固定仍然需要外层长度前缀来定边界。第三条路线换传输层协议。如果想让聊天支持离线消息或跨网段可以把 TCP 换成 WebSocket服务端用gorilla/websocket库客户端用浏览器页面。这样部署时只需要一个 HTTP 端口也方便做内网穿透。但这就偏离了「控制台程序」的初衷适合作为进阶练手。改完之后怎么验证我的习惯是写一个简单的压测脚本用 Go 起 50 个 goroutine 模拟 50 个客户端同时连接、发送、断开观察服务端内存和 CPU 是否稳定以及有没有连接泄漏。命令大概是go run stress_test.go -clients50 -duration30s。如果 50 个客户端跑 30 秒后服务端clientsmap 长度归零说明连接清理逻辑没问题。最后说一个我自己的教训早期改这类网络程序时我总喜欢在broadcast里直接conn.Write觉得简单。结果有一次某个客户端网络抖动整个聊天室卡了十几秒所有人都收不到消息。从那以后我每次写广播逻辑都强制走一遍「发送 channel 独立写 goroutine」的模式哪怕代码多二十行也比线上被投诉强。希望这个项目的拆解能帮你少走一段弯路把这份源码跑起来、改起来真正变成自己顺手的内网工具。本文还有配套的精品资源点击获取

相关新闻

LangGraph实战:从零构建生产级AI智能体

LangGraph实战:从零构建生产级AI智能体

1. 为什么要用LangGraph构建AI智能体 1.1 AI智能体到底“智能”在哪里 先聊一个很多人都会混淆的问题:普通的大模型调用和真正的AI智能体,差别到底在哪? 单纯调一次LLM API,输入Prompt、拿回结果,这只是“单次推理”…

2026/10/17 14:00:55 阅读更多 →
Flutter for OpenHarmony 实战:艺考题库App题目详情页实现

Flutter for OpenHarmony 实战:艺考题库App题目详情页实现

前阵子接了一个艺考机构的题库App需求,目标平台锁定在 OpenHarmony,业务端要求做真题打点、题目详情、答题反馈这些核心模块。当时第一反应就是走 Flutter 这条路,正好赶上了 Flutter for OpenHarmony 的适配方案日趋成熟,最后折腾…

2026/10/17 14:00:55 阅读更多 →
十亿数据下的ShardingJDBC实战:从路由卡顿到性能润滑的调优指南

十亿数据下的ShardingJDBC实战:从路由卡顿到性能润滑的调优指南

十亿数据这个体量,绝大多数团队不是一口气碰上,而是像我们一样,某天看数据库容量报表才发现单表已经涨到三亿、五亿,查询开始明显变慢,然后才把ShardingJDBC请进来。当时我们推进的是一个"模拟项目X"的资金流…

2026/10/17 14:00:55 阅读更多 →

最新新闻

深入理解 DOM NodeList:从基础到实战

深入理解 DOM NodeList:从基础到实战

1. 什么是 NodeListNodeList 是 DOM 中一个非常重要的集合类型,它表示一组节点的集合。当我们使用 document.querySelectorAll() 或 element.childNodes 等 API 时,返回的结果就是 NodeList。NodeList 与数组(Array)有相似之处&am…

2026/10/17 15:59:20 阅读更多 →
实测10款AI论文工具:从开题到答辩的辅助指南

实测10款AI论文工具:从开题到答辩的辅助指南

又到了一年论文季,后台和私信里全是问“有哪些真正能用的AI论文工具”的同学。去年我帮实验室带过不少本科生的毕业论文,自己也踩过不少坑。市面上的所谓“AI论文网站”鱼龙混杂,有真能提效的,也有纯粹割韭菜的。这篇东西不整虚的…

2026/10/17 15:59:20 阅读更多 →
Qt5兼容QFTP移植包:工业现场FTP文件传输实战方案

Qt5兼容QFTP移植包:工业现场FTP文件传输实战方案

简介:本资源是面向Qt初学者与跨平台开发者的QT5环境下FTP功能实现方案,解决QT5官方移除QFTP模块后无法直接使用FTP客户端API的实践难题。压缩包共8个文件(25KB),含4个C源文件(cpp)、3个头文件&a…

2026/10/17 15:59:20 阅读更多 →
华为云鲲鹏ECS三节点Hadoop集群搭建与OBS存储接入实战

华为云鲲鹏ECS三节点Hadoop集群搭建与OBS存储接入实战

简介:实验报告以华为云鲲鹏云环境为依托,完整记录在ECS与OBS上搭建Hadoop集群的全过程,面向高校大数据课程、云计算实训及刚接触分布式平台的初学者。内容从购买弹性云服务器和对象存储服务入手,详细说明AK/SK认证密钥获取、多节点…

2026/10/17 15:59:20 阅读更多 →
自适应滑模控制MATLAB仿真:抖振抑制与收敛性验证

自适应滑模控制MATLAB仿真:抖振抑制与收敛性验证

简介:本资源是一套面向控制工程专业学生与研究人员的自适应滑模控制MATLAB仿真实践包,聚焦空中机械臂等非线性不确定系统的鲁棒控制问题,适用于具备基础自动控制与MATLAB编程能力的学习者。压缩包共12个文件(7个m脚本实现控制器设…

2026/10/17 15:59:20 阅读更多 →
CSAPP第三版实验全攻略:从环境搭建到六大Lab避坑指南

CSAPP第三版实验全攻略:从环境搭建到六大Lab避坑指南

简介:《深入理解计算机系统》第三版配套实验包是一份面向计算机系统学习者的实践资源,适合正在阅读原书或准备系统掌握底层原理的读者。实验包围绕书中经典实验展开,覆盖汇编语言、内存管理、缓存模拟、进程控制、编译器构造与安全攻防等核心…

2026/10/17 15:58:19 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →