skip to content

In io.Seeker, what do the whence values io.SeekStart, io.SeekCurrent and io.SeekEnd mean?

level: juniorimportance: must knowfreq 52%

answer

  1. three origins, one cursor
  2. start, current, end
  3. offset is negative for one of them
  4. the return value is always absolute
  5. zero from the end gives the size

basics

~20 s

The whence value says what the offset is measured from: io.SeekStart from the beginning of the file, io.SeekCurrent from the position you are at now, io.SeekEnd from the end. Seek returns the new absolute offset.

solid answer

~40 s

The interface method is `Seek(offset int64, whence int) (int64, error)`. `whence` picks the origin the `offset` is added to: `io.SeekStart` (0) means from the beginning, `io.SeekCurrent` (1) from wherever the cursor is now, `io.SeekEnd` (2) from the end, so `offset` is normally negative there. Whatever origin you pick, the returned value is always the new position measured from the start of the file, which gives two useful idioms: `f.Seek(0, io.SeekCurrent)` tells you where you are without moving, and `f.Seek(0, io.SeekEnd)` gives the size in bytes. Seeking to a negative resulting offset is an error; seeking past the end is legal, and writing there leaves a zero-filled hole. Seek does not read or write anything itself, and it fails on a stream that has no position, such as a pipe.

code

go · 8 lines
go
// where am I? (offset 0 from current: a no-op move)
pos, err := f.Seek(0, io.SeekCurrent)

// how big is the file? (leaves the cursor at the end)
size, err := f.Seek(0, io.SeekEnd)

// park on an 8-byte trailer at the very end
off, err := f.Seek(-8, io.SeekEnd) // off == size-8

go deeper

for a junior

Memorise the three constants and what each measures from, and be able to say that the value Seek returns is the new position counted from the start. Know that Seek moves a cursor and transfers no data.

for a middle

Explain the two idioms built on the absolute return value: zero from current reports the position, zero from the end reports the size. Be ready to describe the sparse hole that a seek past the end plus a write creates.

for a senior

Show that you know a cursor belongs to the open file, not to a goroutine, and that a wrapper which has buffered ahead is invalidated when you move the offset underneath it. Those are the two ways seeking bites in production code.

for a principal

Frame seekability as a capability you either require in a signature or must not assume. Deciding that a component takes a positioned source rather than a plain stream is an API commitment that propagates to every caller.

## The interface `io.Seeker` is a one-method interface in the standard library's `io` package: ```go type Seeker interface { Seek(offset int64, whence int) (int64, error) } ``` A type that implements it has a **cursor**: a byte position that says where the next `Read` or `Write` will happen. `*os.File`, `*bytes.Reader` and `*strings.Reader` all have one. `Seek` moves that cursor and nothing else — it transfers no bytes. ## whence: the origin the offset is measured from `offset` alone is ambiguous: 100 bytes from where? `whence` answers that, and the `io` package defines exactly three values for it: | constant | value | origin | |---|---|---| | `io.SeekStart` | 0 | the beginning of the file | | `io.SeekCurrent` | 1 | the current cursor position | | `io.SeekEnd` | 2 | the end of the file | So `f.Seek(100, io.SeekStart)` puts the cursor on byte 100. `f.Seek(100, io.SeekCurrent)` skips 100 bytes forward from wherever you are. `f.Seek(-8, io.SeekEnd)` puts the cursor 8 bytes before the end — the usual way to read a trailer that records how many entries a file holds. Offsets may be negative with `io.SeekCurrent` and `io.SeekEnd`; with `io.SeekStart` a negative offset is meaningless and returns an error. Use the named constants rather than the literals 0, 1 and 2. The numbers come from the underlying `lseek(2)` system call, but code that writes `f.Seek(0, 2)` is code a reviewer has to decode. ## What Seek returns The first return value is **the new offset, always measured from the start of the file**, regardless of which `whence` you passed. That normalisation is what makes two idioms work: - `pos, _ := f.Seek(0, io.SeekCurrent)` — a no-op move that reports the current position. - `size, _ := f.Seek(0, io.SeekEnd)` — the file's size in bytes, at the cost of leaving the cursor parked at the end. A very common misreading is to treat the return as "how far I moved" or "how many bytes were read". Neither is true: nothing is read, and the number is absolute, not relative. ## Seeking past the end Moving the cursor beyond the current end of a file is not an error. If you then write, the operating system creates a **hole**: the gap between the old end and your write reads back as zero bytes, and on most file systems it consumes no disk blocks until written. This is how sparse files are made. Reading in that gap before anything is written there simply returns zeros up to the new end, and reading past the end returns `io.EOF`. ## Streams with no position Not every source has a cursor. Seeking a pipe, a terminal, or a socket fails — the operating system reports `ESPIPE`, which surfaces as an error from `Seek`. That is not a bug to work around; it reflects that those sources have no addressable content, only a sequence of bytes that arrives once. This is also why `io.Seeker` is a separate interface from `io.Reader`: seekability is a property of some implementations, not of readers in general. ## Two cautions in real code First, if something has buffered ahead of the cursor — a `bufio.Reader` wrapped around the same `*os.File`, say — moving the file's offset underneath it silently corrupts its view, because the buffer still holds bytes from the old position. Move the cursor before you wrap, or discard and rebuild the wrapper afterwards. Second, a file offset is **per open file description, not per goroutine**. Two goroutines sharing one `*os.File` share one cursor, so a `Seek` followed by a `Read` is not an atomic pair. That is the reason `io.ReaderAt` exists, where the offset is passed as an argument instead of being stored. ## What an interviewer is checking That you know the three constants and what each measures from, that you know the return value is absolute, and that you do not believe `Seek` moves data. The follow-up is usually about how you would read a fixed-size trailer, which `io.SeekEnd` with a negative offset answers in one line.

  • How do you read the last 8 bytes of a file without knowing its size?
    `f.Seek(-8, io.SeekEnd)` puts the cursor 8 bytes before the end and returns that absolute offset, then read 8 bytes. There is no need to ask for the size first: a negative offset with `io.SeekEnd` is exactly the case that whence value exists for.
  • What happens if you Seek past the end of a file and then write?
    It is legal. The gap between the old end and your write becomes a hole that reads back as zero bytes, and on most file systems it occupies no blocks until something is written into it. Reading in the gap returns zeros; reading past the new end returns `io.EOF`.
  • Why does Seek fail on a pipe or a terminal?
    Those sources have no addressable content — bytes arrive once and are gone, so there is no position to move to. The operating system rejects the seek and `Seek` returns an error. It is the concrete reason `io.Seeker` is a separate interface rather than part of `io.Reader`.

Whence is the landmark you count from: the front door, where you are standing, or the back wall. However you count, the answer you get back is the distance from the front door.

saying these in an interview costs you the question

  • Says Seek returns how many bytes the cursor moved
  • Thinks Seek reads bytes as well as moving the cursor
  • Uses io.SeekEnd with a positive offset to reach a trailer
  • Believes seeking past the end of a file is an error
  • Writes the literals 0, 1 and 2 instead of the named constants