Prompt and context
A service reads host and port from configuration and passes fmt.Sprintf("%s:%d", host, port) to net.Dial. IPv4 tests pass, but production exposes IPv6, link-local addresses, and malformed-port parsing. Fix the implementation in Go 1.25, explain net.JoinHostPort, SplitHostPort, zone identifiers, and input-validation boundaries, and discuss the new go vet hostport analyzer. The core skill is portable network programming, so this is a coding question.
What the interviewer evaluates
First, whether you recognize that IPv6 text contains multiple colons, making a bare concatenation such as 2001:db8::1:443 ambiguous.
Second, whether you use the standard library to add brackets according to network-address syntax instead of hand-written branches.
Third, whether you distinguish a host string, zone identifier, port string, and complete authority so brackets and parsing are not duplicated.
Fourth, whether tests and errors cover IPv4, IPv6, %zone, empty hosts, and invalid ports.
Fifth, whether go vet is used as a signal while code review and real connection tests verify the fix.
Questions to clarify first
- Is
hosta bare IP, DNS name, bracketed authority, or a mixture? - Is the port an integer, string, or service name? Is
0allowed? - Must IPv6 link-local addresses and zones such as
%eth0be supported? - Is the network TCP, UDP, or a Unix socket?
- Should brackets and control characters in user input be rejected?
- Can CI bind an IPv6 loopback listener?
A 30-second answer
“Bare concatenation cannot express the boundary between an IPv6 host and its port, so 2001:db8::1:443 is ambiguous. I validate the port, then call net.JoinHostPort(host, strconv.Itoa(port)); the standard library emits [host]:port for IPv6 and preserves zones. I do not add brackets manually. If complete authorities are accepted, I parse them with net.SplitHostPort before rebuilding. Tests cover IPv4, IPv6, %zone, DNS, empty input, invalid ports, and round-trip parsing. Go 1.25’s go vet hostport analyzer is a static gate, not a substitute for network tests.”
Detailed solution
Step 1: Explain the failure
An IPv4 address has one colon separating host and port; an IPv6 address contains colons itself. IPv6 network text therefore brackets the host, as in [2001:db8::1]:443. Bare concatenation provides no boundary, so a parser cannot reliably tell whether the last colon is the port separator.
Step 2: Construct with the standard library
Convert the port to a string and call net.JoinHostPort. When the host contains a colon, it adds brackets; DNS names and IPv4 remain in the ordinary form. It also preserves a zone identifier, so do not add brackets or escape text before calling it.
func dialAddress(host string, port int) (string, error) {
if port < 1 || port > 65535 {
return "", fmt.Errorf("port out of range: %d", port)
}
return net.JoinHostPort(host, strconv.Itoa(port)), nil
}Step 3: Define the input shape
The API should accept a bare host, not an authority that already contains a port. If complete addresses must be supported, first call net.SplitHostPort, normalize the pieces, and rebuild with JoinHostPort. String replacement is unsafe for IPv6 and zones.
Step 4: Handle zone identifiers
An IPv6 link-local address may be fe80::1%eth0, producing [fe80::1%eth0]:443. The zone carries interface scope and must not be dropped. Validate its allowed characters and length so control characters or unexpected authority text cannot enter logs or downstream configuration.
Step 5: Validate and parse complete addresses
After construction, call net.SplitHostPort to check reversibility and assert that host and port match the inputs. It requires a port-bearing network address; missing ports, extra colons, and mismatched brackets should return actionable errors.
Step 6: Understand go vet hostport
Go 1.25 adds the hostport analyzer, which flags fmt.Sprintf("%s:%d", host, port) patterns used to construct addresses for net.Dial. It catches a common mistake, but it cannot replace configuration contracts, zone validation, port bounds, or real network-stack tests.
Step 7: Build a test matrix
Cover 127.0.0.1:443, [2001:db8::1]:443, [fe80::1%eth0]:443, DNS names, an empty host, ports 0, 65536, negative values, and already bracketed input. Where supported, connect to IPv4 and IPv6 loopback listeners; otherwise retain pure construction and split tests and document the CI capability.
A high-quality sample answer
“The defect is address syntax, not Dial: an IPv6 host contains colons, so bare concatenation cannot delimit the port. I define the API as bare host plus integer port, check 1..65535, and call net.JoinHostPort(host, strconv.Itoa(port)); I never hand-write brackets. If a complete authority is accepted, I first split it and rebuild from normalized host and port. I preserve and validate %eth0 for link-local addresses. Tests cover IPv4, IPv6, zones, DNS, invalid ports, and reversible parsing. Go 1.25 go vet hostport is a static gate, while IPv4/IPv6 loopback connections provide integration coverage.”
Common mistakes
- Keep using
fmt.Sprintf("%s:%d", ...)→ IPv6 host and port boundaries are ambiguous → usenet.JoinHostPort. - Add brackets to every host manually → IPv4 and DNS formats become wrong → let the standard library decide.
- Drop
%zone→ link-local IPv6 may not route → preserve and validate the zone. - Treat a complete authority as a bare host → ports or brackets are duplicated → define the input contract and split when needed.
- Test only IPv4 → the production failure path remains uncovered → include IPv6, zones, and loopback tests.
- Rely only on go vet → custom input and runtime failures remain → combine unit, split, and connection tests.
- Skip port validation → negative or overflowing values enter configuration → check the range before construction.
- Log unvalidated raw addresses → control characters or sensitive configuration may leak → normalize and log structured fields.
Follow-up questions
Follow-up 1: Does JoinHostPort verify that host is a valid IP?
It combines host and port according to network-address syntax; it is not DNS resolution or full business validation. Whether domains, zones, or special characters are allowed belongs to the input contract and resolver path.
Follow-up 2: Why not simply use the last colon?
IPv6 compression, zones, and missing ports make string rules fragile. The standard Join/Split pair follows address syntax and reports bracket or port errors explicitly.
Follow-up 3: Can a port string be passed directly?
Yes, JoinHostPort accepts a string port. When the business input is an integer, validate its range and convert with strconv.Itoa so negative or overflowing values are not formatted into an address.
Follow-up 4: How should a zone be sanitized?
Preserve the zone semantics required by the interface, but constrain its character set and length. Do not double-encode %; log host and zone as separate structured fields when possible.
Follow-up 5: When should url.URL be used instead?
Use url.URL when constructing HTTP or HTTPS URIs with schemes, authorities, and paths. Use JoinHostPort for the host:port passed to net.Dial; do not hand-concatenate a URI.
Follow-up 6: How do you prove the fix works with real IPv6?
In an IPv6-capable CI or dedicated environment, bind a [::1]:0 listener, read its assigned port, and connect through the same constructor. Keep pure-function tests for environments without IPv6 and report the distinction.