Why must a Go read loop call net.Conn.SetReadDeadline again before every read?
answer
- wall clock, not a stopwatch
- one setting affects every later read
- after it passes, reads fail instantly
- refresh with time.Now().Add per frame
- the zero time.Time clears it
basics
~20 sA net.Conn deadline is an absolute wall-clock instant, not a per-call timeout. Once that instant passes, every later read fails immediately instead of waiting, so a loop must set a fresh time.Now().Add(...) before each read.
solid answer
~40 s`SetReadDeadline` takes a `time.Time`, not a `time.Duration`. It marks a point on the clock after which reads on that connection stop blocking and return an error, and it applies to reads already in flight as well as future ones. It is a property of the connection, not of one call, so it does not reset itself when a `Read` returns. If you set it once at dial time to `time.Now().Add(30*time.Second)`, the connection reads fine for thirty seconds and then every read fails instantly forever. The idiom is therefore to call `conn.SetReadDeadline(time.Now().Add(d))` at the top of each loop iteration, so one deadline covers one logical unit of work — for a length-prefixed protocol, the 4-byte header plus the body that follows. Passing the zero `time.Time` clears the deadline entirely.
code
go · 14 linesfunc readFrame(c net.Conn) ([]byte, error) {
if err := c.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
return nil, err
}
var n uint32
if err := binary.Read(c, binary.BigEndian, &n); err != nil {
return nil, err
}
buf := make([]byte, n)
if _, err := io.ReadFull(c, buf); err != nil {
return nil, err
}
return buf, nil
}go deeper
Recall the signature: it takes a time.Time, so you write time.Now().Add(d). Be ready to say that the deadline belongs to the connection and does not reset itself after a successful read.
Explain that a deadline applies to pending and future I/O, that a past instant fails reads immediately, and that the zero time.Time disables it. Show where in a read loop you would place the call.
Demonstrate that you size deadlines from protocol behaviour — heartbeat intervals for a stream, latency targets for request/response — and that you treat a timeout mid-frame as fatal for the connection rather than retrying blindly.
Own the convention across the codebase: whether wire layers expose a deadline knob or derive it, and how you keep a single team from inventing three different timeout schemes on connections other services depend on.
## The three methods Every `net.Conn` — TCP, Unix-domain, UDP `net.PacketConn` and the connections `crypto/tls` wraps — carries three deadline methods: ``` SetDeadline(t time.Time) error // both directions SetReadDeadline(t time.Time) error // reads only SetWriteDeadline(t time.Time) error // writes only ``` The argument is a **`time.Time`**, an absolute instant, not a `time.Duration`. This is the single most misread part of the API: engineers coming from socket libraries that take `SO_RCVTIMEO`-style durations expect "give me five seconds per read" and instead get "fail all reads after 15:04:05.999". ## What the deadline is attached to The deadline is state on the connection, not an argument to a call. Setting it affects **all pending and future I/O** in that direction — including a `Read` that another goroutine is already blocked in — and it stays set until you change it. Nothing resets it when a read completes successfully. So the lifecycle of a deadline is entirely in your hands: * **future instant** — reads block normally until data arrives or that instant is reached, whichever comes first; * **instant already passed** — reads return an error immediately without touching the network; * **zero value (`time.Time{}`)** — no deadline; reads block indefinitely. After a deadline has been exceeded the connection is not closed or poisoned: setting a new deadline in the future makes it usable again. ## Why the loop must refresh it Consider a client for a long-lived framed protocol that sets one deadline after dialing and then loops reading frames. For the first `d` of the connection's life everything works. The instant the clock passes the deadline, every subsequent read returns a timeout error with no delay — the client spins through an error loop at full CPU, and the failure looks nothing like a network problem because it is not one. The mirror image is a connection with no deadline at all, which blocks forever the moment the peer stops sending. The correct shape is to set the deadline once per logical read unit: ``` func readFrame(c net.Conn) ([]byte, error) { if err := c.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { return nil, err } var n uint32 if err := binary.Read(c, binary.BigEndian, &n); err != nil { return nil, err } buf := make([]byte, n) _, err := io.ReadFull(c, buf) return buf, err } ``` Notice that one deadline spans **two** read operations here — the length header and the body — and possibly many more syscalls underneath, because `io.ReadFull` loops. That is usually what you want: the budget belongs to "receive one frame", not to "one call into the kernel". If you wanted a separate budget for the body you would set the deadline again after reading the header. ## Recognising the error A read that hits its deadline returns an error that satisfies the `net.Error` interface with `Timeout()` reporting true, and that matches `errors.Is(err, os.ErrDeadlineExceeded)`. Both are worth knowing: the interface assertion is the old idiom, the sentinel comparison is the modern one and works through wrapping. ## Resumability after a timeout The connection survives a deadline, but your **protocol state** may not. If the timeout fired after `io.ReadFull` had already consumed nine of a frame's twelve bytes, those bytes are gone and the stream is now misaligned — the next read would interpret body bytes as a length header. For any framed protocol, treat a timeout that fires mid-frame as fatal for that connection and reconnect. Only a read that consumed nothing (a timeout while idle, waiting for the next frame to begin) is safe to retry on the same connection. ## Choosing the value The deadline should come from the protocol, not from a general intuition about "slow networks". For a request/response exchange it is the worst acceptable server latency. For a long-lived stream that is idle most of the time, a five-second read deadline would fire constantly; the right value is derived from the heartbeat interval — typically enough time for two missed heartbeats — so an idle connection is still proven alive without being torn down. ## Writes deserve the same treatment `SetWriteDeadline` matters less often, because a write into an open TCP send window returns quickly, but it is not optional. When the peer stops reading, the window fills and `Write` blocks exactly as `Read` does. A wire layer that bounds only reads still has a way to hang.
- How do you tell a deadline error apart from any other read failure?The error satisfies the `net.Error` interface with `Timeout()` returning true, and `errors.Is(err, os.ErrDeadlineExceeded)` matches it. Prefer the sentinel check, since it keeps working when a driver wraps the error with `fmt.Errorf("%w", ...)`. A connection closed by another goroutine gives a different error and reports `Timeout()` false.
- After a read times out, can you keep using the connection?The socket itself is fine — set a future deadline and it works again. Your protocol state may not be. If the timeout fired part way through a framed message, the consumed bytes are lost and the stream is misaligned, so reconnect. Only a timeout that consumed nothing, while idle between messages, is safe to retry on the same connection.
- What does SetDeadline do that the read and write variants do not?`SetDeadline` sets both directions to the same instant in one call, which is convenient for a strict request/response exchange with a single overall budget. Separate read and write deadlines are better for a long-lived stream, where the idle read budget is large and the write budget stays short.
It is a parking meter's expiry time, not a countdown you restart by walking back to the car: once the printed time passes, every attempt is refused until you buy a new one.
saying these in an interview costs you the question
- Treats SetReadDeadline as a per-call timeout duration
- Expects a time.Duration argument rather than an instant
- Sets one deadline right after Dial for the connection's whole life
- Thinks an expired deadline makes reads block rather than fail at once
- Assumes a timed-out read can always resume mid-message
- Never sets a write deadline because writes 'do not block'