Let’s set up a decent Command Line Interface (CLI) for our Go project. This has been made easy by Cobra, a library that provides a simple interface to create a powerful modern CLI, like you are used to from git, kubectl, etc.
Cobra comes with a very nice set of features, amongst which:
- Easily add commands and nested commands
- Support for flags (including short & long versions)
- Intelligent suggestions (app srver… did you mean app server?)
- Automatic help generation and support of -h and --help
- Automatically generated bash autocomplete and man pages
Installation
To get started, we’ll create a new go project and initialize it, in my case:
go mod init github.com/akleinloog/hello
Then, install the latest version of Cobra:
go get github.com/spf13/cobra/cobra
And initialize the new project:
cobra init --pkg-name github.com/akleinloog/hello
This adds a main.go file and a cmd folder with a root.go file. The main.go file is very simple, contains some license info and a simple main function:
package main
import "github.com/akleinloog/hello/cmd"
func main() {
cmd.Execute()
}
Take a look at the root.go file in the cmd folder, and you will notice a few things. First, it contains a use instruction and a short and a long description. Change the text to something meaningful for your example. In my case:
var rootCmd = &cobra.Command{
Use: "hello",
Short: "Go Hello",
Long: `A simple HTTP Server written in Go.
It gives out a simple Hello message with a counter, the host name and the requested address.`,
}
It is good practice not to define an action here. In that case, Cobra will ensure to give out a nice set of instructions. See for yourself:
go build
./hello
Another thing to notice is that cobra added an Execute() function, an init() function and an initConfig() function. With that, it added Viper and support for a configuration file. We’ll add another flag later on, and I will discuss Viper and proper configuration in another post.
Adding Commands
Let’s first add a new command that will start our HTTP Server:
cobra add serve
Cobra added a serve.go file in the cmd folder, similar to the root.go we saw earlier. This time it adds a serve command. Let’s update the text again:
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts the HTTP Server.",
Long: `Starts the HTTP Server listening at port 80, where it will return a simple hello on any request.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("serve called")
},
}
In the init() method you see that the command is added to the root command:
func init() {
rootCmd.AddCommand(serveCmd)
...
}
Rebuild and run again:
go build
./hello
Notice that the output has now changed. Information is provided on the commands and on flags that are supported.
Try the help:
./hello serve -h
Help specific to the serve command is provided.
Now try the actual command:
./hello serve
You should see the simple ‘serve called’ output.
Add the HTTP Server
Let’s implement the HTTP server. We’ll keep it simple. In serve.go add the following functions:
var (
requestNr int64 = 0
host string = "unknown"
)
func listen() {
currentHost, err := os.Hostname()
if err != nil {
log.Println("Could not determine host name:", err)
} else {
host = currentHost
}
log.Println("Starting Hello Server on " + host)
http.HandleFunc("/", hello)
err = http.ListenAndServe(":80", nil)
if err != nil {
log.Fatal(err)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
requestNr++
message := fmt.Sprintf("Go Hello %d from %s on %s ./%s\n", requestNr, host, r.Method, r.URL.Path[1:])
log.Print(message)
fmt.Fprint(w, message)
}
And change the serve command to:
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts the HTTP Server.",
Long: `Starts the HTTP Server listening at port 80, where it will return a simple hello on any request.`,
Run: func(cmd *cobra.Command, args []string) {
listen()
},
}
Rebuild and run the server:
go build
./hello serve
The HTTP Server will start and you can visit http://localhost to see the result.
Add an HTTP Client
Let’s add another command that will serve as a client for our Server:
cobra add get
Add the following function to get.go:
func get() {
resp, err := http.Get("http://localhost")
if err != nil {
log.Println(err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for i := 0; scanner.Scan() && i < 5; i++ {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Println(err)
}
}
And make sure it is called from the get command:
Run: func(cmd *cobra.Command, args []string) {
get()
},
Rebuild and start the server:
go build
./hello serve
Then from another console window:
./hello get
And you will see the hello as output!
Add a Flag
To finish up, let’s add a flag to specify the port number that our server will listen on.
We’ll keep it simple for now, we want:
- Works for both the client and the server
- Supports the full –port and the shorthand -p
- Default port is 80
In get.go_, declare a variable to be used as port number:
var clientPort int
And add the port flag to the init() function:
getCmd.Flags().IntVarP(&clientPort,"port","p", 80, "port number")
Do the same thing in serve.go, but use serverPort as variable name.
Rebuild and check the help output:
go build
./hello
./hello serve -h
./hello get -h
Let’s adjust the server so it uses the new port flag. In the listen() function in serve.go, replace
err = http.ListenAndServe(":80", nil)
with
log.Printf("Listening on port %d\n", serverPort)
err = http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil)
Now for the client, in the get() function in get.go, replace
resp, err := http.Get("http://localhost")
with
resp, err := http.Get(fmt.Sprintf("http://localhost:%d", clientPort))
Rebuild and start the server:
go build
./hello serve -p 10080
The server will now be available on http://localhost:10080
Then from another console window:
./hello get -p 10080
You should see something similar to: Then from another console window:
Go Hello 1 from my-mac.home on GET ./
The code
The code of my simple Hello Server is available on GitHub here. I may add a few features there since I intend to use it for some Kubernetes experiments. Another similar example is my simple HTTP Logger.