go
75 lines · 10 steps
How a tail -f follower works in Go
Streaming new lines from a growing file by seeking to the end and polling for fresh data, with context-aware cancellation.
Explained by
highlit
1package logtail
2
3import (
4 "bufio"
5 "context"
6 "errors"
7 "io"
8 "os"
9 "time"
10)
11
12type Line struct {
13 Offset int64
14 Text string
15}
16
17func Tail(ctx context.Context, path string, out chan<- Line) error {
18 f, err := os.Open(path)
19 if err != nil {
20 return err
21 }
22 defer f.Close()
23
24 if _, err := f.Seek(0, io.SeekEnd); err != nil {
25 return err
26 }
27
28 reader := bufio.NewReader(f)
29 ticker := time.NewTicker(200 * time.Millisecond)
30 defer ticker.Stop()
31
32 var pending []byte
33 for {
34 line, err := reader.ReadBytes('\n')
35 switch {
36 case err == nil:
37 offset, _ := f.Seek(0, io.SeekCurrent)
38 select {
39 case out <- Line{Offset: offset, Text: string(pending) + string(line[:len(line)-1])}:
40 case <-ctx.Done():
41 return ctx.Err()
42 }
43 pending = pending[:0]
44 case errors.Is(err, io.EOF):
45 pending = append(pending, line...)
46 if err := waitForData(ctx, f, ticker.C); err != nil {
47 return err
48 }
49 default:
50 return err
51 }
52 }
53}
54
55func waitForData(ctx context.Context, f *os.File, tick <-chan time.Time) error {
56 for {
57 select {
58 case <-ctx.Done():
59 return ctx.Err()
60 case <-tick:
61 info, err := f.Stat()
62 if err != nil {
63 return err
64 }
65 pos, _ := f.Seek(0, io.SeekCurrent)
66 if info.Size() > pos {
67 return nil
68 }
69 if info.Size() < pos {
70 f.Seek(0, io.SeekStart)
71 return nil
72 }
73 }
74 }
75}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Seeking to the end before reading lets you stream only new content, mimicking tail -f.
- 2Partial reads at EOF must be buffered and prepended once the rest of the line arrives.
- 3Polling with a ticker and watching file size handles growth and truncation without busy-looping.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-tail-f-follower-works-in-go-explained-go-8314/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.