Go: "tail -f" -подобный генератор

У меня была эта удобная функция в Python:

def follow(path):
    with open(self.path) as lines:
        lines.seek(0, 2)  # seek to EOF

        while True:
            line = lines.readline()
            if not line:
                time.sleep(0.1)
                    continue
                yield line 

Он делает что-то похожее на UNIX tail -f: вы получаете последние строки файла по мере их поступления. Это удобно, потому что вы можете получить генератор без блокировки и передать его другой функции.

Тогда я должен был сделать то же самое в Го. Я новичок в этом языке, поэтому я не уверен, что то, что я сделал, является идиоматическим/правильным для Go.

Вот код:

func Follow(fileName string) chan string {

    out_chan := make(chan string)

    file, err := os.Open(fileName)
    if err != nil {
        log.Fatal(err)
    }

    file.Seek(0, os.SEEK_END)
    bf := bufio.NewReader(file)

    go func() {
        for {
            line, _, _ := bf.ReadLine()

            if len(line) == 0 {
                time.Sleep(10 * time.Millisecond)
            } else {
                out_chan <- string(line)
            }
        }

        defer file.Close()
        close(out_chan)
    }()

    return out_chan
}

Есть ли более чистый способ сделать это в Go? У меня такое чувство, что использование асинхронного вызова для такой вещи является излишним, и это действительно беспокоит меня.

Ответ 1

Я предлагаю создать обертку вокруг читателя, который спит на EOF:

type tailReader struct {
    io.ReadCloser
}

func (t tailReader) Read(b []byte) (int, error) {
    for {
        n, err := t.ReadCloser.Read(b)
        if n > 0 {
            return n, nil
        } else if err != io.EOF {
            return n, err
        }
        time.Sleep(10 * time.Millisecond)
    }
}

func newTailReader(fileName string) (tailReader, error) {
    f, err := os.Open(fileName)
    if err != nil {
        return tailReader{}, err
    }

    if _, err := f.Seek(0, 2); err != nil {
        return tailReader{}, err
    }
    return tailReader{f}, nil
}

Этот ридер можно использовать везде, где можно использовать io.Reader. Вот как перебирать строки, используя bufio.Scanner:

t, err := newTailReader("somefile")
if err != nil {
    log.Fatal(err)
}
defer t.Close()
scanner := bufio.NewScanner(t)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading:", err)
}

Считыватель также можно использовать для циклического перемещения значений JSON, добавляемых в файл:

t, err := newTailReader("somefile")
if err != nil {
    log.Fatal(err)
}
defer t.Close()
dec := json.NewDecoder(t)
for {
    var v SomeType
    if err := dec.Decode(&v); err != nil {
       log.Fatal(err)
    }
    fmt.Println("the value is ", v)
}

У этого подхода есть несколько преимуществ по сравнению с подходом, используемым в goroutine. Во-первых, отключение легко. Просто закройте файл. Там нет необходимости сигнализировать goroutine, что он должен выйти. Второе преимущество заключается в том, что многие пакеты работают с io.Reader.