Как создать свое первое веб-приложение с помощью Go
Tags: go, webdev, news, tutorial
Published: 2019-11-08 14:57:43

# Как создать свое первое веб-приложение с помощью Go

> Эта статья является переводом статьи "How to build your first web application with Go" автора Ayooluwa Isaiah, [ссылка на оригинал](https://freshman.tech/web-development-with-go/).

Это руководство к вашему первому веб-приложению на Go. Мы создадим [новостное приложение](https://freshman-news.herokuapp.com/), которое использует [News API](https://newsapi.org/) для получения новостных статей по определенной теме, и развернём его на продакшн сервере в конце.

Вы можете найти полный код, используемый для этого урока в этом  [GitHub репозитории](https://github.com/Freshman-tech/news-demo).

## Требования
Единственное требование для этого задания - чтобы на вашем компьютере был [установлен Go]((https://golang.org/doc/install)), и вы немного знакомы с его синтаксисом и конструкциями. Версия Go, которую я использовал при создании приложения, также является самой последней на момент написания: **1.12.9**. Чтобы просмотреть установленную версию Go, используйте команду `go version`.

Если вы считаете это задание слишком сложным для вас, перейдите к моему предыдущему [вводному уроку](https://freshman.tech/golang-guess/) по языку, который должен помочь вам освоиться.

## Итак, начнем!

[Клонируем репозиторий стартовых файлов на GitHub](https://github.com/Freshman-tech/news-demo-starter-files) и `cd` в созданный каталог. У нас есть три основных файла: В файле `main.go` мы напишем весь код Go для этого задания. Файл `index.html` - это шаблон, который будет отправлен в браузер, а `стили` для приложения находятся в `assets/styles.css`.

## Создадим базовый веб-сервер

Давайте начнем с создания базового сервера, который отправляет текст «Hello World!» в браузер при выполнении запроса GET к корню сервера. Измените ваш файл `main.go` так, чтобы он выглядел следующим образом:

```go
package main

import (
	"net/http"
	"os"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("<h1>Hello World!</h1>"))
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	mux := http.NewServeMux()

	mux.HandleFunc("/", indexHandler)
	http.ListenAndServe(":"+port, mux)
}
```

Первая строка `package main` - декларирует, что код в файле `main.go` принадлежит главному пакету. После этого мы импортировали пакет `net/http`, который предоставляет реализации клиента и сервера HTTP для использования в нашем приложении. Этот пакет является частью стандартной библиотеки и входит в каждую установку Go.

В функции `main`,  `http.NewServeMux()` создает новый мультиплексор HTTP-запросов и присваивает его переменной `mux`. По сути, мультиплексор запросов сопоставляет URL-адрес входящих запросов со списком зарегистрированных путей и вызывает соответствующий обработчик для пути всякий раз, когда найдено совпадение.

Далее мы регистрируем нашу первую функцию-обработчик для корневого пути `/`. Эта функция-обработчик является вторым аргументом для `HandleFunc` и всегда имеет сигнатуру `func (w http.ResponseWriter, r * http.Request)`.

Если вы посмотрите на функцию `indexHandler`, вы увидите, что она имеет именно такую сигнатуру, что делает ее действительным вторым аргументом для `HandleFunc`. Параметр `w` - это структура, которую мы используем для отправки ответов на HTTP-запрос. Она реализует метод `Write()`, который принимает слайс байтов и записывает  объединенные данные как часть HTTP-ответа.

С другой стороны, параметр `r` представляет HTTP-запрос, полученный от клиента. Это то, как мы получаем доступ к данным, отправляемым веб-браузером на сервере. Мы еще не используем его здесь, но мы точно будем использовать его позже.

Наконец, у нас есть метод `http.ListenAndServe()`, который запускает сервер на порту 3000, если порт не установлен окружением. Не стесняйтесь использовать другой порт, если 3000 используется на вашем компьютере.

Затем скомпилируйте и выполните код, который вы только что написали:

```bash
go run main.go
```

Если вы перейдете на [http: // localhost: 3000](http://localhost:3000/) в своем браузере, вы должны увидеть текст «Hello World!».

[![Brave browser showing Hello World text](https://res.cloudinary.com/freshman/image/upload/v1566453173/Screenshot_from_2019-08-22_06-52-26.png)](https://res.cloudinary.com/freshman/image/upload/v1566453173/Screenshot_from_2019-08-22_06-52-26.png)

## Шаблоны в Go

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

Шаблоны предоставляют простой способ настроить вывод вашего веб-приложения в зависимости от маршрута без необходимости писать один и тот же код в разных местах. Например, мы можем создать шаблон для панели навигации и использовать его на всех страницах сайта, не дублируя код. Кроме того, мы также получаем возможность добавить некоторую базовую логику на наши веб-страницы.

Go предоставляет две библиотеки шаблонов в своей стандартной библиотеке: `text/template` и `html/template`. Оба предоставляют один и тот же интерфейс, однако пакет `html/template` используется для генерации HTML-вывода, который защищен от инъекций кода, поэтому мы будем использовать его здесь.

Импортируйте этот пакет в ваш файл `main.go` и используйте его следующим образом:

```go
package main

import (
	"html/template"
	"net/http"
	"os"
)

var tpl = template.Must(template.ParseFiles("index.html"))

func indexHandler(w http.ResponseWriter, r *http.Request) {
	tpl.Execute(w, nil)
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	mux := http.NewServeMux()

	mux.HandleFunc("/", indexHandler)
	http.ListenAndServe(":"+port, mux)
}
```

`tpl` - переменная уровня пакета, которая указывает на определение шаблона из предоставленных файлов. Вызов `template.ParseFiles` анализирует файл`index.html` в корне каталога нашего проекта и проверяет его на валидность.

Мы оборачиваем вызов `template.ParseFiles` в `template.Must`, чтобы код вызывал панику при возникновении ошибки. Причина, по которой мы паникуем здесь вместо того, чтобы пытаться обработать ошибку, заключается в том, что нет смысла продолжать выполнение кода, если у нас невалидный шаблон. Это проблема, которая должна быть устранена перед попыткой перезапустить сервер.

В функции `indexHandler` мы выполняем созданный ранее шаблон, предоставляя два аргумента: куда мы хотим записать выходные данные и данные, которые мы хотим передать в шаблон.

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

Остановите запущенный процесс в вашем терминале с помощью Ctrl-C и запустите его снова с помощью `go run main.go`, затем обновите ваш браузер. Вы должны увидеть текст «News App Demo» на странице, как показано ниже:

[![Brave browser showing News App Demo Text](https://res.cloudinary.com/freshman/image/upload/v1566454096/Screenshot_from_2019-08-22_07-08-05.png)](https://res.cloudinary.com/freshman/image/upload/v1566454096/Screenshot_from_2019-08-22_07-08-05.png)

## Добавляем панель навигации на страницу

Замените содержимое тега `<body>` в вашем файле index.html, как показано ниже:

```html
<main>
  <header>
    <a class="logo" href="/">News Demo</a>
    <form action="/search" method="GET">
      <input autofocus class="search-input" value=""
      placeholder="Enter a news topic" type="search" name="q">
    </form>
    <a href="https://github.com/freshman-tech/news" class="button
      github-button">View on Github</a>
  </header>
</main>
```

Затем перезагрузите сервер и обновите ваш браузер. Вы должны увидеть что-то похожее на это:

[![Browser showing unstyled navigation bar](https://res.cloudinary.com/freshman/image/upload/v1566454745/Screenshot_from_2019-08-22_07-18-52.png)](https://res.cloudinary.com/freshman/image/upload/v1566454745/Screenshot_from_2019-08-22_07-18-52.png)

## Работа со статическими файлами

Обратите внимание, что панель навигации, которую мы добавили выше, не имеет стилей, несмотря на тот факт, что мы уже указали их в `<head>` нашего документа.

Это потому, что путь `/` фактически совпадает со всеми путями, которые не обрабатываются в другом месте. Поэтому, если вы перейдете на [http://localhost:3000/assets/style.css](http://localhost:3000/assets/style.css), вы все равно получите домашнюю страницу News Demo вместо файла CSS, потому что маршрут `/assets/style.css` не был объявлен специально.

Но необходимость объявлять явные обработчики для всех наших статических файлов нереальна и не может масштабироваться. К счастью, мы можем создать один обработчик для обслуживания всех статических ресурсов.

Первое, что нужно сделать, - создать экземпляр объекта файлового сервера, передав каталог, в котором находятся все наши статические файлы:

```go
fs := http.FileServer(http.Dir("assets"))
```

Далее нам нужно указать нашему маршрутизатору использовать этот объект файлового сервера для всех путей, начинающихся с префикса `/assets/`:

```go
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
```

Теперь всё вместе:

```go
// main.go

// начало файла

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	mux := http.NewServeMux()

	// Добавьте следующие две строки
	fs := http.FileServer(http.Dir("assets"))
	mux.Handle("/assets/", http.StripPrefix("/assets/", fs))

	mux.HandleFunc("/", indexHandler)
	http.ListenAndServe(":"+port, mux)
}
```
Перезагрузите сервер и обновите браузер. Стили должны включиться, как показано ниже:

[![Brave browser showing styled navigation bar](https://res.cloudinary.com/freshman/image/upload/v1566455131/Screenshot_from_2019-08-22_07-25-09.png)](https://res.cloudinary.com/freshman/image/upload/v1566455131/Screenshot_from_2019-08-22_07-25-09.png)

## Создаем роут /search

Давайте создадим роут, который обрабатывает поисковые запросы для новостных статей. Мы будем использовать [News API](https://newsapi.org/) для обработки запросов, поэтому вам нужно зарегистрироваться для получения бесплатного ключа API [здесь](https://newsapi.org/register).

Этот маршрут ожидает два параметра запроса: `q` представляет запрос пользователя, а `page` используется для пролистывания результатов. Этот параметр `page` является необязательным. Если он не включен в URL, мы просто предположим, что номер страницы результатов имеет значение «1».

Добавьте следующий обработчик под `indexHandler` в ваш файл` main.go`:

```go
func searchHandler(w http.ResponseWriter, r *http.Request) {
	u, err := url.Parse(r.URL.String())
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("Internal server error"))
		return
	}

	params := u.Query()
	searchKey := params.Get("q")
	page := params.Get("page")
	if page == "" {
		page = "1"
	}

	fmt.Println("Search Query is: ", searchKey)
	fmt.Println("Results page is: ", page)
}
```
Приведенный выше код извлекает параметры `q` и `page` из URL-адреса запроса и выводит их оба в терминал.

Затем зарегистрируйте функцию `searchHandler` в качестве обработчика пути`/search`, как показано ниже:

```go
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	mux := http.NewServeMux()

	fs := http.FileServer(http.Dir("assets"))
	mux.Handle("/assets/", http.StripPrefix("/assets/", fs))

  // Add the next line
	mux.HandleFunc("/search", searchHandler)
	mux.HandleFunc("/", indexHandler)
	http.ListenAndServe(":"+port, mux)
}
```
Не забудьте импортировать пакеты `fmt` и` net/url` сверху:

```go
import (
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"os"
)
```
Теперь перезапустите сервер, введите запрос в поле поиска и проверьте терминал. Вы должны увидеть ваш запрос в терминале, как показано ниже:


<figure class="video_container">
  <iframe src="https://res.cloudinary.com/freshman/video/upload/v1566455875/Peek_2019-08-22_07-37.webm" frameborder="0" allowfullscreen="true"> </iframe>
</figure>

## Создаём модель данных

Когда мы делаем запрос к конечной точке [News API`/everything`](https://newsapi.org/docs/endpoints/everything), мы ожидаем ответ json в следующем формате:

```json
{
  "status": "ok",
  "totalResults": 4661,
  "articles": [
    {
      "source": {
        "id": null,
        "name": "Gizmodo.com"
      },
      "author": "Jennings Brown",
      "title": "World's Dumbest Bitcoin Scammer Tries to Scam Bitcoin Educator, Gets Scammed in The Process",
      "description": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about di…",
      "url": "https://gizmodo.com/worlds-dumbest-bitcoin-scammer-tries-to-scam-bitcoin-ed-1837032058",
      "urlToImage": "https://i.kinja-img.com/gawker-media/image/upload/s--uLIW_Oxp--/c_fill,fl_progressive,g_center,h_900,q_80,w_1600/s4us4gembzxlsjrkmnbi.png",
      "publishedAt": "2019-08-07T16:30:00Z",
      "content": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about..."
    }
  ]
}
```
Чтобы работать с этими данными в Go, нам нужно сгенерировать структуру, которая отражает данные при декодировании тела ответа. Конечно, вы можете сделать это вручную, но я предпочитаю использовать веб-сайт [JSON-to-Go](https://mholt.github.io/json-to-go/), который делает этот процесс действительно простым. Он генерирует структуру Go (с тегами), которая будет работать для этого JSON.

Все, что вам нужно сделать, это скопировать объект JSON и вставить его в поле, помеченное **JSON**, затем скопировать вывод и вставить его в свой код. Вот что мы получаем для вышеуказанного объекта JSON:
```go
type AutoGenerated struct {
	Status       string `json:"status"`
	TotalResults int    `json:"totalResults"`
	Articles     []struct {
		Source struct {
			ID   interface{} `json:"id"`
			Name string      `json:"name"`
		} `json:"source"`
		Author      string    `json:"author"`
		Title       string    `json:"title"`
		Description string    `json:"description"`
		URL         string    `json:"url"`
		URLToImage  string    `json:"urlToImage"`
		PublishedAt time.Time `json:"publishedAt"`
		Content     string    `json:"content"`
	} `json:"articles"`
}
```

[![Brave browser showing JSON to Go tool](https://res.cloudinary.com/freshman/image/upload/v1566456278/2019-08-22_07-44.png)](https://res.cloudinary.com/freshman/image/upload/v1566456278/2019-08-22_07-44.png)

Я сделал несколько изменений в структуре `AutoGenerated`, отделив фрагмент `Articles` в его собственную структуру и обновив имя структуры. Вставьте следующее ниже объявление переменной `tpl` в `main.go` и добавьте пакет `time` в ваш импорт:

```go
type Source struct {
	ID   interface{} `json:"id"`
	Name string      `json:"name"`
}

type Article struct {
	Source      Source    `json:"source"`
	Author      string    `json:"author"`
	Title       string    `json:"title"`
	Description string    `json:"description"`
	URL         string    `json:"url"`
	URLToImage  string    `json:"urlToImage"`
	PublishedAt time.Time `json:"publishedAt"`
	Content     string    `json:"content"`
}

type Results struct {
	Status       string    `json:"status"`
	TotalResults int       `json:"totalResults"`
	Articles     []Article `json:"articles"`
}
```

Как вы, возможно, знаете, Go требует, чтобы все экспортируемые поля в структуре начинались с заглавной буквы. Однако принято представлять поля JSON с помощью [camelCase](https://ru.wikipedia.org/wiki/Camel_case) или [snake_case](https://ru.wikipedia.org/wiki/Snake_case), которые не начинаются с заглавной буквы.

Поэтому мы используем теги поля структуры, такие как `json:"id"`, чтобы явно отобразить поле структуры в поле JSON, как показано выше. Это также позволяет использовать совершенно разные имена для структурного поля и соответствующего поля json, если это необходимо.

Наконец, давайте создадим другой тип структуры для каждого поискового запроса. Добавьте это ниже структуры `Results` в `main.go`:
```go
type Search struct {
	SearchKey  string
	NextPage   int
	TotalPages int
	Results    Results
}
```
Эта структура представляет собой каждый поисковый запрос, сделанный пользователем. `SearchKey` - это сам запрос, поле `NextPage` позволяет пролистывать результаты, `TotalPages` - общее количество страниц результатов запроса, а `Results` - текущая страница результатов запроса.

## Отправляем запрос по News API и рендерим результаты
Теперь, когда у нас есть модель данных для нашего приложения, давайте продолжим и сделаем запросы к News API, а затем отрендерим результаты на странице.

Поскольку для News API требуется ключ API, нам нужно найти способ передать его в нашем приложении без жесткого кодирования в коде. Переменные среды являются распространенным подходом, но я решил использовать вместо них флаги командной строки. Go предоставляет пакет `flag`, поддерживающий базовый анализ флагов командной строки, и это то, что мы собираемся использовать здесь.

Сначала объявите новую переменную `apiKey` под переменной `tpl`:

```go
var apiKey *string
```
Затем используйте её в функции `main` следующим образом:
```go
func main() {
	apiKey = flag.String("apikey", "", "Newsapi.org access key")
	flag.Parse()

	if *apiKey == "" {
		log.Fatal("apiKey must be set")
	}

	// остальная часть функции
}
```
Здесь мы вызываем метод `flag.String()`, который позволяет нам определять строковый флаг. Первый аргумент этого метода - имя флага, второй - значение по умолчанию, а третий - описание использования.

После определения всех флагов вам нужно вызвать `flag.Parse()`, чтобы фактически проанализировать их. Наконец, так как `apikey` является обязательным компонентом для этого приложения, мы обеспечиваем аварийное завершение программы, если этот флаг не установлен при выполнении программы.

Убедитесь, что вы добавили пакет `flag` в свой импорт, затем перезапустите сервер и передайте требуемый флаг `apikey`, как показано ниже:

```bash
go run main.go -apikey=<your newsapi access key>
```
Далее, давайте продолжим и обновим `searchHandler`, чтобы поисковый запрос пользователя отправлялся на [newsapi.org](http://newsapi.org/) и результаты отображались в нашем шаблоне.

Замените два вызова метода `fmt.Println()` в конце функции `searchHandler` следующим кодом:

```go
func searchHandler(w http.ResponseWriter, r *http.Request) {
	// beginning  of the function

	search := &Search{}
	search.SearchKey = searchKey

	next, err := strconv.Atoi(page)
	if err != nil {
		http.Error(w, "Unexpected server error", http.StatusInternalServerError)
		return
	}

	search.NextPage = next
	pageSize := 20

	endpoint := fmt.Sprintf("https://newsapi.org/v2/everything?q=%s&pageSize=%d&page=%d&apiKey=%s&sortBy=publishedAt&language=en", url.QueryEscape(search.SearchKey), pageSize, search.NextPage, *apiKey)
	resp, err := http.Get(endpoint)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	err = json.NewDecoder(resp.Body).Decode(&search.Results)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize)))
	err = tpl.Execute(w, search)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
	}
}
```

Сначала мы создаем новый экземпляр структуры `Search` и устанавливаем значение поля `SearchKey` равным значению параметра URL `q` в HTTP-запросе.

После этого мы конвертируем переменную `page` в целое число и присваиваем результат полю `NextPage` переменной `search`. Затем мы создаем переменную `pageSize` и устанавливаем ее значение равным 20. Эта переменная `pageSize` представляет количество результатов, которые API новостей будет возвращать в своем ответе. Это значение может находиться в диапазоне от 0 до 100.

Затем мы создаем конечную точку с помощью `fmt.Sprintf()` и делаем запрос GET к ней. Если ответ от News API не [200 OK](https://freshman.tech/http-status-codes/#200-ok), мы вернем клиенту общую ошибку сервера. В противном случае тело ответа парсится в `search.Results`.

Затем мы вычисляем общее количество страниц путем деления поля `TotalResults` на `pageSize`. Например, если запрос возвращает 100 результатов, а мы одновременно просматриваем только 20, нам нужно будет пролистать пять страниц, чтобы просмотреть все 100 результатов по этому запросу.

После этого мы рендерим наш шаблон и передаем переменную `search` в качестве интерфейса данных. Это позволяет нам получать доступ к данным из объекта JSON в нашем шаблоне, как вы увидите.

Прежде чем перейти к `index.html`, обязательно обновите ваши импорты, как показано ниже:

```go
import (
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"log"
	"math"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"time"
)
```

Давайте продолжим и отобразим результаты на странице, изменив файл `index.html` следующим образом. Добавьте это под тегом `<header>`:

```html
<section class="container">
  <ul class="search-results">
    {{ range .Results.Articles }}
      <li class="news-article">
        <div>
          <a target="_blank" rel="noreferrer noopener" href="{{.URL}}">
            <h3 class="title">{{.Title }}</h3>
          </a>
          <p class="description">{{ .Description }}</p>
          <div class="metadata">
            <p class="source">{{ .Source.Name }}</p>
            <time class="published-date">{{ .PublishedAt }}</time>
          </div>
        </div>
        <img class="article-image" src="{{ .URLToImage }}">
      </li>
    {{ end }}
  </ul>
</section>
```

Чтобы получить доступ к полю структуры в шаблоне, мы используем оператор точки. Этот оператор ссылается на объект структуры (в данном случае `search`), а затем внутри шаблона мы просто указываем имя поля (как `{{.Results}}`).

Блок `range` позволяет нам перебирать слайс в Go и выводить некоторый HTML для каждого элемента в слайсе. Здесь мы перебираем слайс структур `Article`, содержащихся в поле `Articles`, и выводим HTML на каждой итерации.

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

[![Browser showing news listings](https://res.cloudinary.com/freshman/image/upload/v1566478651/Screenshot_from_2019-08-22_13-56-33.png)](https://res.cloudinary.com/freshman/image/upload/v1566478651/Screenshot_from_2019-08-22_13-56-33.png)

## Сохраняем поисковый запрос в инпуте[](https://freshman.tech/web-development-with-go/#persist-the-search-query-in-the-input)

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

Мы можем легко это исправить, обновив атрибут value тега input в нашем файле index.html следующим образом:

```html
<input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q">
```

Перезапустите браузер и выполните новый поиск. Поисковый запрос будет сохранен, как показано ниже:

[![](https://res.cloudinary.com/freshman/image/upload/v1566479245/Screenshot_from_2019-08-22_14-06-52.png)](https://res.cloudinary.com/freshman/image/upload/v1566479245/Screenshot_from_2019-08-22_14-06-52.png)

## Форматируем дату публикации[](https://freshman.tech/web-development-with-go/#format-the-published-date)

Если вы посмотрите на дату в каждой статье, вы увидите, что она плохо читаема. Текущий вывод показывает, как News API возвращает дату публикации статьи. Но мы можем легко изменить это, добавив метод в структуру `Article` и используя его для форматирования даты вместо использования значения по умолчанию.

Давайте добавим следующий код чуть ниже структуры `Article` в `main.go`:

```go
func (a *Article) FormatPublishedDate() string {
	year, month, day := a.PublishedAt.Date()
	return fmt.Sprintf("%v %d, %d", month, day, year)
}
```

Здесь новый метод `FormatPublishedDate` создан в структуре `Article`, и этот метод форматирует поле `PublishedAt` в `Article` и возвращает строку в следующем формате: `10 января 2009`.

Чтобы использовать этот новый метод в вашем шаблоне, замените `.PublishedAt` на `.FormatPublishedDate` в вашем файле `index.html`. Затем перезагрузите сервер и повторите предыдущий поисковый запрос. Это выведет результаты с правильно отформатированным временем, как показано ниже:

[![Brave browser showing correctly formatted date](https://res.cloudinary.com/freshman/image/upload/v1566479638/Screenshot_from_2019-08-22_14-13-40.png)](https://res.cloudinary.com/freshman/image/upload/v1566479638/Screenshot_from_2019-08-22_14-13-40.png)

## Выводим общее количество результатов[](https://freshman.tech/web-development-with-go/#show-the-total-number-of-results)

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

Все, что вам нужно сделать, это добавить следующий код как дочерний элемент `.container`, чуть выше элемента `.search-results` в вашем файле `index.html`:

```go
<div class="result-count">
  {{ if (gt .Results.TotalResults 0)}}
  <p>About <strong>{{ .Results.TotalResults }}</strong> results were found.</p>
  {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }}
  <p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
  {{ end }}
</div>
```

Шаблоны Go поддерживают несколько функций сравнения, некоторые из которых используются выше. Мы используем функцию `gt`, чтобы проверить, что поле `TotalResults` структуры `Results` больше нуля. Если это так, общее количество результатов будет напечатано в верхней части страницы.

В противном случае, если `SearchKey` не равен пустой строке (`(ne .SearchKey "")`) и `TotalResults` равно нулю (`(eq .Results.TotalResults 0)`), то выводится сообщение «No results found».

Перезапустите сервер и введите несколько слов в поле поиска, чтобы не было найдено новостей по вашему запросу. На экране должно появиться сообщение «No results found».

[![Browser showing no results found message](https://res.cloudinary.com/freshman/image/upload/v1566480557/Screenshot_from_2019-08-22_14-29-02.png)](https://res.cloudinary.com/freshman/image/upload/v1566480557/Screenshot_from_2019-08-22_14-29-02.png)

После этого сделайте еще один поисковый запрос на этот раз с популярной темой. Количество результатов будет выведено вверху страницы, как показано ниже:

[![Browser showing results count at the top of the page](https://res.cloudinary.com/freshman/image/upload/v1566480828/Screenshot_from_2019-08-22_14-33-35.png)](https://res.cloudinary.com/freshman/image/upload/v1566480828/Screenshot_from_2019-08-22_14-33-35.png)

## Пагинация[](https://freshman.tech/web-development-with-go/#pagination)

Так как мы отображаем только 20 результатов одновременно, нам нужен способ, чтобы пользователь мог перейти на следующую или предыдущую страницу результатов в любое время.

Сначала добавим кнопку ** Next ** внизу результатов, если последняя страница результатов еще не достигнута. Чтобы определить, была ли достигнута последняя страница результатов, создайте этот новый метод ниже объявления структуры `Search` в` main.go`:

```go
func (s *Search) IsLastPage() bool {
	return s.NextPage >= s.TotalPages
}
```

Этот метод проверяет, больше ли поле `NextPage`, чем поле `TotalPages` в экземпляре `Search`. Чтобы это работало, нам нужно увеличивать `NextPage` каждый раз, когда отображается новая страница результатов. Вот как это сделать:

```go
func searchHandler(w http.ResponseWriter, r *http.Request) {
	// начало функции
	search.TotalPages = int(math.Ceil(float64(search.Results.TotalResults / pageSize)))
	// добавьте этот if блок
	if ok := !search.IsLastPage(); ok {
		search.NextPage++
	}

	// остальная часть функции
}
```

Наконец, давайте добавим кнопку, которая позволит пользователю перейти на следующую страницу результатов. Этот код должен быть помещен ниже `.search-results` в вашем файле `index.html`.

```html
<div class="pagination">
  {{ if (ne .IsLastPage true) }}
    <a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a>
  {{ end }}
</div>
```

Пока последняя страница для этого запроса не была достигнута, кнопка **Next** будет отображаться в нижней части списка результатов.

Как видите, `href` ссылки указывает на маршрут `/search` и сохраняет текущий поисковый запрос в параметре `q`, используя значение `NextPage` в параметре `page`.

Давайте добавим кнопку **Previous**. Эту кнопку следует отображать только в том случае, если текущая страница больше 1. Чтобы сделать это, нам нужно создать новый метод `CurrentPage()` в `Search`, чтобы реализовать это. Добавьте это ниже метода `IsLastPage`:

```go
func (s *Search) CurrentPage() int {
	if s.NextPage == 1 {
		return s.NextPage
	}

	return s.NextPage - 1
}
```

Текущая страница просто `NextPage - 1`, за исключением случаев, когда `NextPage` равен 1. Чтобы получить предыдущую страницу, просто вычтите 1 из текущей страницы. Следующий метод делает именно это:

```go
func (s *Search) PreviousPage() int {
	return s.CurrentPage() - 1
}
```
Таким образом, мы можем добавить следующий код для отображения кнопки **Previous**, только если текущая страница больше 1. Измените элемент `.pagination` в вашем файле `index.html` следующим образом:

```html
<div class="pagination">
  {{ if (gt .NextPage 2) }}
    <a href="/search?q={{ .SearchKey }}&page={{ .PreviousPage }}" class="button previous-page">Previous</a>
  {{ end }}
  {{ if (ne .IsLastPage true) }}
    <a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a>
  {{ end }}
</div>
```

Теперь перезагрузите сервер и сделайте новый поисковый запрос. У вас должно получиться пролистать результаты, как показано ниже:

<figure class="video_container">
  <iframe src="https://res.cloudinary.com/freshman/video/upload/v1566482207/Peek_2019-08-22_14-56.webm" frameborder="0" allowfullscreen="true"> </iframe>
</figure>

## Показываем текущую страницу[](https://freshman.tech/web-development-with-go/#show-the-current-page)

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

Для этого нам нужно всего лишь изменить наш файл `index.html` следующим образом:

```html
<div class="result-count">
  {{ if (gt .Results.TotalResults 0)}}
    <p>About <strong>{{ .Results.TotalResults }}</strong> results were found. You are on page <strong>{{ .CurrentPage }}</strong> of <strong> {{ .TotalPages }}</strong>.</p>
  {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }}
    <p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p>
  {{ end }}
</div>
```

После того, как вы перезапустите сервер и выполните новый поиск, в верхней части страницы будет указана текущая страница и общее количество страниц вместе с общим количеством результатов.

[![Browser showing current page](https://res.cloudinary.com/freshman/image/upload/v1566482694/Screenshot_from_2019-08-22_15-04-27.png)](https://res.cloudinary.com/freshman/image/upload/v1566482694/Screenshot_from_2019-08-22_15-04-27.png)

## Деплоим на Heroku[](https://freshman.tech/web-development-with-go/#deploy-to-heroku)

Теперь, когда наше приложение полнофункционально, давайте продолжим и развернем его в Heroku. [Зарегистрируйте бесплатную учетную запись](https://signup.heroku.com/), затем перейдите по этой ссылке, чтобы создать новое приложение. Укажите для приложения уникальное имя. Я назвал приложение **freshman-news**.

Затем [следуйте инструкциям здесь](https://devcenter.heroku.com/articles/heroku-command-line), чтобы установить интерфейс командной строки Heroku на свой компьютер. Затем выполните команду `heroku login` в терминале, чтобы войти в свою учетную запись Heroku.

Убедитесь, что вы инициализировали git-репозиторий для своего проекта. Если нет, запустите команду `git init` в корне каталога вашего проекта, а затем выполните команду ниже, чтобы установить heroku в качестве удаленного git-репозитория. Замените `freshman-news` названием вашего приложения.

```bash
heroku git:remote -a freshman-news
```

Затем создайте Procfile в корневом каталоге вашего проекта (`touch Procfile`) и вставьте следующее содержимое:

```bash
web: bin/news-demo -apikey $NEWS_API_KEY
```

После этого укажите репозиторий GitHub для своего проекта и версию Go, которую вы используете, в своем файле `go.mod`, как показано ниже. Создайте этот файл, если он еще не существует, в корне проекта.

```go
module github.com/freshman-tech/news-demo

go 1.12.9
```

Перед развертыванием приложения перейдите на вкладку **Settings** на панели инструментов Heroku и нажмите **Reveal Config Vars**. Нам нужно установить переменную среды **NEWS_API_KEY**, чтобы она могла быть передана в бинарный файл при запуске сервера.

[![Heroku config variables](https://res.cloudinary.com/freshman/image/upload/v1566487319/2019-08-22_16-21.png)](https://res.cloudinary.com/freshman/image/upload/v1566487319/2019-08-22_16-21.png)

Наконец, сделайте коммит своего кода и сделайте пуш в Heroku с помощью следующих команд:

```bash
git add .
git commit -m "Initial commit"
git push heroku master
```

После завершения процесса деплоя вы можете открыть https://название_вашего_приложения.herokuapp.com, чтобы просмотреть и протестировать свой проект.

## Заключение[](https://freshman.tech/web-development-with-go/#wrap-up)

В этой статье мы успешно создали приложение News и обучились основам использования Go для веб-разработки. Мы также изучили, как развернуть готовое приложение в Heroku. 

Я надеюсь, что эта статья была полезна для вас. Если у вас есть какие-либо вопросы относительно этого туториала, оставьте комментарий ниже, и я перезвоню вам. 

Спасибо за чтение!