Lesson access পরীক্ষা করা হচ্ছে...

Your First Go Program

Build Your First Command-Line Tool

Video এবং reading22 মিনিটPreview

আপনি একটি free preview lesson দেখছেন।

Goal

By the end of this lesson, you have the first version of the tasker command. It reads commands and arguments from the terminal, checks them, and responds. It also reports errors the way professional command-line tools do.

What you will learn

  • How to read command-line arguments with os.Args
  • How to choose what to do with if and switch
  • The difference between standard output and standard error
  • What exit codes are and how to set them with os.Exit
  • How to turn text into a number safely

Why this matters

Command-line tools are everywhere: git, docker, kubectl, and go itself. Most of them follow the same rules: they take a command, read arguments, print results, and report errors in a standard way. In this lesson, you build these rules by hand. In Module 4, you rebuild Tasker with Cobra, a library that does much of this work for you. If you build it by hand first, you understand what Cobra does for you.

Step 1: Start the Tasker project

From now on, you work in one project for the rest of Part 1. Create it next to your hello folder:

mkdir tasker
cd tasker
go mod init tasker

Copy the task folder from your hello project into tasker. Keep only the Format function and the MaxTitleLength constant in task/task.go:

package task

import "fmt"

// MaxTitleLength is the longest title a task can have.
const MaxTitleLength = 50

// Format returns one line that describes a task.
func Format(id int, title string, done bool) string {
	status := "[ ]"
	if done {
		status = "[x]"
	}
	return fmt.Sprintf("%d. %s %s", id, status, title)
}

Step 2: Read the arguments

Create main.go with a small test:

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println(os.Args)
}

Run it with some arguments:

go run . add Buy milk

The output is similar to this:

[/tmp/go-build123/exe/tasker add Buy milk]

os.Args is a slice of strings. A slice is a list of values. You learn slices in detail in Module 2. For now, you need three things:

  • os.Args[0] is the path of the program itself.
  • os.Args[1] is the first argument, os.Args[2] is the second, and so on.
  • len(os.Args) is the number of items in the list.

The terminal splits arguments at spaces. Buy milk is two arguments. To make it one argument, put quotes around it: "Buy milk".

Step 3: Write the full program

Replace main.go with the complete version:

package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"tasker/task"
)

const version = "0.1.0"

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	command := os.Args[1]

	switch command {
	case "add":
		runAdd(os.Args[2:])
	case "done":
		runDone(os.Args[2:])
	case "version":
		fmt.Println("tasker", version)
	case "help":
		printUsage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command: %s\n", command)
		printUsage()
		os.Exit(1)
	}
}

func runAdd(args []string) {
	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "error: add needs a title")
		os.Exit(1)
	}

	title := strings.Join(args, " ")
	if len(title) > task.MaxTitleLength {
		fmt.Fprintf(os.Stderr, "error: title is longer than %d characters\n", task.MaxTitleLength)
		os.Exit(1)
	}

	fmt.Println("Added:", task.Format(1, title, false))
}

func runDone(args []string) {
	if len(args) != 1 {
		fmt.Fprintln(os.Stderr, "error: done needs exactly one task ID")
		os.Exit(1)
	}

	id, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %q is not a valid task ID\n", args[0])
		os.Exit(1)
	}

	fmt.Println("Completed:", task.Format(id, "(title comes in Module 2)", true))
}

func printUsage() {
	fmt.Fprintln(os.Stderr, `Usage: tasker <command> [arguments]

Commands:
  add <title>   Add a new task
  done <id>     Mark a task as done
  version       Print the version
  help          Show this help`)
}

Build the program and try it:

go build
./tasker add Buy milk
./tasker done 3
./tasker version

On Windows, use .\tasker.exe instead of ./tasker.

Output:

Added: 1. [ ] Buy milk
Completed: 3. [x] (title comes in Module 2)
tasker 0.1.0

Now try some wrong input:

./tasker
./tasker add
./tasker done abc
./tasker fly

Each one prints a clear message. For example:

error: "abc" is not a valid task ID

Tasker does not save tasks yet, so every new task gets ID 1. In Module 2, you store tasks in memory, and in Module 3 you save them to a file.

Step 4: How the program decides what to do

The first check makes sure there is a command:

if len(os.Args) < 2 {

If the user types only tasker, the list has one item, the program path. Without this check, os.Args[1] would crash the program. Always check the length before you read an item from a slice.

switch compares one value against several cases:

switch command {
case "add":
	runAdd(os.Args[2:])
...
default:
	...
}

Go runs the first case that matches and then stops. You do not need break as in some other languages. The default case runs when no other case matches.

os.Args[2:] means "every item from position 2 to the end." For tasker add Buy milk, it is [Buy milk]. Each command function gets only its own arguments.

strings.Join(args, " ") joins the items into one string, with a space between them. So tasker add Buy milk works without quotes.

Step 5: Standard output and standard error

Every program has two output streams:

  • Standard output (stdout) is for results. fmt.Println writes here.
  • Standard error (stderr) is for errors and help messages. fmt.Fprintln(os.Stderr, ...) writes here.

Both appear in your terminal, so they look the same. The difference matters when someone saves or processes the output. Try this:

./tasker add Buy milk > out.txt
./tasker fly > out.txt

The first command saves the result in out.txt. The second command still shows the error in the terminal. > only redirects stdout, so errors are never hidden inside result files.

Fprintln and Fprintf work like Println and Printf, but their first argument says where to write.

Step 6: Exit codes

When a program ends, it returns a number to the system called the exit code:

  • 0 means success.
  • Any other number means failure. 1 is the most common one.

Scripts and other tools read this number to know if your command worked. Check it yourself right after you run a command:

./tasker version
echo $?
./tasker fly
echo $?

The first echo prints 0. The second prints 1. In Windows PowerShell, use echo $LASTEXITCODE.

os.Exit(1) ends the program immediately with exit code 1. When main returns normally, the exit code is 0.

Why it works: small functions with one job each

main does only one thing: it reads the command and sends the work to the right function. Each run function checks its own arguments and does its own work. This shape keeps the program easy to read and easy to extend. To add a command, you add one case and one function.

Cobra uses the same idea. In Module 4, each command becomes its own Cobra command, and Cobra handles help text, unknown commands, and flags for you.

Exercises

  1. Add a list command. It prints three example tasks with task.Format, one done and two open. Add it to the help text.
  2. Accept --help. Make tasker --help and tasker -h show the help text and exit with code 0. Hint: one case can match several values, like case "help", "--help", "-h":.
  3. Reject bad IDs. tasker done -5 and tasker done 0 are accepted right now. Change runDone so that IDs must be greater than 0.
  4. Trim the title. tasker add " " adds a task with an empty-looking title. Use strings.TrimSpace to remove spaces from both ends, then reject titles that are empty.
  5. Check streams and exit codes. For each command in Step 3, predict if its output goes to stdout or stderr, and what its exit code is. Then check with > and echo $?.

Checkpoint

You have finished Module 1 if you can:

  • Explain what os.Args[0], os.Args[1], and os.Args[2:] contain
  • Use switch to choose between commands
  • Explain when to write to stdout and when to write to stderr
  • Explain what exit code 0 and exit code 1 mean
  • Turn a text argument into a number and handle the error