skip to content

Sockets and Dialers

One layer below HTTP: net.Listen and net.Dial, per-connection SetDeadline, CloseWrite for half-close, and Go's two resolvers. Interviewers reach here to see how deep your networking goes.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

19

Why must a Go read loop call net.Conn.SetReadDeadline again before every read?

level: juniorimportance: must knowfreq 55%

answer

  1. wall clock, not a stopwatch
  2. one setting affects every later read
  3. after it passes, reads fail instantly
  4. refresh with time.Now().Add per frame
  5. the zero time.Time clears it

basics

~20 s

A 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 lines
go
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)
	if _, err := io.ReadFull(c, buf); err != nil {
		return nil, err
	}
	return buf, nil
}

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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'
open as a page

In Go, what does net.ListenPacket give you, and how does PacketConn.ReadFrom differ from reading a stream?

level: juniorimportance: must knowfreq 46%

basics

~20 s

net.ListenPacket binds a datagram socket and returns a net.PacketConn. There is nothing to accept: one ReadFrom call returns exactly one whole datagram plus the sender's address, while a stream read returns whatever bytes happen to have arrived.

open as a page

Why can one Read on a net.Conn return fewer bytes than the peer sent in one Write?

level: middleimportance: must knowfreq 58%

basics

~20 s

A net.Conn is a byte stream, not a message queue. Read returns whatever has arrived, so writes can split across reads or merge into one. The application supplies its own framing, such as newline-delimited lines or a length prefix.

open as a page

When does Go's net package use its pure-Go DNS resolver instead of the cgo one?

level: middleimportance: must knowfreq 40%

basics

~20 s

Go has two name resolvers: its own DNS client and one that calls the C library. The Go one is the default on Unix; Go falls back to C when the platform forbids direct queries or the host's resolver configuration needs features Go lacks.

open as a page

In Go, what does net.Listen return, and how do you serve clients from it?

level: juniorimportance: should knowfreq 54%

basics

~20 s

net.Listen returns a net.Listener bound to the given address. You call its Accept method in a loop: each call blocks until a client connects and returns a net.Conn you read from, write to and close, usually in its own goroutine.

open as a page

What does net.LookupHost return, and how do you tell a nonexistent hostname from a resolver failure?

level: juniorimportance: should knowfreq 45%

basics

~20 s

net.LookupHost takes a hostname and returns a slice of IP address strings plus an error. To tell a missing name from a broken resolver, unwrap the error to *net.DNSError with errors.As and check its IsNotFound field.

open as a page

Why parse a net.Conn's RemoteAddr with net.SplitHostPort instead of splitting on a colon?

level: middleimportance: should knowfreq 41%

basics

~20 s

An address string can be IPv6, where the host itself is full of colons and is wrapped in brackets, as in [2001:db8::1]:8080. net.SplitHostPort understands that form and strips the brackets; splitting on a colon corrupts it. net.JoinHostPort rebuilds the string correctly.

open as a page

How do you unblock a goroutine that is already blocked in Read on a net.Conn?

level: middleimportance: should knowfreq 45%

basics

~20 s

From another goroutine, call SetReadDeadline with an instant already in the past, such as time.Now(). Deadlines apply to in-flight operations, so the blocked Read returns a timeout error at once, and the connection stays usable if you set a future deadline afterwards.

open as a page

Why does a Go UDP agent reading into a 512-byte buffer silently lose part of a 1400-byte datagram?

level: middleimportance: should knowfreq 40%

basics

~20 s

A datagram read is all or nothing: ReadFrom copies at most len(buf) bytes and the kernel discards the rest of that message. You get n of 512, a nil error, and the other 888 bytes are gone.

open as a page

A Go daemon exits its accept loop on any error from net.Listener.Accept. What breaks?

level: seniorimportance: should knowfreq 44%

basics

~20 s

One transient failure kills the whole server: the loop returns and the process stops accepting connections. Accept errors need triage — return only when the listener was deliberately closed, and otherwise log, pause briefly and continue.

open as a page

A Go CLI hangs forever in a net.Conn Read against a dead peer, with the read deadline cleared after the handshake. How do you diagnose and fix it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Send SIGQUIT to dump goroutine stacks: the reader sits in IO wait inside a socket read for minutes. With no deadline, a peer that dies without sending FIN or RST leaves TCP silent forever. Fix it by setting a read deadline before every read.

open as a page

Your Go service resolves a hostname on a laptop but not in its new container. How do you diagnose it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Establish which resolver each environment used before theorising. Run with GODEBUG=netdns=go+2 in both places to see the resolver choice and lookup order, then compare the resolver configuration, the hosts file and the C-resolver environment variables between them.

open as a page

A Go agent serving HTTP over a Unix socket cannot rebind /run/agent.sock after a hard kill. Why, and what is the fix?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A Unix listener's address is a file. Go unlinks it in the listener's Close, which a hard kill never runs, so the stale file survives and the next bind fails. Remove it only after checking nothing is live.

open as a page

How do you decide whether to pin every Go service to the pure-Go DNS resolver fleet-wide?

level: principalimportance: should knowfreq 26%

basics

~20 s

Weigh environment parity against what the host's resolver stack provides. Pin Go's own resolver when services need identical behaviour everywhere and use nothing beyond files and DNS; do not pin when the platform relies on host caching or features Go cannot reproduce.

open as a page

What does CloseWrite on a *net.TCPConn do, and what does the peer's next Read return?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

CloseWrite shuts down only the sending half of a TCP connection. It sends a FIN, so once the peer drains what you already sent, its next Read returns io.EOF — while your side can still read the peer's reply until it closes too.

open as a page

Does Go's net resolver cache DNS answers between lookups, and does it honour record TTLs?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

No. Go's own DNS resolver keeps no answer cache and does not expose or retain record TTLs, so every lookup queries again. Any caching you observe comes from the host's resolver stack when the C path is in use, not from Go.

open as a page

What is the difference between the "unix" and "unixgram" network names in Go's net package?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

"unix" is a connection-oriented byte stream you open with net.Listen or net.Dial; "unixgram" is a datagram socket with message boundaries that you open with net.ListenPacket or net.ListenUnixgram. net.Listen rejects "unixgram" outright.

open as a page

What can net.Dialer.Control do that wrapping net.Dial cannot, and when does it run?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

net.Dialer.Control is a hook called after the socket is created but before it connects, once per address tried, with the resolved address and a raw handle. It can adjust the socket or abort that attempt by returning an error.

open as a page

A packet capture on the interface counts 50k UDP datagrams a minute but your Go agent logs 30k — where did the rest go?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Almost always the kernel's socket receive queue overflowed while the read loop was busy doing other work, so datagrams reached the host and were then discarded. Check the UDP drop counter, then keep the read loop empty.

open as a page