Introduction
Structuring a Go CLI project can vary depending on the complexity and requirements of your project. However, in this article, we will be using a common approach to structuring a Go CLI project followed by different large projects.
In the previous article, we wrote a very basic CLI application using Cargo. However, we didn't structure it very well. All the code was dumped into the main function which is not sustainable as our application grows. Also, the code is not at all testable, and we cannot write a single unit test for it. Let's change that now!
The Structure
If you have been following along, you must have a flat file structure as shown below:
~/workspace/greeter via πΉ v1.20.2
β tree .
.
βββ go.mod
βββ go.sum
βββ main.go
The Go community, as a standard practice, groups all the code files building a CLI under the <project-directory>/cmd/<binary-name> path. It will be clearer as we make changes for our application. In your current directory create a subdirectory as cmd. Under cmd create another subdirectory named after the binary you are building. For instance, in our case we want our binary to be called greeter so the directory structure for us will be:
~/workspace/greeter via πΉ v1.20.2
β tree .
.
βββ cmd
βΒ Β βββ greeter
βββ go.mod
βββ go.sum
βββ main.go
In case we wanted to name our binary as xyz then the directory structure would be:
~/workspace/greeter via πΉ v1.20.2
β tree .
.
βββ cmd
βΒ Β βββ xyz
βββ go.mod
βββ go.sum
βββ main.go
The Code
Now that we have the right directories in place, let's create a few files and move the code to its rightful place.
Under the cmd/greeter subdirectory, create two new files named after the two commands we have - root and greet. So, the files to be created are root.go and greet.go. Once done, you must have the following file structure:
~/workspace/greeter via πΉ v1.20.2
β tree .
.
βββ cmd
βΒ Β βββ greeter
βΒ Β Β Β βββ greet.go
βΒ Β Β Β βββ root.go
βββ go.mod
βββ go.sum
βββ main.go
It is time we move root and greet commands from main.go to their respective files. Here is how the root.go and greet.go would look after the code has been moved:
// cmd/greeter/root.go
package greeter
import (
"fmt"
"github.com/spf13/cobra"
)
func RootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "greeter",
Short: "A basic CLI example",
Long: "A basic CLI example using Cobra",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(cmd.OutOrStdout(), "Welcome to Greeter!")
},
}
cmd.AddCommand(GreetCommand())
return cmd
}
// cmd/greeter/greet.go
package greeter
import (
"fmt"
"github.com/spf13/cobra"
)
func GreetCommand() *cobra.Command {
return &cobra.Command{
Use: "greet",
Short: "Greet someone",
Long: "Greet someone by their name",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(cmd.OutOrStdout(), "Hello, %s!\n", args[0])
},
}
}
Notice that we now have functions that return the commands. It is essential because we also want to write some unit tests for these commands. And, in order to achieve that we need to have these commands exported.
Next, we move the main.go file from root of the directory to cmd. Once moved, you must have the following file structure:
~/workspace/greeter via πΉ v1.20.2
β tree .
.
βββ cmd
βΒ Β βββ greeter
βΒ Β βΒ Β βββ greet.go
βΒ Β βΒ Β βββ root.go
βΒ Β βββ main.go
βββ go.mod
βββ go.sum
Now, we need to update the main function in main.go to use the commands exported by greeter package. Here is how we can do that:
package main
import (
"fmt"
"greeter/cmd/greeter"
"os"
)
func main() {
rootCmd := greeter.RootCommand()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Build & Run
We are almost there; however, before we can build our CLI we need to inform Go that we have updated the package/project structure. We can easily do so by executing the following command from the terminal while in the root directory of our project:
~/workspace/greeter via πΉ v1.20.2
β go mod tidy
And with that, we are all set to finally build our CLI using the command below:
~/workspace/greeter via πΉ v1.20.2
β go build -o greeter cmd/main.go
~/workspace/greeter via πΉ v1.20.2
β ./greeter
Welcome to Greeter!
~/workspace/greeter via πΉ v1.20.2
β ./greeter greet Gaurav
Hello, Gaurav!
Congratulations!! You are now all set to add more commands and features to your CLI.
Conclusion
It's essential to keep your code modular, maintainable, and easy to understand. However, remember that this is a general structure, and you can modify it based on your specific project requirements. In the next article we will write unit tests for each command.