In Go, what does net.Listen return, and how do you serve clients from it?
answer
- bind first, then take clients one by one
- three methods: Accept, Close, Addr
- Accept blocks and hands back one client
- the connection is just a stream you close
- port zero lets the OS pick
basics
~20 snet.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.
solid answer
~40 s`net.Listen("tcp", "127.0.0.1:9000")` binds and starts listening, returning a `net.Listener` plus an error. The listener has three methods: `Accept`, `Close` and `Addr`. The server shape is an endless loop calling `ln.Accept()`, which blocks until a client connects and then returns a `net.Conn` for that one client. Because Accept hands back one connection at a time, you normally start a goroutine per connection so the loop can go straight back to accepting. `net.Conn` is an `io.ReadWriteCloser` with addresses and deadlines bolted on, so you can hand it to `bufio.NewScanner` or `io.Copy` like any other stream. Every accepted connection must be closed, and the listener itself must be closed to release the port. Passing port `:0` binds an OS-chosen free port, which you read back from `ln.Addr()` — handy in tests.
code
go · 18 linesln, err := net.Listen("tcp", "127.0.0.1:9000")
if err != nil {
return err
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
return err
}
go handle(conn)
}
func handle(conn net.Conn) {
defer conn.Close()
fmt.Fprintf(conn, "hello %s\n", conn.RemoteAddr())
}go deeper
Be ready to write the eight lines from memory: Listen, check the error, loop on Accept, start a goroutine, and defer Close on the connection. Say out loud that Accept blocks.
Explain why the goroutine is there rather than that it is idiomatic, and know that net.Conn is an interface satisfying io.ReadWriteCloser so the connection composes with bufio and io.Copy.
An interviewer expects you to mention what the loop does on shutdown and on a failing Accept, and to know that the listener holds the port until Close, independent of any accepted connection.
Frame the choice of raw net.Listen against running http.Server over the same listener: hand-rolled accept loops mean you now own timeouts, shutdown and backpressure that the standard HTTP server already implements.
## What `net.Listen` actually gives you ```go ln, err := net.Listen("tcp", "127.0.0.1:9000") ``` The first argument is the *network*: `"tcp"`, `"tcp4"`, `"tcp6"` or `"unix"` are the common stream networks. The second is the address, `host:port` for TCP or a filesystem path for `"unix"`. By the time `net.Listen` returns without an error the socket is created, bound and listening — a client that connects immediately afterwards will not be refused, even if your accept loop has not started yet. What you get back is the `net.Listener` interface, which has exactly three methods: - `Accept() (net.Conn, error)` — block until a client connection is available and return it. - `Close() error` — stop listening and release the port. - `Addr() net.Addr` — the address actually bound. `Addr()` matters more than it looks. If you pass port `0` the operating system picks a free port, and `ln.Addr().String()` is how you discover which one — the standard trick for tests that must not hard-code a port. ## The accept loop ```go for { conn, err := ln.Accept() if err != nil { return err } go handle(conn) } ``` Accept returns **one** connection per call. Nothing is concurrent for you: if `handle` ran inline on the loop's own goroutine, a slow client would stop every other client from being accepted. Starting a goroutine per connection is the idiomatic Go answer, and it is cheap — goroutines start at a couple of kilobytes of stack, so a server holding thousands of idle connections is unremarkable. The handler owns the connection's lifetime and must close it: ```go func handle(conn net.Conn) { defer conn.Close() // read and write here } ``` Forgetting that leaks a file descriptor per connection, and the process eventually cannot accept anything at all. ## What a `net.Conn` is `net.Conn` is an interface, not a struct. It embeds the stream methods you already know — ```go Read(b []byte) (int, error) Write(b []byte) (int, error) Close() error ``` — which is exactly `io.ReadWriteCloser`, so a connection can be handed to anything that takes an `io.Reader` or `io.Writer`: `bufio.NewScanner(conn)`, `io.Copy(conn, src)`, `fmt.Fprintf(conn, ...)`, `json.NewDecoder(conn)`. On top of that it adds `LocalAddr()`, `RemoteAddr()` and the three deadline setters. The dynamic type behind the interface for a TCP listener is `*net.TCPConn`, which carries TCP-specific knobs such as `SetNoDelay` and `SetKeepAlive`. If you need one of those, type-assert: `if tc, ok := conn.(*net.TCPConn); ok { ... }`. Write the rest of your code against the interface so the same handler works over a Unix socket or an in-memory `net.Pipe`. ## The client side is symmetrical ```go conn, err := net.Dial("tcp", "127.0.0.1:9000") ``` `net.Dial` returns the same `net.Conn` type, so both ends of the wire are programmed identically. Once connected, neither side can tell from the type which one dialled. ## Ordinary mistakes The two that bite first are forgetting `defer conn.Close()` in the handler, and treating the connection as message-oriented — one `Write` on the far side is not one `Read` on yours, so you need framing such as newline-delimited lines or a length prefix. The third is running the handler inline, which quietly turns a concurrent server into a one-client-at-a-time server that looks fine in testing with a single client.
- Why start a goroutine per accepted connection instead of handling it inline in the loop?Accept returns one connection at a time, so any work done on the loop's goroutine stalls every later client. Handing each connection to its own goroutine returns the loop to Accept immediately. Goroutines are cheap enough — a few kilobytes of stack each — that one per connection is the normal Go design rather than an optimisation.
- How do you write a test that starts a listener without hard-coding a port?Listen on port zero: `ln, _ := net.Listen("tcp", "127.0.0.1:0")`. The operating system assigns a free ephemeral port, and `ln.Addr().String()` gives you the actual `host:port` to dial. This avoids flaky tests that collide with whatever else is running on the machine.
- What do you get if you type-assert an accepted net.Conn to *net.TCPConn?For a TCP listener the assertion succeeds and gives you the concrete connection with TCP-only methods such as `SetNoDelay`, `SetKeepAlive` and `SetLinger`, which the `net.Conn` interface does not expose. Keep the rest of your code on the interface so the same handler still works over a Unix socket.
The listener is the reception desk: it does not talk to anyone, it just hands you the next visitor who walks in. Accept is asking for the next visitor, and each visitor is shown to their own room so the desk stays free.
saying these in an interview costs you the question
- Thinks net.Listen returns a connection rather than a listener
- Handles each connection inline, serialising all clients
- Never closes the accepted connection, leaking descriptors
- Assumes Accept returns immediately when no client is waiting
- Believes net.Conn is a struct rather than an interface