在 Go 中實現 tail 的跟蹤功能

【導讀】本文介紹了一種 go 實現 tail 功能的思路與實現,此實現在實際項目中可以用於監聽文件變更。

tail 是我們大多數人都熟悉的命令。我假設你也熟悉提供的 -f 選項。如果你不熟悉,知道它會打印出文件的最後幾行即可。最近在一個項目上工作,我想知道我需要做什麼來實現這個功能。這個想法來自閱讀 Feynman 的書:

毫無疑問,你知道如何去做; 但是當你像小孩子一樣玩這類問題,並且你沒有看到答案時... 試圖找出如何去做是很有趣的。然後,當你進入成年時,你會培養出一定的自信,你可以去發現事物; 但是如果他們已經被發現,那你根本不應該再來打擾自己。一個傻瓜能做的事,另一個傻瓜也能做,其他一些傻瓜打你的事實不應該打擾你:你應該爲將要發現的事物而快樂。

實現它可能是一件小事。但我認爲這將是一系列文章中的一個良好的開端,在這篇文章中我寫了如何實現一些東西。因此,讓我帶你看看如何在 Go 中實現 tail -f 的過程。

首先,讓我們瞭解這個問題。tail 命令提供了一個標誌,可以 “跟蹤” 文件。它所做的是等待添加到文件末尾的任何更改並將其打印出來。爲了簡單起見,我不打算實現 tail,而只是實現跟蹤功能。所以我們在開始時打印整個文件,然後在添加它們時打印行。

我首先想到的是這樣做的非常幼稚的方式。打印文件中的所有字節,直到達到 io.EOF; 讓這個過程睡一會兒,然後再試一次。我們來看看這個函數:

func follow(file io.Reader) error {
    r := bufio.NewReader(file)
    for {
        by, err := r.ReadBytes('\n')
        if err != nil && err != io.EOF {
            return  err
        }

        fmt.Print(string(by))
        if err == io.EOF {
            time.Sleep(time.Second)
        }
    }
}

當內容寫入文件時,可悲的是沒有及時做出反應。Linux 提供了一個 API 來監視文件系統事件:inotify AP。手冊頁給了你一個很好的介紹。它提供了兩個我們感興趣的函數:inotify_initinotify_add_watchinotify_init 函數創建一個對象,我們將使用該對象進一步與 API 進行交互。inotify_add_watch 函數允許你指定感興趣的文件事件。API 提供了幾個事件,但我們關心的是修改文件時發出的 IN_MODIFY 事件。

由於我們使用 Go,不得不列出 syscall 包。它爲前面提到的功能提供了包裝器:syscall.InotifyInitsyscall.InotifyAddWatch。使用 syscall 讓我們看看如何實現 follow 函數。爲了簡潔起見,我省略了錯誤處理,當你看到一個 _ 變量被使用時,它是處理返回錯誤的好地方。

func follow(filename string) error {
    file, _ := os.Open(filename)
    fd, _ := syscall.InotifyInit()
    _, _ := syscall.InotifyAddWatch(fd, filename, syscall.IN_MODIFY)
    r := bufio.NewReader(file)
    for {
        by, err := r.ReadBytes('\n')
        if err != nil && err != io.EOF {
            return err
        }
        fmt.Print(string(by))
        if err != io.EOF {
            continue
        }
        if err = waitForChange(fd); err != nil {
            return err
        }
    }
}

func waitForChange(fd int) error {
    for {
        var buf [syscall.SizeofInotifyEvent]byte
        _, _ := syscall.Read(fd, buf[:])
        if err != nil {
            return err
        }
        r := bytes.NewReader(buf[:])
        var ev = syscall.InotifyEvent{}
        _ = binary.Read(r, binary.LittleEndian, &ev)
        if ev.Mask&syscall.IN_MODIFY == syscall.IN_MODIFY {
            return nil
        }
    }
}

InotifyInit 函數返回一個可用於讀取 sycall.InotifyEvent 的文件處理程序。從這個處理程序讀取是一個阻塞操作。這使我們只有在創建事件時才做出反應。

如果您要處理多個操作系統,最好更一般地處理這個操作系統。這就是 fsnotify 軟件包的來源。它提供了一個針對 Linux 的 inotify,BSD 的 kqueue 等的抽象。使用 fsnotify 我們的函數看起來與前面的非常相似,但是更簡單。

func follow(filename string) error {
    file, _ := os.Open(filename)
    watcher, _ := fsnotify.NewWatcher()
    defer watcher.Close()
    _ = watcher.Add(filename)

    r := bufio.NewReader(file)
    for {
        by, err := r.ReadBytes('\n')
        if err != nil && err != io.EOF {
            return err
        }
        fmt.Print(string(by))
        if err != io.EOF {
            continue
        }
        if err = waitForChange(watcher); err != nil {
            return err
        }
    }
}

func waitForChange(w *fsnotify.Watcher) error {
    for {
        select {
        case event := <-w.Events:
            if event.Op&fsnotify.Write == fsnotify.Write {
                return nil
            }
        case err := <-w.Errors:
            return err
        }
    }
}

我希望代碼解釋得比我寫的文本更好。爲了簡潔,我省略了代碼可能失敗的幾種情況。爲了完善功能,我必須深入挖掘。

但這足以讓我對其產生了知識的渴望:它到底是如何工作的。希望你也由此感覺。

轉自:Go 語言中文網

studygolang.com/articles/12489

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Pbm2KatlvKB77j9WWaqoWw