← Posts

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 addrsrange 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:

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