How Go’s net Package Powers High‑Performance Networking: Inside Listen, Accept, Read & Write
This article dissects Go's net package, showing how its Listen, Accept, Read and Write functions combine goroutines with epoll to deliver synchronous‑style code that avoids costly thread switches, and walks through the underlying source code and runtime mechanisms step by step.
1. Using the Go net package
A minimal server built with the official net package demonstrates the core flow: create a listener, accept connections, and handle each connection in a separate goroutine.
func main() {
listener, _ := net.Listen("tcp", "127.0.0.1:9008")
for {
conn, err := listener.Accept()
go process(conn)
}
}
func process(conn net.Conn) {
defer conn.Close()
var buf [1024]byte
_, _ = conn.Read(buf[:])
_, _ = conn.Write([]byte("I am server!"))
// ...
}The program appears synchronous, but each Accept, Read and Write may block the current goroutine, not the OS thread.
2. Listen implementation
Unlike C/Java where listen directly invokes the kernel syscall, Go's net.Listen wraps several steps:
Create a non‑blocking socket.
Bind the socket to a local address.
Call the kernel listen syscall.
Create an epoll object.
Add the listening socket to epoll for incoming connections.
This high‑level wrapper hides the multiple underlying system calls.
2.1 Listen entry point
func Listen(network, address string) (Listener, error) {
var lc ListenConfig
return lc.Listen(context.Background(), network, address)
}The call then reaches ListenConfig.Listen, which for TCP routes to listenTCP and eventually to the low‑level socket function.
2.2 Socket creation (sysSocket)
func sysSocket(family, sotype, proto int) (int, error) {
s, err := socketFunc(family, sotype, proto) // raw socket syscall
syscall.SetNonblock(s, true) // make it non‑blocking
return s, err
}2.3 Bind and listen (listenStream)
func (fd *netFD) listenStream(laddr sockaddr, ...) error {
syscall.Bind(fd.pfd.Sysfd, lsa) // bind
listenFunc(fd.pfd.Sysfd, backlog) // listen
if err := fd.init(); err != nil { // epoll setup
return err
}
return nil
}2.4 Epoll creation and registration
func (pd *pollDesc) init(fd *FD) error {
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd)) // add fd to epoll
// ...
return nil
}The runtime creates an epoll instance (via epollcreate1) and registers the listening socket.
3. Accept process
Invoke the kernel accept syscall.
If no connection is pending, block the current goroutine.
When a connection arrives, add its socket to epoll and return the new netFD.
3.1 Accept a connection
func (ln *TCPListener) Accept() (Conn, error) {
c, err := ln.fd.accept()
return c, err
}
func (fd *netFD) accept() (netfd *netFD, err error) {
d, rsa, errcall, err := fd.pfd.Accept()
netfd, err = newFD(d, fd.family, fd.sotype, fd.net)
netfd.init()
return netfd, err
}3.2 Blocking the goroutine
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue // goroutine parked until readable event
}
}The runtime uses pollDesc.waitRead which eventually calls runtime_pollWait and parks the goroutine via gopark.
3.3 Adding the new connection to epoll
func (fd *netFD) accept() (netfd *netFD, err error) {
// ... after accept succeeds
netfd, err = newFD(d, fd.family, fd.sotype, fd.net)
netfd.init() // registers the new socket with epoll
return netfd, err
}4. Read and Write internals
4.1 Read flow
func (c *conn) Read(b []byte) (int, error) {
return c.fd.Read(b)
}
func (fd *FD) Read(p []byte) (int, error) {
for {
n, err := syscall.Read(fd.Sysfd, p)
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue // block until data is available
}
}
return n, err
}
}4.2 Write flow
func (c *conn) Write(b []byte) (int, error) {
return c.fd.Write(b)
}
func (fd *FD) Write(p []byte) (int, error) {
for {
n, err := syscall.Write(fd.Sysfd, p)
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitWrite(fd.isFile); err == nil {
continue // block until socket is writable
}
}
return n, err
}
}5. Goroutine wake‑up (sysmon)
The runtime launches a periodic monitor goroutine sysmon. It repeatedly calls netpoll, which invokes epoll_wait on the epoll set. When events are ready, the corresponding pollDesc is retrieved and the blocked goroutine is moved to the runnable queue.
func sysmon() {
for {
list := netpoll(0) // block until at least one fd is ready
// process ready descriptors
}
}Inside netpoll:
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
for i := int32(0); i < n; i++ {
var mode int32
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'r' }
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'w' }
if mode != 0 {
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
netpollready(&toRun, pd, mode)
}
} netpollreadycalls netpollunblock to unpark the goroutine for read, write, or both.
Conclusion
Go’s networking stack presents a synchronous‑style API while internally leveraging non‑blocking sockets, epoll, and lightweight goroutine scheduling. This design eliminates the heavy thread‑context‑switch overhead of traditional blocking I/O, achieving high throughput with code that remains easy to read and maintain.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Refining Core Development Skills
Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
