本文介绍 Go 语言中使用 goftp 库实现 FTP 文件上传的同时获取上传进度的正确方法,重点解决因共享 FTP 连接导致的竞态阻塞问题,并提供双连接并发与串行协同两种可靠实现方案。
本文介绍 go 语言中使用 `goftp` 库实现 ftp 文件上传的同时获取上传进度的正确方法,重点解决因共享 ftp 连接导致的竞态阻塞问题,并提供双连接并发与串行协同两种可靠实现方案。
在 Go 中通过 FTP 协议上传大文件时,常需同时显示上传进度(如已传输字节数、速率或远程文件状态),但直接对同一 *goftp.FTP 实例启动多个 goroutine(如一个调用 Stor() 上传,另一个循环调用 List() 查询)会导致连接竞争与协议阻塞——因为 goftp 的底层 TCP 连接是非线程安全的,且 FTP 协议本身不支持真正的并行命令通道。你的原始代码中两个 goroutine 共享 ftp 实例,造成 Stor() 无法正常写入数据流,List() 则反复返回空结果,最终表现为上传“卡住”或静默失败。
✅ 正确方案一:双 FTP 连接(推荐)
为每个 goroutine 分配独立的 FTP 连接实例,彻底隔离 I/O 状态:
// 建立主上传连接
ftpUpload, err := goftp.Connect("serverip:port")
if err != nil {
log.Fatal("upload connect failed:", err)
}
defer ftpUpload.Close()
// 建立监控连接(独立会话)
ftpMonitor, err := goftp.Connect("serverip:port")
if err != nil {
log.Fatal("monitor connect failed:", err)
}
defer ftpMonitor.Close()
// 分别认证(注意:TLS 配置需一致)
config := tls.Config{InsecureSkipVerify: true}
ftpUpload.AuthTLS(config)
ftpMonitor.AuthTLS(config)
ftpUpload.Login("userName", "pass")
ftpMonitor.Login("userName", "pass")
ftpUpload.Cwd("/home/myDir/")
ftpMonitor.Cwd("/home/myDir/")
file, err := os.Open("sth")
if err != nil {
log.Fatal("open file failed:", err)
}
defer file.Close()
// 启动上传(阻塞式,但运行在 goroutine 中)
done := make(chan error, 1)
go func() {
done <- ftpUpload.Stor("sth", file)
}()
// 启动进度监控(轮询远程文件大小)
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
files, err := ftpMonitor.List("sth")
if err == nil && len(files) > 0 {
// 获取远程文件大小(单位:字节)
fmt.Printf("Remote file size: %d bytes\n", files[0].Size)
}
case err := <-done:
if err != nil {
log.Println("Upload failed:", err)
} else {
fmt.Println("Upload completed successfully.")
}
return // 退出监控 goroutine
}
}
}()
// 主 goroutine 等待上传完成(避免 main 提前退出)
<-done⚠️ 注意事项:
Colly Golang Web Scraper and Crawler Framework下载Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 每个 goftp.FTP 实例对应一个独立 TCP 连接和 FTP 控制通道,不可复用;
- 服务端需允许并发连接(多数 FTP 服务器默认支持);
- List() 返回的 files[0].Size 是当前远程文件的已写入字节数,可作为近似进度指标(FTP 协议无标准进度回调,此为常用实践);
- 使用 select + done channel 实现优雅退出,避免监控 goroutine 泄漏。
✅ 方案二:主流程上传 + 异步状态轮询(轻量级)
若服务端连接数受限,可改用单连接顺序执行,配合 io.TeeReader 实时统计上传字节数:
file, _ := os.Open("sth")
defer file.Close()
// 包装 reader,实时记录已读字节数
var uploaded int64
reader := io.TeeReader(file, &byteCounter{&uploaded})
// 主 goroutine 执行上传(同步)
err := ftp.Stor("sth", reader)
if err != nil {
log.Fatal("Upload error:", err)
}
// byteCounter 实现
type byteCounter struct{ n *int64 }
func (c *byteCounter) Write(p []byte) (int, error) {
*c.n += int64(len(p))
return len(p), nil
}此时无需额外 goroutine 监控,uploaded 变量即为实时进度,适合嵌入 UI 或日志输出。
总结
FTP 协议的串行本质决定了单连接无法真正并发执行 STOR 与 LIST。强行共享 *goftp.FTP 实例将引发不可预测的阻塞与竞态。生产环境应优先采用双连接方案:一个专用于上传,一个专用于状态查询。同时务必确保连接生命周期管理(及时 Close)、错误处理与 goroutine 退出控制,才能构建稳定可靠的 FTP 文件传输系统。


















