原创分享 浅析 gowatch 监听文件变动实现原理

yudotyang · 2021年01月04日 · 116 次阅读

刚开始接触 go 时,发现和解释型语言不同,go 是编译型语言,即每次在有程序改动后,需要重新运行 go run 或 go build 进行重新编译,更改才能生效,实则不便。于是乎在网络上搜索发现了 gowatch 这个包,该包可通过监听当前目录下相关文件的变动,对 go 文件实时编译,提高研发效率。那 gowatch 又是如何做到监听文件变化的呢?

通过阅读源码我们发现,在 linux 内核中,有一种用于通知用户空间程序文件系统变化的机制—Inotify。它监控文件系统,并且及时向专门的应用程序发出相关的事件警告,比如删除、读、写和卸载操作等。您还可以跟踪活动的源头和目标等细节。Golang 的标准库 syscall 实现了该机制。为进一步扩展,实现了 fsnotify 包实现了一个基于通道的、跨平台的实时监听接口。如下图:

根据上图可知,监听文件的变化主要依赖于 linux 内核的 INotify 接口机制。Go 的标准库中对其做了实现。而 fsnotify package 的主要作用就是将进一步封装成 watcher 结构体和事件类型结构体的封装,从而实现事件的判断以及目录的监听。下面看下 fsnotify package 中对 watcher 的封装。

type Watcher struct {

    mu sync.Mutex // Map access

    fd int // File descriptor (as returned by the inotify_init() syscall)

    watches map[string]*watch // Map of inotify watches (key: path)

    fsnFlags map[string]uint32 // Map of watched files to flags used for filter

    fsnmut sync.Mutex // Protects access to fsnFlags.

    paths map[int]string // Map of watched paths (key: watch descriptor)

    Error chan error // Errors are sent on this channel

    internalEvent chan *FileEvent // Events are queued on this channel

    Event chan *FileEvent // Events are returned on this channel

    done chan bool // Channel for sending a "quit message" to the reader goroutine

    isClosed bool // Set to true when Close() is first called

}

linux 内核 Inotify 接口简介

inotify 中主要涉及 3 个接口。分别是 inotify_init, inotify_add_watch,read。具体如下:

接口名 作用
int fd = inotify_init() 创建 inotify 实例,返回对应的文件描述符
inotify_add_watch (fd, path, mask) 注册被监视目录或文件的事件
read (fd, buf, BUF_LEN) 读取监听到的文件事件

Inotify 可以监听的文件系统事件列表:

事件名称 事件说明
IN_ACCESS 文件被访问
IN_MODIFY 文件被 write
IN_CLOSE_WRITE 可写文件被 close
IN_OPEN 文件被 open
IN_MOVED_TO 文件被移来,如 mv、cp
IN_CREATE 创建新文件
IN_DELETE 文件被删除,如 rm
IN_DELETE_SELF 自删除,即一个可执行文件在执行时删除自己
IN_MOVE_SELF 自移动,即一个可执行文件在执行时移动自己
IN_ATTRIB 文件属性被修改,如 chmod、chown、touch 等
IN_CLOSE_NOWRITE 不可写文件被 close
IN_MOVED_FROM 文件被移走,如 mv
IN_UNMOUNT 宿主文件系统被 umount
IN_CLOSE 文件被关闭,等同于 (IN_CLOSE_WRITE
IN_MOVE 文件被移动,等同于 (IN_MOVED_FROM

示例应用

接下来是一个简易的示例应用,具体的应用实例可参考 github.com/silenceper/gowatch 包源代码 。 主要逻辑如下:

  1. 初始化 watcher 对象
  2. 将文件或目录加入到 watcher 监控对象的队列
  3. 启动监听协程,实时获取文件对象事件
package main

import (

    "fmt"

    "github.com/howeyc/fsnotify"
    "runtime"
)


var exit chan bool

func main() {
    //1、初始化监控对象watcher
    watcher, err := fsnotify.NewWatcher() 

    if err != nil {

        fmt.Printf("Fail to create new Watcher[ %s ]\n", err)
    }

    //3、启动监听文件对象事件协程
    go func() {
        fmt.Println("开始监听文件变化")
        for {
            select {
            case e := <-watcher.Event:
                // 这里添加根据文件变化的业务逻辑
                fmt.Printf("监听到文件 - %s变化\n", e.Name)
                if e.IsCreate() {
                    fmt.Println("监听到文件创建事件")
                }
                if e.IsDelete() {
                    fmt.Println("监听到文件删除事件")
                }
                if e.IsModify() {
                    fmt.Println("监听到文件修改事件")
                }
                if e.IsRename() {
                    fmt.Println("监听到文件重命名事件")
                }
                if e.IsAttrib() {
                    fmt.Println("监听到文件属性修改事件")
                }

                fmt.Println("根据文件变化开始执行业务逻辑")

            case err := <-watcher.Error:

                fmt.Printf(" %s\n", err.Error())
            }
        }
    }()
    // 2、将需要监听的文件加入到watcher的监听队列中
    paths := []string{"config.yml"}

    for _, path := range paths {

        err = watcher.Watch(path) //将文件加入监听

        if err != nil {

            fmt.Sprintf("Fail to watch directory[ %s ]\n", err)
        }
    }

    <-exit
    runtime.Goexit()
}
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册