net/go.book
All Parts Marketing

Go Language Basics for Networking

If you completed Go Fundamentals, you already know variables, structs, functions, slices, maps, goroutines, and channels — skip to Chapter 3. This chapter is a condensed networking-oriented refresher for readers who started directly in Part 2.

This chapter covers the three Go patterns that underpin every networked program in this book: modelling connections with structs, error handling around I/O, and the goroutine-per-connection accept loop. If any section below feels unfamiliar, the corresponding Go Fundamentals chapter is referenced alongside it.

Go's networking code isn't a special dialect of the language — it's ordinary Go: structs, errors, and goroutines, applied to bytes moving over a wire.


Modelling a Connection with Structs

A Server struct holds configuration; a Client struct holds a live connection. The difference between value and pointer receivers matters immediately:

type Server struct {
	Host string
	Port int
}

func (s Server) Address() string {
	return fmt.Sprintf("%s:%d", s.Host, s.Port)
}

Address uses a value receiver — it only reads fields, so a copy is fine.

type Client struct {
	conn net.Conn
	tag  string
}

func (c *Client) Connect(addr string) error {
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		return err
	}
	c.conn = conn
	return nil
}

Connect uses a pointer receiver (*Client) because it mutates c.conn. A value receiver would update a throwaway copy, leaving the original unchanged.

Copying a struct that holds a connection
Passing a Client by value copies the net.Conn field. Both copies share the same underlying connection, but writes to one copy's other fields never reach the original — a frequent source of silent bugs.

Exercise: Variables and Types

Exercise: Structs and Methods


Error Handling Around I/O

Every networking call can fail. Go's explicit error handling makes that failure visible:

conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
	log.Fatal(err)
}

net package errors implement net.Error, which adds Timeout() bool:

if ne, ok := err.(net.Error); ok && ne.Timeout() {
	// retry on timeout, give up on permanent failure
}

log.Fatal skips your defers
log.Fatal calls os.Exit(1) immediately — any defer conn.Close() written earlier never runs. Inside a server, log.Fatal on a per-connection error kills the entire process. Prefer log.Println plus return or continue.

Exercise: Error Handling


The Goroutine-Per-Connection Accept Loop

The pattern that makes every Go network server possible:

ln, err := net.Listen("tcp", ":9000")
if err != nil {
	log.Fatal(err)
}
for {
	conn, err := ln.Accept()
	if err != nil {
		log.Println("accept error:", err)
		continue
	}
	go handleConn(conn)
}

Accept() blocks until a client connects, returns one net.Conn, then blocks again waiting for the next client. go handleConn(conn) spawns a goroutine so the loop goes straight back to blocking instead of waiting for the current client to finish. Each goroutine costs a few KB of stack — not an OS thread — so this scales to thousands of simultaneous connections.

It does not scale to infinity. A connection flood with no limit exhausts memory and file descriptors even though goroutines are cheap. Chapter 8 (Concurrency in Networking) introduces worker pools — a fixed number of goroutines pulling connections off a channel — as the escape hatch, reached for only once you have measured that unbounded goroutines are the problem.

Exercise: Goroutines and Channels


Real-World Example: A Careful TCP Client

package main

import (
	"fmt"
	"io"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("tcp", "example.com:80")
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}
	defer conn.Close()

	fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")

	buf := make([]byte, 4096)
	for {
		n, err := conn.Read(buf)
		fmt.Print(string(buf[:n]))
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("read error:", err)
			break
		}
	}
}

Key points:

  • defer conn.Close() guarantees cleanup even on early returns.
  • The Read loop continues until io.EOF — TCP has no concept of messages, so a single Read may return part of a response or more than one chunk.
  • buf[:n] prints before checking errRead can return data and io.EOF in the same call.

Every byte a TCP connection hands you comes through the same three-step ritual: read into a slice, check the error, decide whether to keep going.

Exercise: Hello Go

Exercise: Control Structures

Exercise: Functions

Exercise: Slices and Maps

Exercise: TCP Client


Frequently Asked Questions

Why does the careful TCP client print buf[:n] before checking for io.EOF?

Because Read can return both data and io.EOF in the same call — the last chunk of the response can arrive at the exact moment the connection closes. Checking the error first would silently drop the tail of the response.

My program printed "all goroutines are asleep - deadlock!" — what happened?

You almost certainly sent on an unbuffered channel with nothing ready to receive. ch <- "Ping!" on the same goroutine that will later read <-ch, with no go in front of the send, blocks forever. Go's runtime detects the deadlock and panics rather than hanging silently.

Should Address() on my Server struct use a value or pointer receiver?

If the method only reads fields (Host, Port), a value receiver is fine. If it mutates the struct, or the struct holds a net.Conn, mutex, or buffer, give it a pointer receiver — otherwise changes hit a throwaway copy.