刚开始接触go时,发现go程序和php程序的其中一个不同是php是解释性语言,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_CLOSE_NOWRITE) |
IN_MOVE | 文件被移动,等同于(IN_MOVED_FROM | IN_MOVED_TO) |
示例应用
接下来是一个简易的示例应用,具体的应用实例可参考github.com/silenceper/gowatch包源代码 。
主要逻辑如下:
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()
}