Õppetunnile juurdepääsu kontrollimine...

A Real CLI

Organize a Go Project

Video ja lugemine22 minEelvaade

Vaatad tasuta eelvaate õppetundi.

Goal

By the end of this lesson, Tasker has the folder layout that most professional Go projects use. Its module has a proper name, the program lives in cmd/tasker, the core code lives in internal/task, and the project is under version control with Git.

What you will learn

  • How to choose a module path, and why it usually looks like a web address
  • The cmd/ folder for programs and the internal/ folder for private packages
  • How the Go compiler protects internal packages
  • How to build, run, and install a program from a subfolder
  • How to name packages the Go way
  • How to put a Go project under Git

Why this matters

Right now, all of Tasker's main code sits in the project root. That works for a small tool. But Part 2 adds an API server to the same project, and later a test helper and a database package. Each program needs its own folder, and the shared code must be easy to find. When you open any popular Go project, you find the same layout. If you learn it now, you can find your way in other people's code too.

Step 1: Choose a module path

Your go.mod file starts with module tasker. That name works only on your computer. Other projects cannot download or import your code with it.

Go modules are usually named after the place where the code lives, for example a GitHub repository:

github.com/yourname/tasker

Replace yourname with your GitHub user name. You do not need a GitHub repository yet. The path is only a name, but it is a unique name, and when you publish the code later, go get finds it at that address.

Change the module path with one command:

go mod edit -module github.com/yourname/tasker

Open go.mod to check it. The first line is now module github.com/yourname/tasker.

Step 2: Create the new folders

The standard layout looks like this:

tasker/
├── go.mod
├── cmd/
│   └── tasker/
│       ├── main.go
│       ├── commands.go
│       ├── shell.go
│       └── path.go
└── internal/
    └── task/
        ├── actions.go
        ├── errors.go
        ├── file.go
        ├── list.go
        ├── store.go
        └── task.go

Move the files on macOS or Linux:

mkdir -p cmd/tasker internal
mv main.go commands.go shell.go path.go cmd/tasker/
mv task internal/task

On Windows, create the folders and move the files in your file explorer or editor. Delete the play folder and any data folder from earlier lessons. You do not need them anymore.

cmd/ holds one folder for each program that the project builds. Each folder contains a package main. In Part 2, you add cmd/tasker-api next to cmd/tasker.

internal/ holds packages that only this project may use. You learn why this matters in Step 4.

Step 3: Fix the imports

Import paths start with the module path, so every import of the task package must change. In cmd/tasker/commands.go and cmd/tasker/shell.go, replace:

"tasker/task"

with:

"github.com/yourname/tasker/internal/task"

The package name stays task, so the rest of the code does not change. You still write task.NewStore(). Go uses the last part of the import path as the package name.

Tip: most editors can do this for you. In VS Code with the Go extension, a search and replace across all files takes a few seconds. On macOS or Linux, you can also run: grep -rl '"tasker/task"' . | xargs sed -i 's#"tasker/task"#"github.com/yourname/tasker/internal/task"#' (On macOS, write sed -i '' instead of sed -i.)

Now check that everything compiles:

go vet ./...
go list ./...

go list ./... prints every package in the module:

github.com/yourname/tasker/cmd/tasker
github.com/yourname/tasker/internal/task

Step 4: Build and run from the new location

The program is no longer in the project root, so you point the Go tools to its folder:

go run ./cmd/tasker version
go build ./cmd/tasker

Output of the first command:

tasker 0.1.0

go build ./cmd/tasker creates the tasker program in the current folder. The program name comes from the folder name, tasker.

You can also install the program:

go install ./cmd/tasker

go install builds the program and copies it to the Go bin folder, usually ~/go/bin (%USERPROFILE%\go\bin on Windows). If that folder is in your PATH, you can now type tasker from any folder, like any other command:

tasker list

If tasker is not found: run go env GOPATH, then add the bin folder inside it to your PATH. Search for "add folder to PATH" for your operating system if you have not done this before.

Step 5: The internal rule

A package inside a folder named internal can only be imported by code in the folder that contains internal. For Tasker, only code inside tasker/ can import tasker/internal/task.

The compiler enforces this. If another module tries to import your task package:

import "github.com/yourname/tasker/internal/task"

the build fails:

use of internal package github.com/yourname/tasker/internal/task not allowed

Why is this useful? When other people can import a package, every exported name in it becomes a promise. If you rename Store.Add, their code breaks. Code in internal makes no such promise. You can change it as much as you need, and only your own project is affected. Most applications put almost all their code in internal, and only libraries that are meant for others use public packages.

Step 6: Name packages the Go way

Go has clear conventions for package names:

  • Short, lowercase, one word: task, storage, config. No underscores and no capital letters.
  • Name it after what it provides, not after what it contains. Avoid names like utils, helpers, or common. They grow into a box of unrelated code.
  • Do not repeat the package name in exported names. Callers always write the package name first, so task.Task is fine, but task.TaskStore reads as "task task store". That is why the type is called task.Store.

Step 7: Put the project under Git

From now on, you keep every change in Git. Create a file named .gitignore in the project root:

# Built programs
/tasker
/tasker.exe

# Local test data
/data/

Then create the repository and your first commit:

git init
git add .
git commit -m "Organize Tasker into cmd and internal"

The .gitignore file stops Git from saving the built program and your test tasks. Only source code belongs in the repository. Everyone can build the program from the source.

If you want to publish your code, create an empty repository named tasker on GitHub and follow its instructions to push. The module path from Step 1 then points to real code.

Why it works: the layout tells the story

A new developer who opens Tasker can understand it from the folders alone:

  • cmd/tasker is the command-line program. It reads input and prints output.
  • internal/task is the core logic, private to this project.

The layout also guides you when you add code. Does the new code talk to the user? Then it goes in cmd/. Is it a rule about tasks? Then it goes in internal/task. You will add more internal packages in the next lessons, each with one clear job.

Exercises

  1. Run from anywhere. Install Tasker with go install ./cmd/tasker. Open a new terminal in a different folder and run tasker list. Which task file does it use, and why?
  2. Prove the internal rule. Create a new folder outside tasker, run go mod init example.com/other in it, and try to import your task package. Hint: add replace github.com/yourname/tasker => ../tasker and require github.com/yourname/tasker v0.0.0 to its go.mod. Read the error.
  3. A second program. Create cmd/tasker-count/main.go, a tiny program that loads the task file and prints only the number of open tasks. It may import internal/task, because it lives in the same project. Build it with go build ./cmd/tasker-count.
  4. Rename check. Rename the package task to todo (folder and package clause). How many files do you need to change? Change it back afterwards.
  5. Read a real project. Open the GitHub repository of the GitHub CLI (github.com/cli/cli). Find its cmd/ and internal/ folders. What programs does it build?

Checkpoint

You are ready for the next lesson if you can:

  • Explain why a module path usually looks like github.com/name/project
  • Explain what belongs in cmd/ and what belongs in internal/
  • Build, run, and install a program from ./cmd/tasker
  • Explain what the compiler does when another module imports an internal package
  • Name packages following Go conventions