Why parse a net.Conn's RemoteAddr with net.SplitHostPort instead of splitting on a colon?
answer
- the host can be full of colons itself
- IPv6 gets brackets in the text form
- there is a paired function for building
- the port comes back as a string
- a TCP connection can skip parsing entirely
basics
~20 sAn address string can be IPv6, where the host itself is full of colons and is wrapped in brackets, as in [2001:db8::1]:8080. net.SplitHostPort understands that form and strips the brackets; splitting on a colon corrupts it. net.JoinHostPort rebuilds the string correctly.
solid answer
~40 s`conn.RemoteAddr().String()` returns a `host:port` string, and on an IPv6 peer that looks like `[2001:db8::1]:8080` — the host contains colons of its own, so it is bracketed. `strings.Split(s, ":")` on that gives you nonsense, and `strings.LastIndex(s, ":")` gets the split point right but leaves the brackets attached, so the result no longer parses as an IP. `net.SplitHostPort(s)` handles both families, returns `host` and `port` as strings with brackets removed, and returns a `*net.AddrError` for malformed input such as a bare hostname with no port. Going the other way, build addresses with `net.JoinHostPort(host, port)`, which re-adds brackets only when the host needs them. If you actually want the numeric peer IP and have a TCP connection, the cleaner route is a type assertion: `conn.RemoteAddr().(*net.TCPAddr).IP`, which skips string parsing entirely.
code
go · 13 lines// From a string, when that is all you have:
host, port, err := net.SplitHostPort(conn.RemoteAddr().String())
// "[2001:db8::1]:8080" -> host "2001:db8::1", port "8080"
// Better when the network is TCP: no parsing at all.
if ta, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if ta.IP.IsLoopback() {
// local caller
}
}
// Building an address back up, brackets added only when needed:
addr := net.JoinHostPort(host, port)go deeper
Know that Go gives you net.SplitHostPort and net.JoinHostPort and that you should use them instead of string surgery. Remember that the port comes back as a string.
Explain the IPv6 bracket form that breaks a naive split, and name the errors SplitHostPort returns for a missing port or stray colons so it doubles as validation.
Point out where this bites in production — an allowlist or rate-limit key that silently misparses every IPv6 peer — and prefer a *net.TCPAddr type assertion over string parsing when you hold the connection.
Decide once, for the whole codebase, where addresses are normalised and what identity is keyed on, so that IPv6 support is not a per-service discovery each time a network is dual-stacked.
## The shape of a Go address string Every `net.Addr` has a `String()` method, and for TCP and UDP it produces `host:port`. The catch is that IPv6 addresses are written with colons — `2001:db8::1` — so the textual form brackets them to keep the port unambiguous: ``` 127.0.0.1:8080 [2001:db8::1]:8080 [::1]:9000 localhost:8080 [fe80::1%eth0]:8080 ``` The last one carries a zone identifier for a link-local address. Any hand-rolled parser has to cope with all five shapes, and most do not. ## What goes wrong with a manual split ```go parts := strings.Split(addr, ":") // "[2001", "db8", "", "1]", "8080" host := parts[0] // "[2001" — garbage ``` Using `strings.LastIndex(addr, ":")` is a better attempt: it finds the right separator, but the host it yields is `"[2001:db8::1]"` with the brackets still attached. Pass that to `net.ParseIP` and you get nil, so the rate limiter keyed on it, or the allowlist checking it, silently fails open or closed for every IPv6 client. This is the classic bug that ships fine and then breaks the day the service is reachable over IPv6. ## `net.SplitHostPort` ```go host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) ``` It returns both parts as **strings** — the port is `"8080"`, not an int, so use `strconv.Atoi` if you need a number. It strips the IPv6 brackets, so `host` is `"2001:db8::1"` and can be handed straight to `net.ParseIP`. It preserves a zone, giving `"fe80::1%eth0"`. The error cases matter as much as the success case. `SplitHostPort` returns a `*net.AddrError` whose `Err` field describes the problem: - `"example.com"` → `missing port in address` - `"1.2.3.4:80:90"` → `too many colons in address` - `"[::1:80"` → `missing ']' in address` So it doubles as a validator. Note that an empty host is fine: `":8080"` splits into `""` and `"8080"`, which is exactly what you pass to `net.Listen` to bind all interfaces. ## Building an address, not parsing one The inverse is `net.JoinHostPort(host, port)`. It adds brackets when the host contains a colon and leaves them off otherwise: ```go net.JoinHostPort("2001:db8::1", "8080") // "[2001:db8::1]:8080" net.JoinHostPort("10.0.0.1", "8080") // "10.0.0.1:8080" ``` `fmt.Sprintf("%s:%s", host, port)` produces an address that fails to dial for every IPv6 host, and it is the mirror image of the same bug. ## Often you should not be parsing at all For a TCP connection the address behind the interface is a `*net.TCPAddr`, a struct with the parts already separated: ```go if ta, ok := conn.RemoteAddr().(*net.TCPAddr); ok { ip := ta.IP // net.IP port := ta.Port // int zone := ta.Zone // string } ``` That is faster, allocation-free and cannot be tripped by a format edge case. Use the type assertion when you know the network is TCP, and keep `SplitHostPort` for the case where all you have is a string — an address from configuration, an environment variable, or a flag. Once you have a `net.IP` you can classify it with the methods on the type: `IsLoopback`, `IsPrivate`, `IsLinkLocalUnicast`, `IsUnspecified`. Comparing IP *strings* for equality is another trap, since the same address has several textual forms (`::1` and `0:0:0:0:0:0:0:1`); compare with `ip.Equal(other)` instead. ## Interview summary Name the IPv6 bracket form, say that `SplitHostPort` and `JoinHostPort` are the paired tools, note that the port comes back as a string, and mention that a `*net.TCPAddr` type assertion beats parsing when you already hold a TCP connection.
- What does net.SplitHostPort return for the string "example.com" with no port?An error, not a host with an empty port. It returns a `*net.AddrError` whose `Err` field reads `missing port in address`, so the function doubles as a validator for configuration values. An empty host is accepted, though: `":8080"` splits cleanly into an empty host and `"8080"`, which is the form you pass to net.Listen to bind every interface.
- You have a *net.TCPConn and need the peer's IP. What is the cleanest way?Type-assert the address rather than parse the string: `ta, ok := conn.RemoteAddr().(*net.TCPAddr)` gives you `ta.IP` as a `net.IP` and `ta.Port` as an int, with no formatting edge cases and no allocation. Then classify with methods on net.IP such as `IsLoopback` or `IsPrivate`, and compare addresses with `ip.Equal` rather than comparing their strings.
saying these in an interview costs you the question
- Splits the address on a colon and takes the first field
- Uses the last colon but leaves the IPv6 brackets attached
- Builds addresses with fmt.Sprintf host colon port
- Expects the port back as an int rather than a string
- Compares IP addresses as strings instead of with IP.Equal