使用Go開(kāi)發(fā)一個(gè)文件同步小工具(附源碼)
"你復(fù)制,我粘貼;你改了,我跟著動(dòng)。" —— FileSync 的座右銘
大家好!今天我要給大家介紹一個(gè)我最近寫(xiě)的小玩具——FileSync。它不是什么高大上的分布式同步系統(tǒng),也不是什么帶 GUI 界面的炫酷軟件,而是一個(gè)樸實(shí)無(wú)華、靠命令行吃飯、還帶點(diǎn)"佛系"氣質(zhì)的 Go 語(yǔ)言小工具。
它的任務(wù)很簡(jiǎn)單:把 A 文件夾里的內(nèi)容,原封不動(dòng)地同步到 B 文件夾里,并且自動(dòng)跳過(guò)日志目錄(比如 \logs\、\log\、\runlog\),避免把一堆沒(méi)用的日志也搬過(guò)去占地方。
最重要的是——它還能優(yōu)雅退出!只要你在終端敲個(gè) exit,它就會(huì)乖乖收工,不鬧脾氣
它是怎么工作的
FileSync 的邏輯其實(shí)非常直白:
- 啟動(dòng)主線程:開(kāi)始遍歷源目錄(fromDir)。
- 智能過(guò)濾:遇到名字里帶 log 的文件夾?直接跳過(guò)!我們不關(guān)心日志,只關(guān)心"正經(jīng) 文件"。
- 同步操作:
- 如果是目錄 → 在目標(biāo)位置創(chuàng)建同名目錄。
- 如果是文件 → 比較修改時(shí)間!只有源文件比目標(biāo)新(或目標(biāo)不存在),才復(fù)制過(guò)去,并保留原始修改時(shí)間。
- 每小時(shí)掃一次:干完一輪活,就去"打坐"一小時(shí)(其實(shí)是 sleep 3600秒),然后繼續(xù)巡邏。
- 隨時(shí)聽(tīng)令退出:主 goroutine 在后臺(tái)干活,主線程監(jiān)聽(tīng)用戶輸入。一旦你說(shuō)"exit",它立刻收手,等所有任務(wù)結(jié)束再退出,絕不拖泥帶水。
是不是有點(diǎn)像一個(gè)勤懇又聽(tīng)話的數(shù)字園???
為什么說(shuō)它"佛系"
- 它不會(huì)瘋狂刷屏告訴你"我又復(fù)制了一個(gè)文件!"(雖然現(xiàn)在會(huì)打印路徑,但你可以輕松注釋掉)。
- 它不爭(zhēng)不搶?zhuān)啃r(shí)才工作一次,其余時(shí)間都在"冥想"。
- 它尊重文件的"前世今生"——連修改時(shí)間都要原樣保留,生怕打擾了文件的情緒。
- 你說(shuō)"走",它絕不賴著不走,還會(huì)禮貌地說(shuō)一句:"FileSync soft exit"。
這哪是程序?分明是個(gè)修行千年的老和尚寫(xiě)的代碼
使用須知(別踩坑)
必須傳兩個(gè)參數(shù):./FileSync /path/to/source /path/to/target
路徑分隔符注意:代碼里用了 \\ 來(lái)判斷 Windows 風(fēng)格的日志路徑。如果你在 Linux/macOS 上跑,可能需要改成 /logs/。不過(guò) filepath.Walk 是跨平臺(tái)的,所以實(shí)際運(yùn)行沒(méi)問(wèn)題,只是過(guò)濾邏輯可能失效(因?yàn)?Linux 路徑不含反斜杠)。建議改成 / 或使用 filepath.Separator 更健壯。
權(quán)限問(wèn)題:確保程序有讀源目錄、寫(xiě)目標(biāo)目錄的權(quán)限。
大文件警告:目前是全量復(fù)制,沒(méi)做增量或斷點(diǎn)續(xù)傳,超大文件可能會(huì)卡一下。
改進(jìn)建議(留給未來(lái)的你)
用 fsnotify 監(jiān)聽(tīng)文件變化,實(shí)時(shí)同步,告別"每小時(shí)打坐"。
支持配置文件,自定義忽略規(guī)則。
加日志輸出開(kāi)關(guān),別總往 stdout 打。
支持雙向同步 or 增量備份模式。
給它起個(gè)更酷的名字,比如 ZenSync?
源碼奉上
下面就是這個(gè)"佛系同步器"的完整源碼,Go 語(yǔ)言編寫(xiě),簡(jiǎn)潔明了,歡迎拿去魔改!
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
var exit = false // 退出狀態(tài)控制
var wg sync.WaitGroup // 保證正常退出
func main() {
fmt.Println("FileSync soft run")
if len(os.Args) < 3 {
fmt.Println("Usage: FileSync <source_dir> <target_dir>")
return
}
fromDir := os.Args[1]
toDir := os.Args[2]
wg.Add(1)
go mainRun(fromDir, toDir) // 啟動(dòng)工作主線程
for {
var cmd string
fmt.Scanln(&cmd)
fmt.Println("cmd:", cmd)
if cmd == "exit" {
exit = true
break
} else {
fmt.Println("unknow command")
fmt.Println("exit exit soft")
}
}
wg.Wait()
fmt.Println("FileSync soft exit")
}
func mainRun(fromDir, toDir string) {
defer wg.Done()
for !exit {
filepath.Walk(fromDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
fmt.Println(path)
p := strings.ToLower(path)
// 跳過(guò)常見(jiàn)日志目錄(注意:Windows風(fēng)格路徑)
if strings.Contains(p, "\\runlog\\") ||
strings.Contains(p, "\\logs\\") ||
strings.Contains(p, "\\log\\") {
return nil
}
if info.IsDir() {
syncDir(strings.Replace(path, fromDir, toDir, 1))
} else {
syncFile(path, strings.Replace(path, fromDir, toDir, 1))
}
return nil
})
// 每小時(shí)同步一次
for i := 0; i < 60*60; i++ {
time.Sleep(time.Second)
if exit {
break
}
}
}
}
func syncDir(dirname string) {
if err := os.MkdirAll(dirname, 0777); err != nil {
fmt.Println(err)
}
}
func syncFile(f, t string) {
CopyFile(f, t)
}
func CopyFile(f, t string) (written int64, err error) {
src, err := os.Open(f)
if err != nil {
return
}
defer src.Close()
st, _ := src.Stat()
mt := st.ModTime()
var dst *os.File
if ft, exist := checkFileIsExist(t); exist {
// 修改時(shí)間相同,跳過(guò)復(fù)制
if st.ModTime().UnixNano() == ft.UnixNano() {
fmt.Println("修改時(shí)間相同,放棄")
return
}
dst, err = os.OpenFile(t, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0777)
if err != nil {
fmt.Println(err)
return
}
} else {
dst, err = os.Create(t)
if err != nil {
fmt.Println(err)
return
}
}
l, err := io.Copy(dst, src)
dst.Close()
if err == nil {
// 保留原始修改時(shí)間
err := os.Chtimes(t, mt, mt)
if err != nil {
fmt.Println(err)
}
}
return l, err
}
func checkFileIsExist(filename string) (time.Time, bool) {
fi, err := os.Stat(filename)
if os.IsNotExist(err) {
return time.Now(), false
} else if err != nil {
return time.Now(), false
}
return fi.ModTime(), true
}
結(jié)語(yǔ)
雖然這個(gè)小工具簡(jiǎn)單,但它體現(xiàn)了 Go 語(yǔ)言的并發(fā)之美(一個(gè) goroutine 干活,主線程監(jiān)聽(tīng)退出)、文件操作的便捷性,以及——一點(diǎn)點(diǎn)程序員的幽默感。
下次當(dāng)你需要一個(gè)輕量、可控、不吵不鬧的同步腳本時(shí),不妨試試這個(gè)"佛系 FileSync"。說(shuō)不定,它還能幫你悟出點(diǎn)編程禪意呢
代碼已開(kāi)源,心法自悟。
P.S. 如果你在 macOS/Linux 上使用,請(qǐng)記得把 \\log\\ 這類(lèi)判斷改成 /log/,或者更優(yōu)雅地用 filepath.Join("log") 來(lái)處理路徑分隔符哦!
到此這篇關(guān)于使用Go開(kāi)發(fā)一個(gè)文件同步小工具(附源碼)的文章就介紹到這了,更多相關(guān)Go文件同步內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang循環(huán)變量捕獲問(wèn)題??的解決
本在Go語(yǔ)言中,循環(huán)中啟動(dòng)協(xié)程時(shí),直接在協(xié)程閉包中引用循環(huán)變量會(huì)導(dǎo)致所有協(xié)程共享同一個(gè)變量,從而引發(fā)變量捕獲問(wèn)題,本文就來(lái)介紹一下該問(wèn)題的解決,感興趣的可以了解一下2025-11-11
Go語(yǔ)言基礎(chǔ)教程之函數(shù)和方法詳解
在Go語(yǔ)言中,函數(shù)和方法在聲明方式上存在顯著差異,理解這一點(diǎn)是正確解讀文檔的關(guān)鍵,這篇文章主要介紹了Go語(yǔ)言基礎(chǔ)教程之函數(shù)和方法的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-10-10
Goland使用Go Modules創(chuàng)建/管理項(xiàng)目的操作
這篇文章主要介紹了Goland使用Go Modules創(chuàng)建/管理項(xiàng)目的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05
Go語(yǔ)言題解LeetCode599兩個(gè)列表的最小索引總和
這篇文章主要為大家介紹了Go語(yǔ)言題解LeetCode599兩個(gè)列表的最小索引總和示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
解析golang 標(biāo)準(zhǔn)庫(kù)template的代碼生成方法
這個(gè)項(xiàng)目的自動(dòng)生成代碼都是基于 golang 的標(biāo)準(zhǔn)庫(kù) template 的,所以這篇文章也算是對(duì)使用 template 庫(kù)的一次總結(jié),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11
GoLang BoltDB數(shù)據(jù)庫(kù)詳解
這篇文章主要介紹了GoLang BoltDB數(shù)據(jù)庫(kù),boltdb是使用Go語(yǔ)言編寫(xiě)的開(kāi)源的鍵值對(duì)數(shù)據(jù)庫(kù),boltdb存儲(chǔ)數(shù)據(jù)時(shí) key和value都要求是字節(jié)數(shù)據(jù),此處需要使用到 序列化和反序列化2023-02-02
Go?Singleflight導(dǎo)致死鎖問(wèn)題解決分析
這篇文章主要為大家介紹了Go?Singleflight導(dǎo)致死鎖問(wèn)題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Go使用sync.Map來(lái)解決map的并發(fā)操作問(wèn)題
在 Golang 中 map 不是并發(fā)安全的,sync.Map 的引入確實(shí)解決了 map 的并發(fā)安全問(wèn)題,本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下2021-10-10

