Go — One Real Program: DNS Lookup
Go — One Real Program: DNS Lookup
Most Go tutorials start with variables. Then loops. Then functions. By the time you’ve written ten small programs that do nothing, you still don’t know what Go is actually good for or why anyone would choose it.
This course works differently.
The program already exists. It runs. You will see its output.
The Program
dnsback is a Go CLI tool. You give it a domain name. It reaches out to live DNS servers and returns A records, MX records, and TXT records.
go run main.go github.com
A 140.82.114.4
MX aspmx.l.google.com. (priority 1)
MX alt1.aspmx.l.google.com. (priority 5)
MX alt2.aspmx.l.google.com. (priority 10)
TXT v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com ~all
Real data. A real domain. Right now.
That is the destination.
Before You Begin
You need:
Go installed. Version 1.21 or later. Check with go version. If it’s not
there, get it from go.dev/dl. The install is
straightforward on Linux, macOS, and Windows — unpack, set your PATH, done.
A terminal you’re comfortable in. You don’t need to be a Linux power user.
You need to know how to navigate directories and run commands. If you can cd
and run a program, you’re ready.
Network access. dnsback reaches out to DNS servers on the public internet. Any standard connection works.
No IDE required. No prior Go experience. No external packages — everything dnsback uses is in Go’s standard library.
Get the source and run it:
git clone https://github.com/bensantora-tech/dnsback
cd dnsback
go run main.go github.com
See it work. Then come back.
The Entry Point
What happens when a Go program starts?
Open main.go and find func main(). Every Go program has exactly one. It is
where execution begins.
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: dnsback <domain>")
os.Exit(1)
}
domain := os.Args[1]
lookupA(domain)
lookupMX(domain)
lookupTXT(domain)
}
os.Args is a slice — a list of strings — holding everything the user typed
on the command line. os.Args[0] is always the program name itself. os.Args[1]
is the first argument — the domain the user provided.
len(os.Args) returns the length of that slice. If it’s less than 2, the
user didn’t provide a domain. Handle the bad case immediately, then exit.
fmt.Fprintln(os.Stderr, ...) writes to standard error, not standard output.
Error messages belong on stderr. Output belongs on stdout. The distinction matters
when programs are chained together in pipelines.
os.Exit(1) terminates the program immediately with a non-zero exit code.
Non-zero signals failure to the shell. Zero signals success. This is how programs
communicate their outcome to whatever called them.
After the check, main calls three functions — one for each record type. Each
function handles one job. main orchestrates them.
Exercise: Run the program without a domain argument. Observe the error message
and exit code (echo $?). Then run it with a domain. Observe the difference.
Reaching Out
How does Go talk to DNS?
Find lookupA:
func lookupA(domain string) {
addrs, err := net.LookupHost(domain)
if err != nil {
fmt.Printf("A error: %v\n", err)
return
}
for _, addr := range addrs {
fmt.Printf("A %s\n", addr)
}
}
net.LookupHost sends a DNS query for the domain and returns a slice of IP
addresses and an error. Go functions can return more than one value. The pattern
is always the same: call the function, check the error immediately, handle it
or continue.
if err != nil — in Go, nil means nothing, no value, no error. If err
is not nil, something went wrong. Handle it, then return. This is Go’s error
handling model: errors are values, not exceptions. You check them at the point
they occur.
fmt.Printf("A error: %v\n", err) — %v is the general-purpose format
verb. It formats any value in its default representation. For an error, that
means the error message string.
for _, addr := range addrs — range iterates over a slice. On each
iteration it gives you an index and a value. The _ discards the index — you
don’t need it here. addr holds the current IP address string.
The function doesn’t return anything. It has one job: print the A records for
a domain. If it fails, it says so and returns. Either way, main continues to
the next lookup.
Structs in the Standard Library
How does Go represent structured data it didn’t create?
Find lookupMX:
func lookupMX(domain string) {
records, err := net.LookupMX(domain)
if err != nil {
fmt.Printf("MX error: %v\n", err)
return
}
for _, mx := range records {
fmt.Printf("MX %s (priority %d)\n", mx.Host, mx.Pref)
}
}
net.LookupMX returns a slice of *net.MX — pointers to structs defined
in Go’s standard library. You don’t define this struct. The net package defines it:
type MX struct {
Host string
Pref uint16
}
You access its fields with dot notation: mx.Host is the mail server hostname,
mx.Pref is the priority. Lower priority numbers are preferred.
%d is the format verb for integers. %s is for strings. The right verb
matters — using %s on an integer produces garbage output.
Now find lookupTXT:
func lookupTXT(domain string) {
records, err := net.LookupTXT(domain)
if err != nil {
fmt.Printf("TXT error: %v\n", err)
return
}
for _, txt := range records {
fmt.Printf("TXT %s\n", txt)
}
}
net.LookupTXT returns a slice of plain strings — no struct, no fields.
TXT records are freeform text. SPF policies, DKIM keys, domain verification
tokens — all stored as raw strings. No structure to decode, just iterate and print.
The three lookup functions return three different shapes: a slice of strings
(LookupHost), a slice of structs (LookupMX), and a slice of plain strings
(LookupTXT). Same pattern, different data. That is the net package’s design
— consistent calling convention, varied return types depending on what DNS
actually stores.
Exercise: Add a lookupNS function using net.LookupNS. It returns
[]*net.NS, where NS has a single field: Host. Register it in main.
Test it on a domain.
The Module
go.mod:
module dnsback
go 1.21.0
Two lines. No dependencies. net, fmt, os — all standard library. The
program reaches out over the network and returns live data with nothing installed
beyond Go itself.
Final Exercise
Add a --all flag that also looks up NS and CNAME records alongside the
existing output.
Requirements:
- Use the
flagpackage to parse a--allboolean flag - If
--allis set, calllookupNSandlookupCNAMEin addition to the existing three net.LookupCNAMEreturns a single string, not a slice- Keep the output format consistent with the existing lines
This exercise uses: the flag package, boolean values, conditional logic, and
two new net lookup functions.
What You’ve Covered
One program. Here’s what you now understand:
Go fundamentals: entry point, functions, multiple return values, error handling, slices, range loops, fmt format verbs, exported vs unexported identifiers.
CLI input: os.Args, argument validation, os.Exit, stderr vs stdout.
The net package: LookupHost, LookupMX, LookupTXT — reaching out over
the network with nothing but the standard library.
Structs from the outside: using types defined by packages you didn’t write, accessing their fields, understanding what they represent.
Nothing here came from a syntax drill. Every concept earned its place because the program needed it.
dnsback source: github.com/bensantora-tech/dnsback Written by Ben Santora — bensantora.com