Contents

Go CLI - Project Structure

In this post, we will create a proper structure for our CLI created with Cobra in Go.


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!


In case you prefer following along a video, you can checkout the following video on my YouTube channel.


Note
The project structure in the video is a little different from the one referred in this article. In video, we put the root, and greet command in their own packages. However, both approaches are correct. You may pick one as per your use case.

The Structure

If you have been following along, you must have a flat file structure as shown below:

1
2
3
4
5
6
~/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:

1
2
3
4
5
6
7
8
~/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:

1
2
3
4
5
6
7
8
~/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
~/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
~/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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:

1
2
~/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
~/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.