Почему 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