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.