Привет, Хабр! Представляю вашему вниманию перевод статьи "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)
}
Первая строка 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
Если вы перейдете на http: // localhost: 3000 в своем браузере, вы должны увидеть текст «Hello World!».
Шаблоны в 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)
}
tpl
— переменная уровня пакета, которая указывает на определение шаблона из предоставленных файлов. Вызов template.ParseFiles
анализирует файлindex.html
в корне каталога нашего проекта и проверяет его на валидность.
Мы оборачиваем вызов template.ParseFiles
в template.Must
, чтобы код вызывал панику при возникновении ошибки. Причина, по которой мы паникуем здесь вместо того, чтобы пытаться обработать ошибку, заключается в том, что нет смысла продолжать выполнение кода, если у нас невалидный шаблон. Это проблема, которая должна быть устранена перед попыткой перезапустить сервер.
В функции indexHandler
мы выполняем созданный ранее шаблон, предоставляя два аргумента: куда мы хотим записать выходные данные и данные, которые мы хотим передать в шаблон.
В приведенном выше случае мы записываем выходные данные в интерфейс ResponseWriter
и, поскольку у нас нет никаких данных для передачи в наш шаблон в настоящее время, в качестве второго аргумента передается nil
.
Остановите запущенный процесс в вашем терминале с помощью Ctrl-C и запустите его снова с помощью go run main.go
, затем обновите ваш браузер. Вы должны увидеть текст «News App Demo» на странице, как показано ниже:
Добавляем панель навигации на страницу
Замените содержимое тега <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>
Затем перезагрузите сервер и обновите ваш браузер. Вы должны увидеть что-то похожее на это:
Работа со статическими файлами
Обратите внимание, что панель навигации, которую мы добавили выше, не имеет стилей, несмотря на тот факт, что мы уже указали их в <head>
нашего документа.
Это потому, что путь /
фактически совпадает со всеми путями, которые не обрабатываются в другом месте. Поэтому, если вы перейдете на http://localhost:3000/assets/style.css, вы все равно получите домашнюю страницу News Demo вместо файла CSS, потому что маршрут /assets/style.css
не был объявлен специально.
Но необходимость объявлять явные обработчики для всех наших статических файлов нереальна и не может масштабироваться. К счастью, мы можем создать один обработчик для обслуживания всех статических ресурсов.
Первое, что нужно сделать, — создать экземпляр объекта файлового сервера, передав каталог, в котором находятся все наши статические файлы:
fs := http.FileServer(http.Dir("assets"))
Далее нам нужно указать нашему маршрутизатору использовать этот объект файлового сервера для всех путей, начинающихся с префикса /assets/
:
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
Теперь всё вместе:
// 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)
}
Перезагрузите сервер и обновите браузер. Стили должны включиться, как показано ниже:
Создаем роут /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)
}
Приведенный выше код извлекает параметры 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)
}
Не забудьте импортировать пакеты fmt
иnet/url
сверху:
import (
"fmt"
"html/template"
"net/http"
"net/url"
"os"
)
Теперь перезапустите сервер, введите запрос в поле поиска и проверьте терминал. Вы должны увидеть ваш запрос в терминале, как показано ниже:
Создаём модель данных
Когда мы делаем запрос к конечной точке 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..."
}
]
}
Чтобы работать с этими данными в 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"`
}
Я сделал несколько изменений в структуре 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"`
}
Как вы, возможно, знаете, Go требует, чтобы все экспортируемые поля в структуре начинались с заглавной буквы. Однако принято представлять поля JSON с помощью camelCase или snake_case, которые не начинаются с заглавной буквы.
Поэтому мы используем теги поля структуры, такие как json:"id"
, чтобы явно отобразить поле структуры в поле JSON, как показано выше. Это также позволяет использовать совершенно разные имена для структурного поля и соответствующего поля json, если это необходимо.
Наконец, давайте создадим другой тип структуры для каждого поискового запроса. Добавьте это ниже структуры Results
в main.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
:
var apiKey *string
Затем используйте её в функции main
следующим образом:
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
, как показано ниже:
go run main.go -apikey=<your newsapi access key>
Далее, давайте продолжим и обновим 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)
}
}
Сначала мы создаем новый экземпляр структуры 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"
)
Давайте продолжим и отобразим результаты на странице, изменив файл 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>
Чтобы получить доступ к полю структуры в шаблоне, мы используем оператор точки. Этот оператор ссылается на объект структуры (в данном случае search
), а затем внутри шаблона мы просто указываем имя поля (как {{.Results}}
).
Блок range
позволяет нам перебирать слайс в Go и выводить некоторый HTML для каждого элемента в слайсе. Здесь мы перебираем слайс структур Article
, содержащихся в поле Articles
, и выводим HTML на каждой итерации.
Перезагрузите сервер, обновите браузер и выполните поиск новостей по популярной теме. Вы должны получить список из 20 результатов на странице, как показано на скрине ниже.
Сохраняем поисковый запрос в инпуте
Обратите внимание, что поисковый запрос исчезает из ввода, когда страница обновляется с результатами. В идеале запрос должен сохраняться до тех пор, пока пользователь не выполнит новый поиск. Вот как Google Search работает, например.
Мы можем легко это исправить, обновив атрибут value
тега input
в нашем файле index.html
следующим образом:
<input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q">
Перезапустите браузер и выполните новый поиск. Поисковый запрос будет сохранен, как показано ниже:
Форматируем дату публикации
Если вы посмотрите на дату в каждой статье, вы увидите, что она плохо читаема. Текущий вывод показывает, как 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)
}
Здесь новый метод FormatPublishedDate
создан в структуре Article
, и этот метод форматирует поле PublishedAt
в Article
и возвращает строку в следующем формате: 10 января 2009
.
Чтобы использовать этот новый метод в вашем шаблоне, замените .PublishedAt
на .FormatPublishedDate
в вашем файле index.html
. Затем перезагрузите сервер и повторите предыдущий поисковый запрос. Это выведет результаты с правильно отформатированным временем, как показано ниже:
Выводим общее количество результатов
Давайте улучшим пользовательский интерфейс нашего новостного приложения, указав общее количество результатов в верхней части страницы, а затем отобразим сообщение на случай, если по определенному запросу не найдено ни одного результата.
Все, что вам нужно сделать, это добавить следующий код как дочерний элемент .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>
Шаблоны Go поддерживают несколько функций сравнения, некоторые из которых используются выше. Мы используем функцию gt
, чтобы проверить, что поле TotalResults
структуры Results
больше нуля. Если это так, общее количество результатов будет напечатано в верхней части страницы.
В противном случае, если SearchKey
не равен пустой строке ((ne .SearchKey "")
) и TotalResults
равно нулю ((eq .Results.TotalResults 0)
), то выводится сообщение «No results found».
Перезапустите сервер и введите несколько слов в поле поиска, чтобы не было найдено новостей по вашему запросу. На экране должно появиться сообщение «No results found».
После этого сделайте еще один поисковый запрос на этот раз с популярной темой. Количество результатов будет выведено вверху страницы, как показано ниже:
Пагинация
Так как мы отображаем только 20 результатов одновременно, нам нужен способ, чтобы пользователь мог перейти на следующую или предыдущую страницу результатов в любое время.
Сначала добавим кнопку Next внизу результатов, если последняя страница результатов еще не достигнута. Чтобы определить, была ли достигнута последняя страница результатов, создайте этот новый метод ниже объявления структуры Search
вmain.go
:
func (s *Search) IsLastPage() bool {
return s.NextPage >= s.TotalPages
}
Этот метод проверяет, больше ли поле 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++
}
// остальная часть функции
}
Наконец, давайте добавим кнопку, которая позволит пользователю перейти на следующую страницу результатов. Этот код должен быть помещен ниже .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>
Пока последняя страница для этого запроса не была достигнута, кнопка 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
}
Текущая страница просто NextPage - 1
, за исключением случаев, когда NextPage
равен 1. Чтобы получить предыдущую страницу, просто вычтите 1 из текущей страницы. Следующий метод делает именно это:
func (s *Search) PreviousPage() int {
return s.CurrentPage() - 1
}
Таким образом, мы можем добавить следующий код для отображения кнопки 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>
Теперь перезагрузите сервер и сделайте новый поисковый запрос. У вас должно получиться пролистать результаты, как показано ниже:
Показываем текущую страницу
Вместо того, чтобы отображать только общее количество результатов, найденных для запроса, пользователю также полезно просмотреть общее количество страниц для этого запроса и страницу, на которой он в данный момент находится.
Для этого нам нужно всего лишь изменить наш файл 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>
После того, как вы перезапустите сервер и выполните новый поиск, в верхней части страницы будет указана текущая страница и общее количество страниц вместе с общим количеством результатов.
Деплоим на Heroku
Теперь, когда наше приложение полнофункционально, давайте продолжим и развернем его в Heroku. Зарегистрируйте бесплатную учетную запись, затем перейдите по этой ссылке, чтобы создать новое приложение. Укажите для приложения уникальное имя. Я назвал приложение freshman-news.
Затем следуйте инструкциям здесь, чтобы установить интерфейс командной строки Heroku на свой компьютер. Затем выполните команду heroku login
в терминале, чтобы войти в свою учетную запись Heroku.
Убедитесь, что вы инициализировали git-репозиторий для своего проекта. Если нет, запустите команду git init
в корне каталога вашего проекта, а затем выполните команду ниже, чтобы установить heroku в качестве удаленного git-репозитория. Замените freshman-news
названием вашего приложения.
heroku git:remote -a freshman-news
Затем создайте Procfile в корневом каталоге вашего проекта (touch Procfile
) и вставьте следующее содержимое:
web: bin/news-demo -apikey $NEWS_API_KEY
После этого укажите репозиторий GitHub для своего проекта и версию Go, которую вы используете, в своем файле go.mod
, как показано ниже. Создайте этот файл, если он еще не существует, в корне проекта.
module github.com/freshman-tech/news-demo
go 1.12.9
Перед развертыванием приложения перейдите на вкладку Settings на панели инструментов Heroku и нажмите Reveal Config Vars. Нам нужно установить переменную среды NEWS_API_KEY, чтобы она могла быть передана в бинарный файл при запуске сервера.
Наконец, сделайте коммит своего кода и сделайте пуш в Heroku с помощью следующих команд:
git add .
git commit -m "Initial commit"
git push heroku master
После завершения процесса деплоя вы можете открыть https://название_вашего_приложения.herokuapp.com, чтобы просмотреть и протестировать свой проект.
Заключение
В этой статье мы успешно создали приложение News и обучились основам использования Go для веб-разработки. Мы также изучили, как развернуть готовое приложение в Heroku.
Я надеюсь, что эта статья была полезна для вас. Если у вас есть какие-либо вопросы относительно этого туториала, оставьте комментарий ниже, и я перезвоню вам.
Спасибо за чтение!
Комментарии (13)
pawlo16
11.11.2019 20:15От jsp и asp/asp.net принципиально отличается тем, что не динамический html, а генератор кода на Go. Нет движка сервлетов с магическими диррективами. На quicktemplate вы пишете обычные Go функции и методы, создающие строки текста по определённым правилам.
Не соглашусь с Вами. Для xslt не требуется какой-то специальной поддержки. Можно использовать стандартные шаблоны Go, производящие xslt с динамическим связыванием. Можно создавать XSLT с помощью quicktemplate.tuxi
11.11.2019 23:00Это наверное был ответ на мое сообщение.
Я говорил про XSL трансформер. Не надо «генерить» шаблоны, их пишет фронтендщик (или при миграции проекта переносит со старого).
Трансформеру дают на вход xml и готовый xsl шаблон, а на выходе получают то, что описано в шаблоне (xhtml, xml, да хоть pdf). С минимальными танцами. Вот это было бы супер.pawlo16
11.11.2019 23:17Да, я по ошибке написал не туда.
Не могли бы вы показать пример искомой либы на другом языке? Если фронтендщик всё разверстал и нужно только правильно приготовить XML, то вроде проблем с Go не должно быть (если я правильно понял о чём речь)tuxi
11.11.2019 23:44Ну есть два титана: Xalan и Saxon, но есть и некоторое кол-во других реализаций, скажем так, менее популярных по ряду причин, но по моему опыту, экстремально производительных. Некоторые из них умеют компилировать шаблоны в код (в java классы например). Вот нужно что то такое же. И чтобы не прыгать с бубном, как например в случае с ratago, устанавливая для него libxml и тп.
kalyan_nishchebrod
11.11.2019 21:10Синтаксис вроде терпим, хоть и без женириков, но онанистское апи (bytes to buffer по 10 раз туда сюда например) чистая боль.
ukt
11.11.2019 21:52Если у вас есть какие-либо вопросы относительно этого туториала, оставьте комментарий ниже, и я перезвоню вам.
В го есть указатели?
Как сложную структуру с разнородным содержимым(байты инты и пр) разыменовывать по указателю?
Допустим то же самое, но на голанге:
Заголовок спойлераtypedef struct{ int a; int b; }mux_struct; #include "stdio.h" int main(int argc, const char * argv[]) { char buf[sizeof(int)*2]; buf[0] = 0x12; buf[1] = 0x34; buf[2] = 0x56; buf[3] = 0x78; buf[4] = 0xab; buf[6] = 0xcd; buf[7] = 0xef; buf[8] = 0xff; mux_struct *mx = (mux_struct*)(buf); printf("0x%x\n", mx->a); printf("0x%x\n", mx->b); }
GLeBaTi
12.11.2019 10:42Без unsafe можно сделать так:
mx:= mux_struct{} err := binary.Read(buf, binary.BigEndian, &mx)
MR27
12.11.2019 15:21Простите, но в 2019 году?
Таких примеров на Хабре было лет 5 назад полным-полно. Да, это было логично тогда.
Но сейчас — хотелось бы более хитрые ньюансики в подобной статье.
BassDimas
12.11.2019 15:21+1Отлично! Для новичков самое оно, попробовать и разбираться с Go дальше, спасибо!
pawlo16
omg шаблоны в Go — это такой дичайший отстой, что их даже врагам народа да террористам на дальних подступах не гуманно впаривать. Не дебажатся, не декомпозируются, не типизируются, вообще ни как не проверяются. Использовать их в серьёзном продакшене — чистое безумие. Используйте github.com/valyala/quicktemplate будет вам счастье
В остальном материал статьи — очередное бессмысленное перетирание из пустого в порожнее о том, как написать хелуворлд на Го, информационной контент которого укладывается в пол экрана оф. доков по пакету net/http
tuxi
Если говорить про веб, то на мой взгляд, Go не хватает своей приличной реализации XSLT. Если бы она была бы (без использования exec), то многие небольшие вещи для веба можно было бы делать проще, читабельнее и быстрее.
GoodGod
Не слушайте его, я например наоборот много полезного узнал из статьи. В свое время прочитал книжку по Go, но хотелось увидеть какое-то живое (но в то же время простое) приложение, чтобы подтвердить себе что «вот именно так пишутся приложения на Go, именно такие пакеты сейчас используются, именно такие походы сейчас более-менее правильные и т.д.». Это как раз то что нужно, простое реальное приложение на Go, не особо замудреное. Спасибо за статью.
Ad_augusta_per_angusta Автор
Я увидел эту статью в личном бложике программиста из Нигерии, не думаю, что она так сразу попадается новичкам, которые хотят изучать Go, а между тем, материал в ней изложен понятно и последовательно. Видно, что автор искренне хочет донести эти простые вещи простыми словами. И у него это получается, почему бы и не запилить перевод статьи для удобства восприятия?