Now that we have completed all five of our programs, it's time to put them all together so that we can use our tool to find an available domain name for our chat application. The simplest way to do this is to use the technique we have been using throughout this chapter: using pipes in a terminal to connect the output and input.
In the terminal, navigate to the parent folder of the five programs and run the following single line of code:
./synonyms/synonyms | ./sprinkle/sprinkle | ./coolify/coolify | ./domainify/domainify | ./available/available
Once the programs are running, type in a starting word and see how it generates suggestions before checking their availability.
For example, typing in chat
might cause the programs to take the following actions:
chat
goes into synonyms
and out comes a series of synonyms:confab
confabulation
schmooze
sprinkle
where they are augmented with web-friendly prefixes and suffixes such as:confabapp
goconfabulation
schmooze time
coolify
, where the vowels are potentially tweaked:confabaapp
goconfabulatioon
schmoooze time
domainify
where they are turned into valid domain names:confabaapp.com
goconfabulatioon.net
schmooze-time.com
available
where they are checked against the WHOIS server to see whether somebody has already taken the domain or not:confabaapp.com
×goconfabulatioon.net
✔schmooze-time.com
✔Running our solution by piping programs together is an elegant architecture, but it doesn't have a very elegant interface. Specifically, whenever we want to run our solution, we have to type the long messy line where each program is listed separated by pipe characters. In this section, we are going to write a Go program that uses the os/exec
package to run each subprogram while piping the output from one into the input of the next as per our design.
Create a new folder called domainfinder
alongside the other five programs, and create another new folder called lib
inside that folder. The lib
folder is where we will keep builds of our subprograms, but we don't want to be copying and pasting them every time we make a change. Instead, we will write a script that builds the subprograms 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 insert the following code:
#!/bin/bash echo Building domainfinder... go build -o domainfinder echo Building synonyms... cd ../synonyms go build -o ../domainfinder/lib/synonyms echo Building available... cd ../available go build -o ../domainfinder/lib/available cd ../build echo Building sprinkle... cd ../sprinkle go build -o ../domainfinder/lib/sprinkle cd ../build echo Building coolify... cd ../coolify go build -o ../domainfinder/lib/coolify cd ../build echo Building domainify... cd ../domainify go build -o ../domainfinder/lib/domainify cd ../build echo Done.
The preceding script simply builds all of our subprograms (including domainfinder
, which we are yet to write) telling go build
to place them in our lib
folder. Be sure to give the new script execution rights by doing chmod +x build.sh
, or something similar. Run this script from a terminal and look inside the lib
folder to ensure that it has indeed placed the binaries for our subprograms in there.
Create a new file called main.go
inside domainfinder
and insert the following code in the file:
package main var cmdChain = []*exec.Cmd{ exec.Command("lib/synonyms"), exec.Command("lib/sprinkle"), exec.Command("lib/coolify"), exec.Command("lib/domainify"), exec.Command("lib/available"), } func main() { cmdChain[0].Stdin = os.Stdin cmdChain[len(cmdChain)-1].Stdout = os.Stdout for i := 0; i < len(cmdChain)-1; i++ { thisCmd := cmdChain[i] nextCmd := cmdChain[i+1] stdout, err := thisCmd.StdoutPipe() if err != nil { log.Fatalln(err) } nextCmd.Stdin = stdout } for _, cmd := range cmdChain { if err := cmd.Start(); err != nil { log.Fatalln(err) } else { defer cmd.Process.Kill() } } for _, cmd := range cmdChain { if err := cmd.Wait(); err != nil { log.Fatalln(err) } } }
The os/exec
package gives us everything we need to work with running external programs or commands from within Go programs. First, our cmdChain
slice contains *exec.Cmd
commands in the order in which we want to join them together.
At the top of the
main
function, we tie the Stdin
(standard in stream) of the first program to the os.Stdin
stream for this program, and the Stdout
(standard out stream) of the last program to the os.Stdout
stream for this program. This means that, like before, we will be taking input through the standard input stream and writing output to the standard output stream.
Our next block of code is where we join the subprograms together by iterating over each item and setting its Stdin
to the Stdout
of the program before it.
The following table shows each program, with a description of where it gets its input from, and where its output goes:
Program |
Input (Stdin) |
Output (Stdout) |
---|---|---|
|
The same |
|
|
|
|
|
|
|
|
|
|
|
|
The same |
We then iterate over each command calling the Start
method, which runs the program in the background (as opposed to the Run
method which will block our code until the subprogram exits—which of course is no good since we have to run five programs at the same time). If anything goes wrong, we bail with log.Fatalln
, but if the program starts successfully, we then defer a call to kill the process. This helps us ensure the subprograms exit when our main
function exits, which will be when the domainfinder
program ends.
Once all of the programs are running, we then iterate over every command again and wait for it to finish. This is to ensure that domainfinder
doesn't exit early and kill off all the subprograms too soon.
Run the build.sh
or build.bat
script again and notice that the domainfinder
program has the same behavior as we have seen before, with a much more elegant interface.