{} The Go Reference

Net basics · Web · Beginner

DNS & Addressing

From names to numbers — net.ParseIP and net/netip, SplitHostPort/JoinHostPort, CIDR prefixes and subnet membership, and the net.Resolver that turns hostnames into IPs over the network.

Net basics Beginner ⏱ 5 min read Complete

📇 Analogy

DNS is the phone book of the internet. You know a name — example.com — but the network only routes to numbers — 93.184.216.34. A resolver looks the name up and hands back the number. Parsing and splitting addresses, by contrast, is just reading the phone book entry you already have — no call required.

Addresses: parsing vs. resolving

Two very different things get lumped together as “addressing.” Parsing an IP or a host:port string is pure, local, and instant. Resolving a hostname means a trip to a DNS server over the network — slow, fallible, and worth a timeout.

graph LR
N["name<br/>example.com:443"] -->|"SplitHostPort"| H["host: example.com<br/>port: 443"]
H -->|"Resolver (DNS, network)"| IP["93.184.216.34<br/>2606:2800:..."]
L["literal<br/>10.0.0.1"] -->|"ParseIP / ParseAddr (local)"| A["net.IP / netip.Addr"]

Modern Go has two IP types. The classic net.IP is a []byte slice — it allocates and can’t be a map key. The newer net/netip gives you netip.Addr and netip.AddrPort: small, comparable value types you can use with == and as map keys. Prefer net/netip for new code.

Inspecting addresses, in-process

All of this is pure string parsing — no DNS, no socket — so it runs anywhere and is fully deterministic:

addr.go — editable & runnable
package main

import (
"fmt"
"net"
"net/netip"
)

func main() {
// 1) net.ParseIP — the classic []byte-backed net.IP type
v4 := net.ParseIP("192.168.0.42")
v6 := net.ParseIP("2001:db8::1")
fmt.Println("v4:", v4, "is4:", v4.To4() != nil)
fmt.Println("v6:", v6, "is4:", v6.To4() != nil)
fmt.Println("loopback?", net.ParseIP("127.0.0.1").IsLoopback())
fmt.Println("private?", v4.IsPrivate())

// 2) net.SplitHostPort — break "host:port" into its parts (DNS-free)
host, port, _ := net.SplitHostPort("example.com:8443")
fmt.Println("host:", host, "port:", port)

// JoinHostPort wraps an IPv6 literal in [] for you
fmt.Println("joined:", net.JoinHostPort("2001:db8::1", "443"))

// 3) net/netip — the modern, comparable, allocation-free address type
ap, _ := netip.ParseAddrPort("[2001:db8::1]:443")
fmt.Println("addr:", ap.Addr(), "port:", ap.Port(), "is6:", ap.Addr().Is6())

a, _ := netip.ParseAddr("10.0.0.1")
fmt.Println("netip addr:", a, "is4:", a.Is4(), "private:", a.IsPrivate())
}

A few things to notice: SplitHostPort strips the IPv6 brackets, JoinHostPort puts them back, and ports are always strings in these APIs ("443"), because a host:port literal is text — convert with strconv.Atoi only when you need the number.

Subnets and CIDR prefixes

A CIDR prefix like 10.0.0.0/8 describes a range of addresses — essential for allow-lists, firewall rules, and classifying traffic. net/netip models it as a netip.Prefix, with Contains for membership and Masked to normalize to the network address. Pure parsing, so it runs anywhere:

cidr.go — editable & runnable
package main

import (
"fmt"
"net/netip"
)

func main() {
prefix := netip.MustParsePrefix("10.0.0.0/8")

for _, s := range []string{"10.1.2.3", "192.168.0.1", "10.255.255.255"} {
	ip := netip.MustParseAddr(s)
	fmt.Printf("%-15s in %s? %v\n", s, prefix, prefix.Contains(ip))
}

// Masked() zeroes the host bits → the canonical network address.
p := netip.MustParsePrefix("192.168.5.130/24")
fmt.Println("network:", p.Masked()) // 192.168.5.0/24

// The classic []byte API does the same: net.ParseCIDR + (*IPNet).Contains.
}

Resolving names with DNS

When you actually need to turn a name into IPs, you hit the network. These calls reach a DNS server, so they can be slow or fail — pass a context.Context with a deadline. (Shown as fenced examples because they touch the network and won’t run in a sandbox.)

// quick one-shots
ips, err := net.LookupHost("example.com")     // []string of IPs
addrs, err := net.LookupIP("example.com")     // []net.IP
cname, err := net.LookupCNAME("www.example.com")

// the configurable way: a *net.Resolver with a context deadline
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var r net.Resolver
hosts, err := r.LookupHost(ctx, "example.com")
mxs, err := r.LookupMX(ctx, "example.com")     // mail servers
_ = err

Most of the time you never call these directly — net.Dial("tcp", "example.com:443") and the HTTP client resolve the name for you under the hood, trying each returned address in turn.

Reference

TaskCall
Parse an IP literalnetip.ParseAddr (or net.ParseIP)
Parse host:portnetip.ParseAddrPort (or net.SplitHostPort)
Build host:portnet.JoinHostPort(host, port)
Parse a subnetnetip.ParsePrefix("10.0.0.0/8")
Subnet membershipprefix.Contains(addr)
Is it private/loopback?addr.IsPrivate() / addr.IsLoopback()
Resolve a name → IPsnet.LookupIP / (*net.Resolver).LookupHost(ctx, …)
MX / CNAME / TXT recordsnet.LookupMX / LookupCNAME / LookupTXT

⚠️ Parsing is local; resolving is a network call

The playgrounds are pure parsing (no DNS, no socket) so they’re safe and deterministic anywhere. The trap is forgetting that LookupHost/Resolver methods are network calls: they can block, time out, or return multiple addresses (IPv4 and IPv6). Always pass a context with a timeout, expect more than one IP back, and don’t cache results past their TTL — DNS records change. For new code, prefer net/netip types so addresses stay comparable and allocation-free.

See also

  • TCP sockets and UDP sockets — what you do with a resolved address.
  • HTTP client — resolves names for you; reuses connections per host.
  • timecontext.WithTimeout for bounding a lookup.

Next: turning a byte stream into messages — framing & custom protocols.

Check your understanding

Score: 0 / 5

1. Why prefer net/netip's Addr over the older net.IP for new code?

net/netip.Addr is a compact value type: comparable with ==, usable as a map key, and allocation-free. net.IP is a []byte slice, so it allocates and can't be compared with == or used as a key. net.IP still works everywhere, but netip is the modern default.

2. What does net.SplitHostPort("[2001:db8::1]:443") return?

SplitHostPort understands IPv6 literals and removes the surrounding brackets, returning the bare host and the port as strings. Its inverse, JoinHostPort, adds the brackets back around an IPv6 literal.

3. What does a DNS lookup like net.LookupHost("example.com") actually do?

LookupHost (and the methods on net.Resolver) send a query to a DNS server and return the resolved IPs. It hits the network and can be slow or fail — unlike net.ParseIP/SplitHostPort, which are pure local string parsing.

4. How do you test whether an IP falls inside a subnet like 10.0.0.0/8?

netip.ParsePrefix("10.0.0.0/8") yields a netip.Prefix; Contains(addr) reports membership. (The older net.ParseCIDR + IPNet.Contains does the same with []byte types.) Useful for allow-lists and classifying private vs public addresses.

5. A name resolves to several IPs (IPv4 and IPv6). What should your code assume?

Hosts commonly have both A (IPv4) and AAAA (IPv6) records, sometimes several each. net.Dial walks the list until one connects (Happy Eyeballs). Treat resolution as returning a set, give it a timeout, and don't cache past the TTL.

Comments

Sign in with GitHub to join the discussion.