Погружение в Go путем создания приложения CLI

Вы обернули голову вокруг синтаксиса Go и практиковались в них один за другим, однако вы не будете чувствовать себя комфортно при написании приложений в Go, если не построите их.

В этом посте блога мы построим CLI-приложение в Go, которое мы будем называть go-grab-xkcd. Это приложение получает комиксы из XKCD и предоставляет вам различные опции через аргументы командной строки.

Мы не будем использовать никаких внешних зависимостей и соберем все приложение, используя только стандартную библиотеку Go.

Идея приложения выглядит глупо, но его целью является получение удобства написания производственного (своего рода) кода в Go, а не приобретение Google.

В конце также есть Бэш-бонус.

Замечание: Этот пост предполагает, что читатель знаком с синтаксисом и терминологией Go и находится где-то между новичком и промежуточным звеном.

Давайте сначала запустим приложение и посмотрим на него в действии…

$ go-grab-xkcd --help

Usage of go-grab-xkcd:
  -n int
        Comic number to fetch (default latest)
  -o string
        Print output in format: text/json (default "text")
  -s    Save image to current directory
  -t int
        Client timeout in seconds (default 30)
$ go-grab-xkcd -n 323

Title: Ballmer Peak
Comic No: 323
Date: 1-10-2007
Description: Apple uses automated schnapps IVs.
Image: https://imgs.xkcd.com/comics/ballmer_peak.png
$ go-grab-xkcd -n 323 -o json
{
  "title": "Ballmer Peak",
  "number": 323,
  "date": "1-10-2007",
  "description": "Apple uses automated schnapps IVs.",
  "image": "https://imgs.xkcd.com/comics/ballmer_peak.png"
}

Вы можете попробовать остальные варианты, загрузив и запустив приложение для своего компьютера.

После окончания этого урока вы освоитесь со следующими темами:

  1. Принятие аргументов командной строки
  2. Взаимопреобразование между JSON и Go Structs
  3. Выполнение вызовов API
  4. Создание файлов (загрузка и сохранение из Интернета)
  5. Манипуляция строкой

Ниже приведена структура проекта

$ tree go-grab-xkcd
go-grab-xkcd
├── client
│   └── xkcd.go
└── model
    └── comic.go
├── main.go
└── go.mod
  • go.mod- Файл Go Modules, используемый в Go для управления пакетами
  • main.go - Основная точка входа в приложение
  • comic.go- Идет представление данных в виде structи операций над ним
  • xkcd.go - клиент xkcd для выполнения HTTP-вызовов к API, анализа ответа и сохранения на диск

1: Инициализация проекта

Необходимо создать go.mod файл

$ go mod init

Это поможет в управлении пакетами (например, package.json в JS).

2: API xkcd

xkcd удивителен, вам не нужны никакие регистрации или ключи доступа для использования их API. Откройте «документацию» API xkcd, и вы увидите, что есть 2 конечные точки:

  1. http://xkcd.com/info.0.json - ПОЛУЧИТЬ последний комикс
  2. http://xkcd.com/614/info.0.json - Получить конкретный комикс по номеру комика

Ниже приводится ответ JSON от этих конечных точек:

{
  "num": 2311,
  "month": "5",
  "day": "25",
  "year": "2020",
  "title": "Confidence Interval",
  "alt": "The worst part is that's the millisigma interval.",
  "img": "https://imgs.xkcd.com/comics/confidence_interval.png",
  "safe_title": "Confidence Interval",
  "link": "",
  "news": "",
  "transcript": ""
}

Релевантный xkcd

2: Создание модели для комикса

На основании вышеизложенного ответ JSON, мы создаем struct называется ComicResponse в comic.go внутри model пакета

type ComicResponse struct {
	Month      string `json:"month"`
	Num        int    `json:"num"`
	Link       string `json:"link"`
	Year       string `json:"year"`
	News       string `json:"news"`
	SafeTitle  string `json:"safe_title"`
	Transcript string `json:"transcript"`
	Alt        string `json:"alt"`
	Img        string `json:"img"`
	Title      string `json:"title"`
	Day        string `json:"day"`
}

Вы можете использовать инструмент JSON-to-Go для автоматической генерации структуры из JSON.

Также создайте другую структуру, которая будет использоваться для вывода данных из нашего приложения.

type Comic struct {
	Title       string `json:"title"`
	Number      int    `json:"number"`
	Date        string `json:"date"`
	Description string `json:"description"`
	Image       string `json:"image"`
}

Добавьте следующие два метода в ComicResponse структуру

// FormattedDate formats individual date elements into a single string
func (cr ComicResponse) FormattedDate() string {
	return fmt.Sprintf("%s-%s-%s", cr.Day, cr.Month, cr.Year)
}
// Comic converts ComicResponse that we receive from the API to our application's output format, Comic
func (cr ComicResponse) Comic() Comic {
	return Comic{
		Title:       cr.Title,
		Number:      cr.Num,
		Date:        cr.FormattedDate(),
		Description: cr.Alt,
		Image:       cr.Img,
	}
}

Затем добавьте следующие два метода в Comic структуру

// PrettyString creates a pretty string of the Comic that we'll use as output
func (c Comic) PrettyString() string {
	p := fmt.Sprintf(
		"Title: %s\nComic No: %d\nDate: %s\nDescription: %s\nImage: %s\n",
		c.Title, c.Number, c.Date, c.Description, c.Image)
	return p
}
// JSON converts the Comic struct to JSON, we'll use the JSON string as output
func (c Comic) JSON() string {
	cJSON, err := json.Marshal(c)
	if err != nil {
		return ""
	}
	return string(cJSON)
}

3: Настройка клиента xkcd для создания запроса, анализа ответа и сохранения на диск

Создайте xkcd.goфайл внутри clientпакета.

Сначала определим собственный тип , называемый ComicNumberкакint

type ComicNumber int

Определим постоянные:

const (
	// BaseURL of xkcd
	BaseURL string = "https://xkcd.com"
	// DefaultClientTimeout is time to wait before cancelling the request
	DefaultClientTimeout time.Duration = 30 * time.Second
	// LatestComic is the latest comic number according to the xkcd API
	LatestComic ComicNumber = 0
)

Создайте структуру XKCDClient, она будет использоваться для отправки запросов к API.

// XKCDClient is the client for XKCD
type XKCDClient struct {
	client  *http.Client
	baseURL string
}

// NewXKCDClient creates a new XKCDClient
func NewXKCDClient() *XKCDClient {
	return &XKCDClient{
		client: &http.Client{
			Timeout: DefaultClientTimeout,
		},
		baseURL: BaseURL,
	}
}

Добавьте следующие 4 метода к XKCDClient

1. SetTimeout()

// SetTimeout overrides the default ClientTimeout
func (hc *XKCDClient) SetTimeout(d time.Duration) {
    hc.client.Timeout = d
}

2. Fetch()

// Fetch retrieves the comic as per provided comic number
func (hc *XKCDClient) Fetch(n ComicNumber, save bool) (model.Comic, error) {
    resp, err := hc.client.Get(hc.buildURL(n))
    if err != nil {
        return model.Comic{}, err
    }
    defer resp.Body.Close()

    var comicResp model.ComicResponse
    if err := json.NewDecoder(resp.Body).Decode(&comicResp); err != nil {
        return model.Comic{}, err
    }

    if save {
        if err := hc.SaveToDisk(comicResp.Img, "."); err != nil {
            fmt.Println("Failed to save image!")
        }
    }
    return comicResp.Comic(), nil
}

3. SaveToDisk()

// SaveToDisk downloads and saves the comic locally
func (hc *XKCDClient) SaveToDisk(url, savePath string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    absSavePath, _ := filepath.Abs(savePath)
    filePath := fmt.Sprintf("%s/%s", absSavePath, path.Base(url))

    file, err := os.Create(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    _, err = io.Copy(file, resp.Body)
    if err != nil {
        return err
    }
    return nil
}

4. buildURL()

func (hc *XKCDClient) buildURL(n ComicNumber) string {
    var finalURL string
    if n == LatestComic {
        finalURL = fmt.Sprintf("%s/info.0.json", hc.baseURL)
    } else {
        finalURL = fmt.Sprintf("%s/%d/info.0.json", hc.baseURL, n)
    }
    return finalURL
}

4: Подключаем все

Внутри main() функции мы подключаем все провода

  • Прочитайте аргументы команды
  • Создать экземпляр XKCDClient
  • Получить из API, используя XKCDClient
  • Вывод
Прочитайте аргументы команды
comicNo := flag.Int(
    "n", int(client.LatestComic), "Comic number to fetch (default latest)",
)
clientTimeout := flag.Int64(
    "t", int64(client.DefaultClientTimeout.Seconds()), "Client timeout in seconds",
)
saveImage := flag.Bool(
    "s", false, "Save image to current directory",
)
outputType := flag.String(
    "o", "text", "Print output in format: text/json",
)
flag.Parse()
Создаем экземпляр XKCDClient
xkcdClient := client.NewXKCDClient()
xkcdClient.SetTimeout(time.Duration(*clientTimeout) * time.Second)
Получаем из API, используя XKCDClient
comic, err := xkcdClient.Fetch(client.ComicNumber(*comicNo), *saveImage)
if err != nil {
    log.Println(err)
}
Вывод
if *outputType == "json" {
    fmt.Println(comic.JSON())
} else {
    fmt.Println(comic.PrettyString())
}

Запустите программу следующим образом:

$ go run main.go -n 323 -o json

Или создайте его как исполняемый файл для вашего ноутбука, а затем запустите

$ go build .
$ ./go-grab-xkcd -n 323 -s -o json

Найдите полный исходный код в репозитории Github - go-grab-xkcd

Баш Бонус

Скачать несколько комиксов поочередно с помощью этой простой магии оболочки

$ for i in {1..10}; do ./go-grab-xkcd -n $i -s; done;

Приведенный выше код оболочки просто вызывает нашу go-grab-xkcdкоманду в forцикле, а iзначение подставляется в виде комического числа, поскольку xkcd использует последовательные целые числа в качестве комического числа / идентификатора.