От идеи до CLI-утилиты на Go: пишем, компилируем, деплоим

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Go — идеальный язык для CLI-утилит?

Представьте: вам нужно написать инструмент, который будет запускаться в консоли, обрабатывать файлы, ходить по API или просто автоматизировать рутину. Вы можете выбрать Python, Node.js или Bash. Но если скорость, размер бинарника и простота распространения критичны — ваш выбор CLI утилиты на Go.

Go компилируется в один статический бинарник. Никаких зависимостей, никаких «а у вас Python 3.8 стоит?». Скачал — запустил. Это делает Golang CLI инструменты стандартом де-факто в мире DevOps (вспомните Docker, Kubernetes, Hugo — все они написаны на Go).

В этой статье мы не просто поговорим о теории. Мы создадим с нуля реальную консольную утилиту на Go, которая будет парсить JSON, работать с HTTP и выводить красивые таблицы. Вы узнаете, как структурировать проект, обрабатывать ошибки и компилировать под любую платформу.

Настройка окружения и структура проекта

Прежде чем писать код, убедитесь, что у вас установлен Go (версия 1.21+). Проверить просто:

go version

Создадим директорию для нашего проекта. Назовем утилиту json2table — она будет читать JSON из stdin или файла и выводить данные в виде таблицы.

mkdir json2table
cd json2table
go mod init github.com/ваш-username/json2table

Структура проекта будет классической для разработки на Go:

  • main.go — точка входа
  • internal/ — внутренняя логика (парсинг, вывод)
  • cmd/ — код для обработки команд (если используем Cobra)

Мы начнем с простого, без фреймворков, а потом добавим флаги и хелп.

Пишем ядро утилиты: парсинг JSON и вывод таблицы

Создайте файл internal/parser.go. Здесь мы будем разбирать JSON и превращать его в структуру.

package internal

import (
    "encoding/json"
    "fmt"
    "io"
)

// Record представляет одну строку таблицы
type Record map[string]interface{}

// ParseJSON читает JSON из reader и возвращает слайс записей
func ParseJSON(r io.Reader) ([]Record, error) {
    var data []Record
    decoder := json.NewDecoder(r)
    if err := decoder.Decode(&data); err != nil {
        return nil, fmt.Errorf("ошибка парсинга JSON: %w", err)
    }
    return data, nil
}

Теперь создадим internal/table.go для форматированного вывода:

package internal

import (
    "fmt"
    "strings"
    "text/tabwriter"
    "os"
)

// PrintTable выводит записи в виде таблицы с выравниванием
func PrintTable(records []Record) {
    if len(records) == 0 {
        fmt.Println("Нет данных для отображения")
        return
    }

    // Собираем все уникальные ключи (заголовки столбцов)
    headers := make([]string, 0)
    seen := make(map[string]bool)
    for _, rec := range records {
        for key := range rec {
            if !seen[key] {
                headers = append(headers, key)
                seen[key] = true
            }
        }
    }

    // Используем tabwriter для красивого вывода
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)

    // Заголовок
    fmt.Fprintln(w, strings.Join(headers, "\t"))

    // Разделитель
    sep := make([]string, len(headers))
    for i, h := range headers {
        sep[i] = strings.Repeat("-", len(h))
    }
    fmt.Fprintln(w, strings.Join(sep, "\t"))

    // Данные
    for _, rec := range records {
        row := make([]string, len(headers))
        for i, h := range headers {
            if val, ok := rec[h]; ok {
                row[i] = fmt.Sprintf("%v", val)
            } else {
                row[i] = ""
            }
        }
        fmt.Fprintln(w, strings.Join(row, "\t"))
    }

    w.Flush()
}

Собираем всё в main.go

Теперь напишем точку входа. Наша консольная утилита на Go будет читать данные из stdin (pipe) или из файла, переданного как аргумент.

package main

import (
    "fmt"
    "os"
    "github.com/ваш-username/json2table/internal"
)

func main() {
    var reader *os.File

    // Если передан аргумент — читаем файл, иначе stdin
    if len(os.Args) > 1 {
        file, err := os.Open(os.Args[1])
        if err != nil {
            fmt.Fprintf(os.Stderr, "Ошибка открытия файла: %v\n", err)
            os.Exit(1)
        }
        defer file.Close()
        reader = file
    } else {
        stat, _ := os.Stdin.Stat()
        if (stat.Mode() & os.ModeCharDevice) != 0 {
            fmt.Println("Использование: json2table [файл] или пайп JSON'а")
            os.Exit(1)
        }
        reader = os.Stdin
    }

    records, err := internal.ParseJSON(reader)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
        os.Exit(1)
    }

    internal.PrintTable(records)
}

Проверьте сборку:

go build -o json2table .
echo '[{"name":"Alice","age":30},{"name":"Bob","age":25}]' | ./json2table

Результат:

name    age
----    ---
Alice   30
Bob     25

Круто, правда? Но это только начало. Добавим флаги и сделаем утилиту профессиональной.

Добавляем флаги с помощью стандартной библиотеки

Стандартный пакет flag позволяет легко добавить опции. Давайте поддержим флаг --pretty для форматирования JSON и --delimiter для смены разделителя.

Обновим main.go:

package main

import (
    "flag"
    "fmt"
    "os"
    "github.com/ваш-username/json2table/internal"
)

func main() {
    // Определяем флаги
    pretty := flag.Bool("pretty", false, "Форматировать JSON перед выводом")
    delimiter := flag.String("delimiter", "\t", "Разделитель столбцов в таблице")
    flag.Parse()

    // Остальные аргументы — это файлы
    args := flag.Args()

    var reader *os.File

    if len(args) > 0 {
        file, err := os.Open(args[0])
        if err != nil {
            fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
            os.Exit(1)
        }
        defer file.Close()
        reader = file
    } else {
        stat, _ := os.Stdin.Stat()
        if (stat.Mode() & os.ModeCharDevice) != 0 {
            fmt.Println("Использование: json2table [--pretty] [--delimiter=,] [файл]")
            flag.PrintDefaults()
            os.Exit                

Блоги

Рекомендаций по книгам