go 源码学习之---Tail 源码分析
已经有两个月没有写博客了,也有好几个月没有看go相关的内容了,由于工作原因最近在做java以及大数据相关的内容,导致最近工作较忙,博客停止了更新,正好想捡起之前go的东西,所以找了一个源码学习
这个也是之前用go写日志收集的时候用到的一个包 :github.com/hpcloud/tail, 这次就学习一下人家的源码,为了方便看这个代码,我将这个包进行了简化,也是用于方便理解,代码放到了:https://github.com/pythonsite/tail, 这个代码包可能无法正常用,只是为了方面理解tail这个包,以及学习人家的代码
精简后的代码目录
│ tail.go
│
└─watch
filechanges.go
inotify.go
inotify_tracker.go
watch.go
tail.go: 这里包含着tail包的核心代码,主要的逻辑处理时在这个里面
watch: 这个包主要用于对文件的监控,用于将文件的变化通知到tail.如:文件修改了,文件删除了,文件内容追加了
tail.go 代码分析
在tail.go中主要有几下几个结构体:
// Line 结构体用于存读每行的时候的对象
type Line struct {
Text string //当前行的内容
Time time.Time // 时间
Err error // Error from tail
} type SeekInfo struct {
Offset int64
Whence int
} // 关于配置的结构体
type Config struct {
Location *SeekInfo
ReOpen bool
MustExist bool // 要打开的文件是否必须存在
Poll bool Pipe bool Follow bool // 是否继续读取新的一行,可以理解为tail -f 命令 } // 核心的结构体Tail
type Tail struct {
Filename string // 要打开的文件名
Lines chan *Line // 用于存每行内容的Line结构体 Config watcher watch.FileWatcher
changes *watch.FileChanges tomb.Tomb file *os.File
reader *bufio.Reader
lk sync.Mutex
}
tail,err := tail.TailFile(conf.LogPath,tail.Config{
ReOpen:true,
Follow:true,
Location:&tail.SeekInfo{Offset:,Whence:},
MustExist:false,
Poll:true,
})
既然我们使用的时候就会在最开始的时候调用tail.TailFile方法,就直接看这个方法:
// 主要用于Tail结构体的初始化
func TailFile(filename string, config Config) (*Tail, error) {
t := &Tail {
Filename: filename,
Lines: make(chan *Line),
Config: config,
}
t.watcher = watch.NewInotifyFileWatcher(filename)
if t.MustExist {
var err error
t.file, err = OpenFile(t.Filename)
if err != nil {
return nil, err
}
}
go t.tailFileSync() return t, nil
}
从这个代码里我们就可以看到它首先初始化了Tail结构体并且对Tail中的watcher进行的复制,先暂时不看watch相关的内容
然后就是关于文件是否必须存在的判断处理,最后开启了一个一个线程执行tailFileSync()方法,我们接着看tailFileSync方法
func (tail *Tail) tailFileSync(){
defer tail.Done()
defer tail.close() if !tail.MustExist {
err := tail.reopen()
if err != nil {
if err != tomb.ErrDying {
tail.Kill(err)
}
return
}
} tail.openReader() var offset int64
var err error
// 一行行读文件内容
for { if !tail.Pipe {
offset,err = tail.Tell()
if err != nil {
tail.Kill(err)
return
}
} line, err := tail.readLine()
if err == nil {
// 将读取的一行内容放到chan中
tail.sendLine(line)
} else if err == io.EOF {
// 表示读到文件的最后了
// 如果Follow 设置为false的话就不会继续读文件
if !tail.Follow {
if line != "" {
tail.sendLine(line)
}
return
}
// 如果Follow设置为True则会继续读
if tail.Follow && line != "" {
err := tail.seekTo(SeekInfo{Offset: offset, Whence: })
if err != nil {
tail.Kill(err)
return
}
}
// 如果读到文件最后,文件并没有新的内容增加
err := tail.waitForChanges()
if err != nil {
if err != ErrStop {
tail.Kill(err)
}
return
} } else {
// 既不是文件结尾,也没有error
tail.Killf("error reading %s :%s", tail.Filename, err)
return
} select {
case <- tail.Dying():
if tail.Err() == errStopAtEOF {
continue
}
return
default:
}
}
}
这个方法里主要是先调用了openReader方法,这个方法其实并没有做什么,只是对tail.reqader进行了赋值:tail.reader = bufio.NewReader(tail.file)
接着就是循环一行行的读文件
在循环里最开始判断了tail.Pipe的值,这个值一般开始我也并不会设置,所以默认就是false,所以就会执行tail.Tell()方法,这个方法主要是用于获取文件当前行的位置信息,下面是Tell的代码内容:
// 获取文件当前行的位置信息
func (tail *Tail) Tell()(offset int64, err error) {
if tail.file == nil {
return
}
offset, err = tail.file.Seek(, os.SEEK_CUR)
if err != nil {
return
}
tail.lk.Lock()
defer tail.lk.Unlock()
if tail.reader == nil {
return
}
offset -= int64(tail.reader.Buffered())
return
}
接着会调用tail.readLine()方法,这个方法就是用于获取文件的一行内容,同时将一行内容实例化为Line对象,然后扔到管道tail.Lines中
//将读取的文件的每行内容存入到Line结构体中,并最终存入到tail.Lines的chan中
func (tail *Tail) sendLine(line string) bool {
now := time.Now()
lines := []string{line} for _, line := range lines {
tail.Lines <- &Line {
line,
now,
nil,
}
}
return true
}
最后的大量if 判断其实主要是针对读到文件末尾后的一些操作,
Tail结构体在最后定义的时候有一个参数:Follow, 这个参数的目的就是当读到文件最后的时候是否继续读文件, 如果最开始设置了false,那么读到最后之后就不会在读文件了
如果设置为True,那么读到文件最后之后会保存文件的位置信息,并执行waitForChanges() 去等待文件的变化,waitForChanges()代码内容如下:
// 等待文件的变化事件
func (tail *Tail) waitForChanges() error {
if tail.changes == nil {
// 这里是获取文件指针的当前位置
pos, err := tail.file.Seek(,os.SEEK_CUR)
if err != nil {
return err
}
tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos)
if err != nil {
return err
}
}
// 和inotify中进行很巧妙的配合,这里通过select 来进行查看那个chan变化了,来知道文件的变化
select {
case <- tail.changes.Modified: // 文件被修改
return nil
case <- tail.changes.Deleted: // 文件被删除或者移动到其他目录
tail.changes = nil
// 如果文件被删除或者被移动到其他目录,则会尝试重新打开文件
if tail.ReOpen {
fmt.Printf("Re-opening moved/deleted file %s...",tail.Filename)
if err := tail.reopen();err != nil {
return err
}
fmt.Printf("Successfully reopened %s", tail.Filename)
tail.openReader()
return nil
} else {
fmt.Printf("Stoping tail as file not longer exists: %s", tail.Filename)
return ErrStop
}
case <- tail.changes.Truncated: // 文件被追加新的内容
fmt.Printf("Re-opening truncated file %s....", tail.Filename)
if err := tail.reopen();err != nil {
return err
}
fmt.Printf("SuccessFuly reopend truncated %s", tail.Filename)
tail.openReader()
return nil
case <- tail.Dying():
return nil
}
panic("unreachable")
}
看到这里的时候其实就能感觉到,别人写的代码其实也并不是非常复杂,也是很普通的代码,但是你会觉得人家很多地方用的非常巧妙,
这段代码中主要的是的内容就是select部分,这个部分通过select监控
从而知道文件的变化,是修改了,还是删除了,还是追加内容了,这几个其实都是一个channel,这几个channel中的内容是怎么放进去的呢,接下来看watch包中的内容
watch包代码分析
首先先看一下watch包中的watch.go,这个里面其实就是定一个了一个FileWatcher的接口
type FileWatcher interface {
BlockUntilExists(*tomb.Tomb) error ChangeEvents(*tomb.Tomb, int64) (*FileChanges, error)
}
接着我们看一下inotify.go文件,这个里面我们就可以看到定一个InotifyFileWatcher结构体,并且实现了FileWatcher 这个接口
type InotifyFileWatcher struct {
Filename string
Size int64
} func NewInotifyFileWatcher(filename string) *InotifyFileWatcher {
fw := &InotifyFileWatcher {
filepath.Clean(filename),
,
}
return fw
} // 关于文件改变事件的处理,当文件被修改了或者文件内容被追加了,进行通知
func (fw *InotifyFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) {
err := Watch(fw.Filename)
if err != nil {
return nil, err
} changes := NewFileChanges()
fw.Size = pos go func() {
events := Events(fw.Filename) for {
prevSize := fw.Size var evt fsnotify.Event
var ok bool select {
case evt, ok = <- events:
if !ok {
RemoveWatch(fw.Filename)
return
}
case <- t.Dying():
RemoveWatch(fw.Filename)
return
}
switch {
case evt.Op & fsnotify.Remove == fsnotify.Remove:
fallthrough
case evt.Op & fsnotify.Rename == fsnotify.Rename:
RemoveWatch(fw.Filename)
changes.NotifyDeleted()
return case evt.Op & fsnotify.Chmod == fsnotify.Chmod:
fallthrough
case evt.Op & fsnotify.Write == fsnotify.Write:
fi, err := os.Stat(fw.Filename)
if err != nil {
// 文件如果被删除了通知文件删除到chan
if os.IsNotExist(err) {
RemoveWatch(fw.Filename)
changes.NotifyDeleted()
return
} }
fw.Size = fi.Size() if prevSize > && prevSize > fw.Size {
// 表示文件内容增加了
changes.NotifyTruncated()
} else {
// 表示文件被修改了
changes.NotifyModified()
} prevSize = fw.Size
} }
}()
return changes, nil
} func (fw *InotifyFileWatcher) BlockUntilExists(t *tomb.Tomb) error {
err := WatchCreate(fw.Filename)
if err != nil {
return err
}
defer RemoveWatchCreate(fw.Filename)
if _, err := os.Stat(fw.Filename);!os.IsNotExist(err) {
return err
}
events := Events(fw.Filename)
for {
select {
case evt, ok := <- events:
if !ok {
return fmt.Errorf("inotify watcher has been closed")
}
evtName, err := filepath.Abs(evt.Name)
if err != nil {
return err
}
fwFilename, err := filepath.Abs(fw.Filename)
if err != nil {
return err
}
if evtName == fwFilename {
return nil
}
case <- t.Dying():
return tomb.ErrDying
}
}
panic("unreachable")
}
实现的接口就两个方法:
type FileChanges struct {
Modified chan bool // 修改
Truncated chan bool // 增加
Deleted chan bool // 删除
} func NewFileChanges() *FileChanges {
return &FileChanges{
make(chan bool, ),
make(chan bool, ),
make(chan bool, ),
}
} func (fc *FileChanges) NotifyModified() {
sendOnlyIfEmpty(fc.Modified)
} func (fc *FileChanges) NotifyTruncated() {
sendOnlyIfEmpty(fc.Truncated)
} func (fc *FileChanges) NotifyDeleted() {
sendOnlyIfEmpty(fc.Deleted)
} func sendOnlyIfEmpty(ch chan bool) {
select {
case ch <- true:
default:
}
}
在这个里面也是可以学习到人家写的这个地方非常巧妙,虽然谈不上代码高达上,但是看着会让你很舒服,通过这个结构体,当文件被删除,修改和增加的时候就会让对应的channel中插入一个true,并且这里
的channel都是不带缓冲区的,只有当tail中触发一次之后,channel中的内容就会被获取出来,从而触发tail继续读文件的内容
go 源码学习之---Tail 源码分析的更多相关文章
- 【 js 基础 】【 源码学习 】backbone 源码阅读(一)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(二)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(source-code-study)进行参考交流,有详细的源码注释,以及知识总结,同时 ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(三)浅谈 REST 和 CRUD
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(三)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- Jetty源码学习-编译Jetty源码二三事
工作小几个月了,JDK基础和web应用框架学的的差不多了,开始学习Jetty源码,费了小半天才编译成功,把自己拆过的坑记录下来. 编译前的环境: MAVEN 3.3.Eclips eLuna Serv ...
- Java集合源码学习(三)LinkedList分析
前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ...
- elasticsearch5.5.3 源码学习 idea下源码编译
1.学习elasticsearch 源码,通过搜索“elasticsearch源码”,进行相关搜索. 2.因源码gradle编译,选择gradle-3.5可以编译通过,对应elasticsearc ...
- Java集合源码学习(四)HashMap分析
ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据 ...
- Java集合源码学习(二)ArrayList分析
>>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ...
随机推荐
- 其实我们可以少写点if else和switch
前言 作为搬砖在第一线的底层工人,业务场景从来是没有做不到只有想不到的复杂. 不过他强任他强,if-else全搞定,搬就完了.但是随着业务迭代或者项目交接,自己在看自己或者别人的if代码的时候,心情就 ...
- CSS-变量
为什么使用 css variables 借用Scrimba上的: easier to get started (no transpiling) have access to the DOM 1.loc ...
- python 多线程和多进程
多线程与多进程 知识预览 一 进程与线程的概念 二 threading模块 三 multiprocessing模块 四 协程 五 IO模型 回到顶部 一 进程与线程的概念 1.1 进程 考虑一个场景: ...
- Redis自学笔记:3.5入门-集合类型
3.5集合类型 3.5.1介绍 在集合中的每个元素都是不同的,且没有顺序 表3-4集合类型和列表类型的对比 - 集合类型 列表类型 存储内容 至多232-1个字符串 至多232-1个字符串 有序性 否 ...
- emitted value instead of an instance of error the scope attribute for scoped slots webpack babel polyfill
api20180803.vue emitted value instead of an instance of error the scope attribute for scoped slots h ...
- BZOJ.5249.[九省联考2018]iiidx(贪心 线段树)
BZOJ LOJ 洛谷 \(d_i\)不同就不用说了,建出树来\(DFS\)一遍. 对于\(d_i\)不同的情况: Solution 1: xxy tql! 考虑如何把这些数依次填到树里. 首先对于已 ...
- BZOJ.2597.[WC2007]剪刀石头布(费用流zkw)
BZOJ 洛谷 \(Description\) 给定一张部分边方向已确定的竞赛图.你需要给剩下的边确定方向,使得图中的三元环数量最多. \(n\leq100\). \(Solution\) 这种选择之 ...
- LOJ.6053.简单的函数(Min_25筛)
题目链接 Min_25筛见这里: https://www.cnblogs.com/cjyyb/p/9185093.html https://www.cnblogs.com/zhoushuyu/p/91 ...
- UOJ.35.[模板]后缀排序(后缀数组 倍增)
题目链接 论找到一个好的教程的正确性.. 后缀数组 下标从1编号: //299ms 2560kb #include <cstdio> #include <cstring> #i ...
- [CF1039D]You Are Given a Tree
[CF1039D]You Are Given a Tree 题目大意: 给定一棵\(n(n\le10^5)\)个节点的树.对于每一个正整数\(k(1\le k\le n)\),求最多能找出多少条包含\ ...