Building a domain name generator using Golang.

Building a domain name generator using Golang.

Creating a command line application in Go from start to finish.

To build software that generates domain names, you need to understand what a domain name is and what a good domain name should look like.

What is a domain generator?

A domain name generator is a software that will help you create and choose the perfect domain name for your site. Ideally, the domain you register should be easy to spell and remember, while encompassing the values of your business or brand.

What you will learn:

  1. How to build a command-line application with just a single code file.

  2. How to combine two or more tools using standard streams.

  3. How to use a third-party JSON RESTFUL API.

  4. How to use in and out pipes in GO code.

  5. How to build a WHOIS client to look up domain information.

The complete application will be a combination of five simple distinct programs that will combine to generate beautiful domain names and verify if the domain names generated are available or already taken. The programs include:

  1. Sprinkle: This program will add some web-friendly words to increase the chances of finding available domain names.

  2. Domainify: This program will ensure words conform to the rules of domain names by removing unacceptable characters. Once this is done, it will replace spaces with a hyphen and suffix an appropriate top-level domain like “.com” or “.org”.

  3. Coolify: This program will change a boring old normal word to Web 2.0 by fiddling around with vowels.

  4. Synonyms: This program will use a third-party API to find synonyms.

  5. Available: This program will check to see if the domain is available or not, using the appropriate WHOIS server.

This might sound like a lot, wheeew…., It is a lot actually, but in the end, you would have a domain name generator that you’d love.

The complete code for this project lives here.

Sprinkle

This program adds some sugar terms to whatever word you input to improve the odds of finding available names. Many startups employ this approach to keep the core message consistent while being able to afford the .com domain. For example, if we pass in the word “cheat”, it might output “cheatapp”; alternatively, if we pass in “speal”, we may get ” spealtime”.

We are going to use the math/rand to randomly generate sugar words. It makes our program look intelligent by introducing elements of chance and decision-making. Enough of talking, let’s get our hands dirty, we are going to do the following:

  1. Define an array, using a constant to indicate where the original word will appear and where the sugar word will appear.

  2. Use the bufio package to scan input from stdin and stdout and write output using fmt.Println.

  3. Use the math/rand to select the sugar word to apply.

All our programs will live in the $GOPATH/src//github.com/<your-github-username> folder. To find your GOPATH, run go env in your terminal.

Inside the folder you just created, create a new folder called sprinkle and add a main.go file containing the code below.

The code above is our complete sprinkle program. We have defined a constant, a variable that contains a slice of strings and the mandatory main function which is the entry point of our program. The otherword constant is a placeholder that helps us specify where the original word you input will appear in any of the transformation options. An example like otherword+”extra”, this means that we want “extra” to appear at the end of otherword.

All the transformation options are stored in the transforms variable which is a slice of strings. We added different formats for transformations like adding “app” to the end of the word or “lets” before the word. Feel free to add any other transformation ideas you have to it.

In the main function, the first thing we do is use the current time as a random seed. Computers can’t generate random numbers, changing the seed number of a random algorithm gives the illusion that it can. We are using the nanoseconds because it is different each time we run the program, if we don’t add it, the numbers generated by math/rand will be the same every time we run the program.

We created a bufio.Scanner object by calling bufio.Newscanner and ask it to read the input from os.stdin, which is the standard input stream, this is will be the same for all 5 programs since we will be building for the terminal.

A call to the Scan method tells the scanner to read the next block of bytes from the input, and then return a bool value indicating if it found anything or not. If Scan finds any content, it returns true and the body of forloop is executed, if no content is found, Scan returns a false and the loop is broken. The bytes selected ate stored n the Bytes method of the scanner, and the handy Text method that we use converts the []byte slice into a string for us.

Inside the forloop, we use rand.intn to select a random item from the transforms slice and use strings.Replace to insert the original word where the otherword string appears. Finally, we use fmt.Println to print the output to the terminal.

The math/rand package provides insecure random numbers. If you want to write code that uses random numbers for security purposes, you must opt for the crypto/rand package.

Let’s build the program and play with it.

go build

./sprinkle

The first command compiles the program, the second program runs the program. Once the programs start running and you input any word, in my case, I typed the word "cool" and it showed the transformations as shown in the image below.

Sprinkle doesn't exit by default, so you have to hit ctrl+c to exit the program. The program will randomly transform the word you type in. We have succesfully completed our first program, it has a very simple but useful function as we will see when the whole project is complete.

Instead of hardcoding the transformations array as we have done, see if you can externalize it via flags or store them in a text file or database.


Domainify

The transformation ideas from sprinkle don't have spaces but might have if we start adding other ideas to the list or if we are getting the transformations from a file or database, they may also contain characters that are not allowed in domains. Domainify converts a line of text into an acceptable domain format and adds a random TLD (Top-level domain) to the end.

Create a new folder called domainfy in the same directory as sprinkle and add a main.go file with the following code below:

We have two variables in the code above, the tld variable is a slice of strings that stores the tlds we want to add to the transformed words generated from the sprinkle program. The allowedChars variables stores the acceptable characters a domain name should have.

Inside the main function, we generate a random seed and scan the terminal for any input, we then converted the text to lowercase and build up a new slice of rune types saved in the newText variable. From lines 24 to 27, we iterated through the lowercase version of whatever word that is inputted and check if there is a space in it, if true, we replace the space with a "-" character. Finally, we convert newText from a rune slice to a string and add a .com or a .net at the end and print it out to the terminal. Run Domainify

go build and ./domainify

The image below shows the result of some tests.

We can run sprinkle and domainify together using a pipe. To do this, go to your terminal and navigate to the parent folder containing the two programs and run the command below:

./sprinkle/sprinkle | ./domainify/domainify

Here we ran the sprinkle program and piped the output to the domainify program. Sprinkle uses the terminal and domainify outputs to the terminal. Typing a particular word a few times again, the result will look like the ones we saw when testing sprinkle, but now they are acceptable domain names and any space is replaced with a hyphen. Piping between programs allows us to compose command-line tools together. Some tests and results are shown below:

We are only supporting .com and .net TLDs and it is fairly limiting, see if you can add a list of other TLDs via a command-line flag.


Coolify

Very often, domain names for common words like "chat", are already taken. One way to get domain names around these common words is to play around with vowels in the words. Cooley is a program that will play with vowels of words that are received from domainify and output modified versions of the same word.

Create a new folder named coolify alongside sprinkle and domainify, and create a main.go file with the codes below:

Coolify is more complicated than the other two programs, we declared two boolean constants and a function. randBool is a helper function that uses the rand package to generate a random number, it returns true, if the generated number is 0 else, it returns false, so there is a 50 percent chance of it being true

The main function starts by initializing the Seed method from the rand package and creating a scanner of the standard input stream before executing the loop for each line of input. We call randBool first to decide whether we are going to mutate the word or not, so Coolify will only affect half the words passed through it. We then iterate over each rune in the string and look for a vowel character in the V1 variable. If not, we keep looking through the strings for another vowel, which allows us to randomly select a vowel from the words rather than always modifying the same one.

Let's build Coolify and see how cool it is:

go build && ./coolify

You can see that I had to pass the same words twice before the program can modify its vowels. Let's see how Coolify works with Sprinkle and Domainify by chaining them with a pipe. In the terminal, navigate back to the parent folder and run the command below:

./coolify/coolify | ./sprinkle/sprinkle | ./domainify/domainify

Coolify works on vowels only, Can you update the algorithm to operate on all characters it encounters?


Synonyms

Our program can modify words, but to make the software more usable, we need to provide synonyms to words as viable suggestions. To achieve this, we will use a third-party API that provides word synonyms. This will allow us to suggest more domain names and still retain the original meaning. Unlike Sprinkle and Domainify, synonyms will write out more than one response for each word given to it. Our piping architecture means this won't be much of an issue since each of the three programs can read multiple lines from the input source.

We will use Big Huge Thesaurus API. It has a very clean and simple API that allows us to make a single HTTP GET request to find synonyms. Before you can use this API, You'll need an API key, which you can get here.

Using environment variables for configuration

API keys are sensitive configuration information that you should not share with others. We could store them as constants in our code, but this is not a good practice because we'd share our keys when we share our codes. A better solution is to use environment variables, this will allow you to easily change if you need to, you could also have different keys for different deployments.

Create a new environment variable called BHT_APIKEY and set your API key as its value. For machines running bash shell, you can modify your ~/.bashrc file or similar to include export commands as shown below:

export BHT_APIKEY=12hjhdgy67dhdkdh8esd3

On windows machines, you can navigate to the properties of your computer and look for environment variables in the advanced section.

consuming an API

Making a GET request from a browser shows us what the structure of the JSON response data looks like when looking for the word love:

{
    "noun": {
        "syn": [
            "passion",
            "beloved",
            "dear"
        ]
    },
    "verb": {
        "syn":[
            "love",
            "roll in the hay",
            "Make out"
        ],
        "ant": [
            "hate"
        ]
    }
}

To turn this JSON string data into something we can use, we must decode it into structures we can use via the encoding/json package.

Inside the Synonyms folder, create a file called bighugo.go and then add the code below:

In the code above, the Bighuge type defines the API key and provides a Synonyms method that is responsible for accessing the endpoint, parsing the response, and returning the results. The synonyms and the the word structures describe the JSON response format in Go terms, namely, an object containing noun and verb objects, which in turn contains a slice of strings in a variable called Syn. The tags (strings in backticks) tell the encoding/json package which fields to map to which variables; this is required since we have given them different names.

The Synonyms method takes a term argument and uses http.Get to make a web request to the API endpoint in which the URL contains, not only the API key value but also the term value too. If the web request fails for some reason, we will make a call to log.Fatalln, which will write the error to the standard error stream and exit the program with a non-zero exit code (actually an exit code of 1). This indicates that an error has occurred. If the web request is successful, we pass the response body to the json.NewDecoder method which decodes the bytes into the data variable that is our synonyms type. We defer the closing of the response body to keep the memory clean before using Go's built-in append function to concatenate both noun and verb synonyms to the syns slice that we then return.

Although we have implemented the Bighuge thesaurus, it's not the only option. Adding a Thesaurus interface allows us to create multiple implementations and use anyone conveniently. Create a thesaurus.go inside the synonyms folder and the interface definition below:

package thesaurus
type Thesaurus interface {
    Synonymns(term string) ([]string, error)
}

This simple interface describes a method that takes a term string and returns either a slice of string containing the synonyms or an error (if something goes wrong). Our Bighuge the structure already implements this interface, but now, anyone can add interchangeable implementations for other services, like dictionary.com or the Merriam-Webster online service.

Create a main.go file inside the synonyms folder and add the code below:

We have written a complete program that can look for synonyms of words by integrating the Bighuge Thesaurus API.

Let's build the program and see what kind of synonyms the API give when we input the word come:

go build && ./synonymns

I couldn't help but laugh at these results, but our program is working. It returns a list of synonyms as the output, one per line.

Getting domain suggestions

By composing the four programs we have built so far, we already have a useful tool for suggesting domain names. We can run the programs while piping the output to the input in an appropriate way. In your terminal, navigate to the parent folder containing the whole program and run the following commands:

./synonyms/synonyms | ./sprinkle/sprinkle | ./coolify/coolify | ./domainify/domainify

Type a few words to see the suggestions, for example, when you type sprinkle and hit enter, you may see the following"

We still haven't solved the biggest problem yet. The fact that we have no idea whether the suggested domain names are available or not. So we have to sit and type each of them into a website. Our final program will address that issue.


Available

Our final program, Available will connect to a WHOIS server to ask for details about the domains passed to it, if no details are returned, we can safely assume that the domain is availale for purchase.

Create a new folder called available alongside others and add a main.go file to it containing the following codes:

The exists function implements the WHOIS specification by opening a connection to port 43 on the specified WhosisServer instance with a call to net.Dial. We then defer closing the connection, which means that no matter how the function exits, Close() will still be called on the conn connection. Once the connection is simple, we simply write the domain followed by rn.

Essentially we are looking for a "no match" in the response, and this is how we will decide whether a domain exists or not. We use the bufio.Scanner method to help us iterate over the lines in the response. Passing the connection to NewScanner also works because net.Conn is an io.Reader too. We use strings.ToLower so we don't have to worry about the case sensitivity and strings.Contains to check whether anyone of the lines contains "no match" text. If it does, we return false (since the domain doesn't exist); otherwise, we return true.

Composing all five programs

Now that we have finished the programs, it is time to put them all together. One way to do that is to use use the os/exec package to run each subprogram while piping the output from one to the input of the next, as per our design.

Create a new folder called domainfinder alongside the other five programs and create a new folder called lib inside this folder. The lib folder is where we will keep builds of our subprograms, but we don't want to copy them every time we make a change. Instead, we will write a script that builds the programs and copies the binaries to the lib folder for us.

Create a new file called build.sh on Unix machines or build.bat for Windows and add the codes below to it.

The script above simply builds all our subprograms (including domainfinder, which we are yet to write). Be sure to give execution rights to the new script by doing chmod +x build.sh

Run the script and look inside the lib folder to ensure that it has indeed placed the binaries for our subprograms. NB: It will throw an error because domainfinder doesn't have any .go files yet.

Create a new file main.go inside domainfinder and insert the following code into the file:

The os/exec package gives us everything we need to run external programs or commands from within Go programs. First, the cmdChain slice contains *exec.Cmd command in the order in which we want to chain them together.

Finally, we have built our domainfinder program that composes all the subprograms together, giving us a solution that is clean, simple and elegant.

A complete code of this project is found here.