Video Coming Soon...

Created by Zed A. Shaw Updated 2025-11-14 00:08:54

39: head and tail

The tail command prints out the tail of a file or input stream, which becomes insanely useful when you have to manage your own systems. You'll be "tailing that log" all the time, which means using tail to get the last 20 or so lines of a log file.

The head command rarely shows up in my daily Linux usage, but it's actually the easier of the two to implement which is why I have you start with head.To implement head you count how many lines, then stop when you read the required number. With tail you have to "eat" lines until you reach the end, then print the last 20.

The Challenge

You don't have to be efficient with your implementation, but try to get as many command line options working as possible. If you want to go for extra points you'll try to find a way to make tail not use too much memory. My implementation collects the entire contents of a file into memory, then prints only the tail. See if you can do better.

The documentation is here:

Requirements

Nothing new here, just the usual file I/O packages.

See the list of requirements

The head Code

As I mentioned before head is easy, but if you're stuck take a peek:

See my first version of headView Source file go-coreutils/head/main.go Only

package main

import (
  "fmt"
  "bufio"
  "os"
  "log"
  "flag"
  "io"
)

func HeadFile(file io.Reader, count int) {
  scan := bufio.NewScanner(file)

  for cur_line := 0; scan.Scan(); cur_line++ {
    line := scan.Text()

    if cur_line < count {
      fmt.Println(line)
    }
  }

  if scan.Err() != nil { log.Fatal(scan.Err()) }
}

func main() {
  var count int

  flag.IntVar(&count, "n", 10, "number of lines")
  flag.Parse()

  if flag.NArg() > 0 {
    files := flag.Args()
    for _, fname := range files {
      file, err := os.Open(fname)
      if err != nil { log.Fatal(err) }

      HeadFile(file, count)
    }
  } else {
    HeadFile(os.Stdin, count)
  }
}

The tail Code

The tail command is harder, since you have to append to a list and then print the tail from that. However, this is a junk way to implement tail, so maybe you can do better?

See my first version of tailView Source file go-coreutils/tail/main.go Only

package main

import (
  "fmt"
  "bufio"
  "os"
  "log"
  "flag"
  "io"
)

func TailFile(file io.Reader, count int) {
  scan := bufio.NewScanner(file)
  var lines []string

  for scan.Scan() {
    lines = append(lines, scan.Text())
  }

  if scan.Err() != nil { log.Fatal(scan.Err()) }

  start := len(lines) - count
  if start < 0 { start = 0 }

  for line := int(start); line < len(lines); line++ {
    fmt.Println(lines[line])
  }
}

type Opts struct {
  Count int
  Files []string
}

func ParseOpts() Opts {
  var opts Opts

  flag.IntVar(&opts.Count, "n", 10, "number of lines")
  flag.Parse()

  opts.Files = flag.Args()

  return opts
}

func main() {
  opts := ParseOpts()

  if len(opts.Files) > 0 {
    for _, fname := range opts.Files {
      file, err := os.Open(fname)
      if err != nil { log.Fatal(err) }

      TailFile(file, opts.Count)
    }
  } else {
    TailFile(os.Stdin, opts.Count)
  }
}

HINT Read about ring buffers.

Testing It

If you want to push your learning further then you can try to implement an automated test for this. I actually need to learn how to test utilities like this with Go, so for now just consider this an extra challenge for later until I learn how to teach it.

Previous Lesson Next Lesson

Register for Learn Go the Hard Way

Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.