在本地开发机和远程服务器之间保持文件一致,是部署和备份中的常见需求。用Golang写同步工具,既能编译成单文件二进制,又能利用并发轻松处理大量小文件。实现的重点不在于传输本身,而在于如何快速、准确地判断哪些文件需要同步。

一、同步工具的整体设计
一个实用的文件同步工具通常分为三个模块:扫描模块负责生成本地文件清单,对比模块负责和远程清单做差异计算,执行模块负责上传、下载或删除。这样的分层让逻辑清晰,也方便后续替换传输协议。
本地清单可以用一个map来保存,键是相对路径,值是文件元数据。远程清单通过调用远程接口或解析命令输出来获取。两者结构一致,对比时只需遍历一次本地map,并检查远程是否存在对应项。
1.1 文件元数据结构
为了准确判断文件是否变化,不能只依赖文件名。我们至少需要记录文件大小、修改时间以及内容哈希。修改时间用于快速初筛,哈希用于最终确认内容一致。
下面定义基础的元数据结构和清单类型,后续对比算法都基于它展开。
package main
import (
"time"
)
// FileMeta 保存单个文件的对比信息
type FileMeta struct {
Path string
Size int64
ModTime time.Time
Hash string
}
// FileList 是路径到元数据的映射
type FileList map[string]FileMeta
二、本地文件扫描与哈希计算
扫描阶段使用filepath.Walk遍历目录,跳过临时文件和目录本身。对每个常规文件,读取大小与修改时间,并计算sha256哈希。哈希计算虽然稍慢,但只在大小或修改时间不一致时才需要做,能大幅减少开销。
为了提升扫描速度,可以为每个文件启动一个goroutine计算哈希,再用channel回收结果。不过对于普通机械盘,过度并发反而会因IO争抢变慢,一般限制为8到16个worker即可。
2.1 计算文件哈希的辅助函数
下面函数接收文件路径,返回sha256摘要的十六进制字符串。我们在对比算法中把它封装起来,仅在必要时调用。
import (
"crypto/sha256"
"encoding/hex"
"os"
)
// calcHash 计算文件内容的sha256值
func calcHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
buf := make([]byte, 32*1024)
for {
n, readErr := f.Read(buf)
if n > 0 {
h.Write(buf[:n])
}
if readErr != nil {
break
}
}
return hex.EncodeToString(h.Sum(nil)), nil
}
2.2 扫描本地目录
扫描函数返回FileList,忽略以小数点开头的隐藏临时文件。修改时间使用UTC统一,避免时区导致误判。
import (
"filepath"
"os"
"path/filepath"
)
// scanLocal 扫描root目录下的所有普通文件
func scanLocal(root string) (FileList, error) {
list := make(FileList)
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Base(p)[0] == '.' {
return nil
}
rel, _ := filepath.Rel(root, p)
meta := FileMeta{
Path: rel,
Size: info.Size(),
ModTime: info.ModTime().UTC(),
}
h, herr := calcHash(p)
if herr != nil {
return herr
}
meta.Hash = h
list[rel] = meta
return nil
})
return list, err
}
三、本地与远程文件对比算法
对比算法的目标是产出三类差异:需上传(本地有远程无或内容不同)、需删除(远程有本地无)、一致(跳过)。如果远程清单通过HTTP接口返回JSON,只需反序列化为FileList即可直接比对。
基础策略是先比大小,大小不同直接判定为修改;大小相同再比修改时间,时间不同计算哈希确认;哈希一致才认为相同。这样绝大多数情况不用算哈希,速度很快。
3.1 差异结果结构
定义DiffResult保存待上传和待删除的路径切片,供执行模块使用。
// DiffResult 保存对比后的差异
type DiffResult struct {
Upload []string
Delete []string
}
3.2 核心对比逻辑
函数接收本地清单和远程清单,返回差异。注意远程不存在的文件直接加入上传列表,本地不存在的文件加入删除列表。
// diff 对比本地与远程清单
func diff(local, remote FileList) DiffResult {
var res DiffResult
for path, lm := range local {
rm, ok := remote[path]
if !ok {
res.Upload = append(res.Upload, path)
continue
}
if lm.Size != rm.Size || !lm.ModTime.Equal(rm.ModTime) {
if lm.Hash != rm.Hash {
res.Upload = append(res.Upload, path)
}
}
}
for path := range remote {
if _, ok := local[path]; !ok {
res.Delete = append(res.Delete, path)
}
}
return res
}
3.3 算法优缺点分析
上述算法实现简单,适合文件数量在几万以内的场景。它的优势是无需提前传输文件内容,仅用元信息就能决策。缺点是首次全量哈希仍可能耗时,可通过只比较修改时间来进一步加速,但会有极小概率漏掉同大小同时间但内容变了的情况。
如果远程支持rsync风格的分块签名,可把算法升级为分块对比,只传差异块。不过那已超出基础工具范畴,中小项目用元信息对比已足够稳定。
四、并发执行同步操作
得到差异列表后,用固定容量的worker池并发上传或删除。每个任务通过SSH的scp或自定义HTTP接口完成。并发数建议与CPU核数或网络带宽匹配,避免打满连接数。
下面示例用带缓冲的channel控制并发,任务函数可根据实际传输协议替换。
import "sync"
// workerPool 并发执行上传任务
func workerPool(tasks []string, workers int, do func(string)) {
ch := make(chan string, len(tasks))
for _, t := range tasks {
ch <- t
}
close(ch)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for t := range ch {
do(t)
}
}()
}
wg.Wait()
}
4.1 简单的任务函数示例
这里仅打印路径模拟上传,真实环境可替换为调用os.ReadFile后通过HTTP POST发送。
// fakeUpload 模拟上传动作
func fakeUpload(p string) {
println("upload:", p)
}
// 使用方式
// workerPool(res.Upload, 8, fakeUpload)
五、总结与扩展思路
用Golang实现本地远程同步工具,核心是把对比算法和传输解耦。元信息对比配合并发执行,已能覆盖大多数轻量同步需求。后续可加入配置文件忽略规则、断点续传以及双向同步冲突解决,让工具更贴近日常使用。
整体代码量少、依赖只有标准库,交叉编译后可直接丢到树莓派或服务器运行,是替代重型同步方案的务实选择。