DEV Community

Petrovichev Sergey
Petrovichev Sergey

Posted on • Edited on

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

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

Эта статья является переводом статьи "How to build your first web application with Go" автора Ayooluwa Isaiah, ссылка на оригинал.

Это руководство к вашему первому веб-приложению на Go. Мы создадим новостное приложение, которое использует News API для получения новостных статей по определенной теме, и развернём его на продакшн сервере в конце.

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

Требования

Единственное требование для этого задания - чтобы на вашем компьютере был установлен Go, и вы немного знакомы с его синтаксисом и конструкциями. Версия Go, которую я использовал при создании приложения, также является самой последней на момент написания: 1.12.9. Чтобы просмотреть установленную версию Go, используйте команду go version.

Если вы считаете это задание слишком сложным для вас, перейдите к моему предыдущему вводному уроку по языку, который должен помочь вам освоиться.

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

Клонируем репозиторий стартовых файлов на GitHub и cd в созданный каталог. У нас есть три основных файла: В файле main.go мы напишем весь код Go для этого задания. Файл index.html - это шаблон, который будет отправлен в браузер, а стили для приложения находятся в assets/styles.css.

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

Давайте начнем с создания базового сервера, который отправляет текст «Hello World!» в браузер при выполнении запроса GET к корню сервера. Измените ваш файл main.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)
}
Enter fullscreen mode Exit fullscreen mode

Первая строка 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 используется на вашем компьютере.

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

go run main.go
Enter fullscreen mode Exit fullscreen mode

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

Brave browser showing Hello World text

Шаблоны в Go

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

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

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

Импортируйте этот пакет в ваш файл main.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)
}
Enter fullscreen mode Exit fullscreen mode

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

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

Замените содержимое тега <body> в вашем файле index.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>
Enter fullscreen mode Exit fullscreen mode

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

Browser showing unstyled navigation bar

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

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

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

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

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

fs := http.FileServer(http.Dir("assets"))
Enter fullscreen mode Exit fullscreen mode

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

mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
Enter fullscreen mode Exit fullscreen mode

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

// 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)
}
Enter fullscreen mode Exit fullscreen mode

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

Brave browser showing styled navigation bar

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

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

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

Добавьте следующий обработчик под indexHandler в ваш файлmain.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)
}
Enter fullscreen mode Exit fullscreen mode

Приведенный выше код извлекает параметры q и page из URL-адреса запроса и выводит их оба в терминал.

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

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)
}
Enter fullscreen mode Exit fullscreen mode

Не забудьте импортировать пакеты fmt иnet/url сверху:

import (
    "fmt"
    "html/template"
    "net/http"
    "net/url"
    "os"
)
Enter fullscreen mode Exit fullscreen mode

Теперь перезапустите сервер, введите запрос в поле поиска и проверьте терминал. Вы должны увидеть ваш запрос в терминале, как показано ниже:

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

Когда мы делаем запрос к конечной точке News API/everything, мы ожидаем ответ 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..."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Чтобы работать с этими данными в Go, нам нужно сгенерировать структуру, которая отражает данные при декодировании тела ответа. Конечно, вы можете сделать это вручную, но я предпочитаю использовать веб-сайт JSON-to-Go, который делает этот процесс действительно простым. Он генерирует структуру Go (с тегами), которая будет работать для этого JSON.

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

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"`
}
Enter fullscreen mode Exit fullscreen mode

Brave browser showing JSON to Go tool

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

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"`
}
Enter fullscreen mode Exit fullscreen mode

Как вы, возможно, знаете, Go требует, чтобы все экспортируемые поля в структуре начинались с заглавной буквы. Однако принято представлять поля JSON с помощью camelCase или snake_case, которые не начинаются с заглавной буквы.

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

Наконец, давайте создадим другой тип структуры для каждого поискового запроса. Добавьте это ниже структуры Results в main.go:

type Search struct {
    SearchKey  string
    NextPage   int
    TotalPages int
    Results    Results
}
Enter fullscreen mode Exit fullscreen mode

Эта структура представляет собой каждый поисковый запрос, сделанный пользователем. SearchKey - это сам запрос, поле NextPage позволяет пролистывать результаты, TotalPages - общее количество страниц результатов запроса, а Results - текущая страница результатов запроса.

Отправляем запрос по News API и рендерим результаты

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

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

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

var apiKey *string
Enter fullscreen mode Exit fullscreen mode

Затем используйте её в функции main следующим образом:

func main() {
    apiKey = flag.String("apikey", "", "Newsapi.org access key")
    flag.Parse()

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

    // остальная часть функции
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

go run main.go -apikey=<your newsapi access key>
Enter fullscreen mode Exit fullscreen mode

Далее, давайте продолжим и обновим searchHandler, чтобы поисковый запрос пользователя отправлялся на newsapi.org и результаты отображались в нашем шаблоне.

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

Затем мы создаем конечную точку с помощью fmt.Sprintf() и делаем запрос GET к ней. Если ответ от News API не 200 OK, мы вернем клиенту общую ошибку сервера. В противном случае тело ответа парсится в search.Results.

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

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

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

import (
    "encoding/json"
    "flag"
    "fmt"
    "html/template"
    "log"
    "math"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

Browser showing news listings

Сохраняем поисковый запрос в инпуте

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

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

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

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

Форматируем дату публикации

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

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

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

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

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

Brave browser showing correctly formatted date

Выводим общее количество результатов

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

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

Browser showing no results found message

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

Browser showing results count at the top of the page

Пагинация

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

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

func (s *Search) IsLastPage() bool {
    return s.NextPage >= s.TotalPages
}
Enter fullscreen mode Exit fullscreen mode

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

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++
    }

    // остальная часть функции
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

    return s.NextPage - 1
}
Enter fullscreen mode Exit fullscreen mode

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

func (s *Search) PreviousPage() int {
    return s.CurrentPage() - 1
}
Enter fullscreen mode Exit fullscreen mode

Таким образом, мы можем добавить следующий код для отображения кнопки Previous, только если текущая страница больше 1. Измените элемент .pagination в вашем файле index.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>
Enter fullscreen mode Exit fullscreen mode

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

Показываем текущую страницу

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

Для этого нам нужно всего лишь изменить наш файл index.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>
Enter fullscreen mode Exit fullscreen mode

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

Browser showing current page

Деплоим на Heroku

Теперь, когда наше приложение полнофункционально, давайте продолжим и развернем его в Heroku. Зарегистрируйте бесплатную учетную запись, затем перейдите по этой ссылке, чтобы создать новое приложение. Укажите для приложения уникальное имя. Я назвал приложение freshman-news.

Затем следуйте инструкциям здесь, чтобы установить интерфейс командной строки Heroku на свой компьютер. Затем выполните команду heroku login в терминале, чтобы войти в свою учетную запись Heroku.

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

heroku git:remote -a freshman-news
Enter fullscreen mode Exit fullscreen mode

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

web: bin/news-demo -apikey $NEWS_API_KEY
Enter fullscreen mode Exit fullscreen mode

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

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

go 1.12.9
Enter fullscreen mode Exit fullscreen mode

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

Heroku config variables

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

git add .
git commit -m "Initial commit"
git push heroku master
Enter fullscreen mode Exit fullscreen mode

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

Заключение

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

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

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

Top comments (0)