已经有两个月没有写博客了,也有好几个月没有看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
}
Line 结构体用于存读取文件的每行内容
Tail 是核心的结构体,我们使用tail这个包的时候其实就是会先调用初始化这个struct的方法TailFile,如我在写日志收集的时候的使用:
    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监控

tail.changes.Modified
tail.changes.Deleted
tail.changes.Truncated

从而知道文件的变化,是修改了,还是删除了,还是追加内容了,这几个其实都是一个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")
}

实现的接口就两个方法:

ChangeEvents: 这个主要是监控文件的变化,是删除了,还是被修改了,或者是文件,然后将状态信息通过调用:changes.NotifyTruncated()或者

changes.NotifyDeleted() 或者changes.NotifyModified() 将状态信息更新到channel中,这样我们在分析tail.go 中最后的分析的那部分channel中的数据,就是在这里
放进去的
BlockUntilExists:这个主要是关于文件不存在的时候,如果最开始的时候可以允许文件不存在,那么就会 在这里通过for循环一直等待,知道文件存在
 
再看看filechanges.go 文件,代码内容如下:
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 源码分析的更多相关文章

  1. 【 js 基础 】【 源码学习 】backbone 源码阅读(一)

    最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...

  2. 【 js 基础 】【 源码学习 】backbone 源码阅读(二)

    最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(source-code-study)进行参考交流,有详细的源码注释,以及知识总结,同时 ...

  3. 【 js 基础 】【 源码学习 】backbone 源码阅读(三)浅谈 REST 和 CRUD

    最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...

  4. 【 js 基础 】【 源码学习 】backbone 源码阅读(三)

    最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...

  5. Jetty源码学习-编译Jetty源码二三事

    工作小几个月了,JDK基础和web应用框架学的的差不多了,开始学习Jetty源码,费了小半天才编译成功,把自己拆过的坑记录下来. 编译前的环境: MAVEN 3.3.Eclips eLuna Serv ...

  6. Java集合源码学习(三)LinkedList分析

    前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ...

  7. elasticsearch5.5.3 源码学习 idea下源码编译

    1.学习elasticsearch 源码,通过搜索“elasticsearch源码”,进行相关搜索.   2.因源码gradle编译,选择gradle-3.5可以编译通过,对应elasticsearc ...

  8. Java集合源码学习(四)HashMap分析

    ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据 ...

  9. Java集合源码学习(二)ArrayList分析

    >>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ...

随机推荐

  1. vsftpd中配置文件详解

    在vsftp服务器中,配置文件/etc/vsftpd/vsftpd.conf文件是配置的核心内容,其具体的配置信息详细情况如下: 1.默认配置: 1>允许匿名用户和本地用户登陆. anonymo ...

  2. PHP.ini 文件大小限制

    post_max_size=80000000000M upload_max_filesize=2000000000M

  3. DOS简单文件指令

    DOS和Windows最大的不同在于DOS命令方式操作,所以使用者需要记住大量命令及其格式使用方法,DOS命令分为内部命令和外部命令, 内部命令是随每次启动的COMMAND.COM装入并常驻内存,而外 ...

  4. 数据源、数据集、同步任务、数据仓库、元数据、数据目录、主题、来源系统、标签、增量识别字段、修改同步、ES索引、HBase列族、元数据同步、

    数据源.数据集.同步任务.数据仓库.元数据.数据目录.主题.来源系统.标签. 增量识别字段.修改同步.ES索引.HBase列族.元数据同步.DS.ODS.DW.DM.zk集群地址 == 数据源 数据源 ...

  5. BZOJ5057 : 区间k小值5

    整体二分,按时间顺序依次考虑对于权值落在$[l,r]$内的所有操作. 对于每个修改操作,若权值范围完全包含了$[l,r]$,那么在更深层的分治中它都完全包含它,对每个询问的贡献是定值,因此在当前层将贡 ...

  6. Centos 7 安装配置

    修改网卡 最小化安装没有安装vim,暂时使用vi 进入到network-scripts 目录 [root@node1 ~]# cd /etc/sysconfig/network-scripts/ [r ...

  7. python字符串面试题:找出一个字符串中第一个字母和最后一个字符是第一次重复,中间没有重复且最长的子串

    1.给出任意一个字符串,打印一个最长子串字符串及其长度,如果有相同长度的子字符串,都要一起打印出来,该子字符串满足以下条件, 第一个字母和最后一个字符是第一次重复 这个子字符串的中间字母没有重复 这个 ...

  8. numpy快速入门

    numpy快速入门 numpy是python的科学计算的核心库,很多更高层次的库都基于numpy.博主不太喜欢重量级的MATLAB,于是用numpy进行科学计算成为了不二选择. 本文主要参考Scipy ...

  9. 删除office拥有多个都需要激活的授权信息

    首先确认office目录下存在“ospp.vbs”文件,可以搜索确认文件路径. 我的是在C:\Program Files\Microsoft Office\Office16  然后以管理员身份打开cm ...

  10. 学习Struts--Chap01:了解Struts2

    学习之前的一些话: 这是我系统学习Java知识这么久以来第一次接触web框架,很不幸的是刚开始学习它就听到很多人说这个框架现在已经过时了,很多企业现在开发项目几乎不会用这个框架了,就是有用这个框架的项 ...