Backend Development 18 min read

Deep Dive into Golang net Package: Internal Mechanics of Listen, Accept, Read and Write

This article explains how Go's net package implements network operations—showing a simple server example, then dissecting the internal workflow of Listen, Accept, Read and Write, including socket creation, epoll integration, coroutine blocking and wake‑up mechanisms.

IT Services Circle
IT Services Circle
IT Services Circle
Deep Dive into Golang net Package: Internal Mechanics of Listen, Accept, Read and Write

Before coroutines became popular, traditional network programming relied on synchronous blocking calls that incurred heavy CPU overhead (e.g., a 3 µs context switch) and callback‑based asynchronous models that were hard to understand. Go introduced lightweight goroutines combined with epoll to provide a simple, high‑performance networking model.

A minimal Go server using the official net package is shown below; it creates a listener, accepts connections, and processes each connection in a separate goroutine with basic read/write operations.

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
    _, err := conn.Read(buf[:])
    _, err = conn.Write([]byte("I am server!"))
    // ...
}

The program appears synchronous, but under the hood each Accept , Read and Write may block the current goroutine while the runtime efficiently parks it and resumes it when the underlying file descriptor becomes ready.

Listen implementation : Go's net.Listen is just an entry point that creates a ListenConfig and eventually calls sysSocket to create a non‑blocking socket, bind it, and invoke the kernel listen syscall. The steps performed are:

Create a socket and set it to non‑blocking mode.

Bind the socket to the specified address.

Call listen to start listening.

Create an epoll object.

Add the listening socket to epoll for event monitoring.

Relevant source snippets:

// net/dial.go
func Listen(network, address string) (Listener, error) {
    var lc ListenConfig
    return lc.Listen(context.Background(), network, address)
}
// net/sock_posix.go (simplified)
func socket(ctx context.Context, net string, family, ...) (fd *netFD, err error) {
    s, err := sysSocket(family, sotype, proto)
    // set non‑blocking
    syscall.SetNonblock(s, true)
    // bind and listen later in listenStream
    return
}

Accept process : The Accept method repeatedly calls the kernel accept syscall. If no connection is pending, it receives EAGAIN and the goroutine is parked via pd.waitRead . When a connection arrives, the new socket is wrapped in a netFD , initialized (which registers it with epoll), and returned to the caller.

// net/fd_unix.go (simplified)
func (fd *netFD) accept() (netfd *netFD, err error) {
    d, rsa, err := fd.pfd.Accept()
    if err == syscall.EAGAIN {
        if err = fd.pd.waitRead(fd.isFile); err == nil {
            continue // retry after being woken
        }
    }
    netfd, err = newFD(d, fd.family, fd.sotype, fd.net)
    netfd.init() // registers with epoll
    return netfd, nil
}

Read and Write : Both operations call the respective system calls. When the kernel returns EAGAIN , the goroutine is parked with pd.waitRead or pd.waitWrite . Once the socket becomes readable or writable, the runtime’s poller wakes the goroutine.

// internal/poll/fd_unix.go (Read)
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
            }
        }
        return n, err
    }
}
// internal/poll/fd_unix.go (Write)
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
            }
        }
        return n, err
    }
}

Goroutine wake‑up : The runtime’s sysmon goroutine periodically calls netpoll , which invokes epoll_wait on the epoll descriptor. Ready events are translated into a mode (read/write) and the corresponding pollDesc is passed to netpollready , which unblocks the waiting goroutine and places it on the run queue.

// runtime/netpoll_epoll.go (simplified)
func netpoll(delay int64) gList {
    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)
        }
    }
    return toRun
}

In summary, Go’s net package hides the complexity of socket creation, epoll management and coroutine scheduling, offering developers a synchronous‑style API while delivering the performance of an event‑driven model.

concurrencygolangRuntimenetworkingepollgoroutine.NET
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.