Como criar seu primeiro aplicativo Web usando o Go

Olá Habr! Apresento a você a tradução do artigo "Como criar seu primeiro aplicativo da Web com o Go", de Ayooluwa Isaiah.


Este é o guia para seu primeiro aplicativo da web Go. Criaremos um aplicativo de notícias que usa a API de notícias para receber artigos sobre um tópico específico e implantá-lo no servidor de produção no final.


Você pode encontrar o código completo usado para este tutorial neste repositório GitHub .


Exigências


O único requisito para esta tarefa é que o Go esteja instalado no seu computador e você esteja um pouco familiarizado com sua sintaxe e construções. A versão Go que usei para criar o aplicativo também é a mais recente no momento da redação deste documento: 1.12.9 . Para visualizar a versão instalada do Go, use o comando go version .


Se você acha esta tarefa muito difícil para você, vá para a minha lição introdutória anterior, que deve ajudá-lo a começar.


Então, vamos começar!


Nós clonamos o repositório de arquivos inicial no GitHub e cd no diretório criado. Temos três arquivos principais: No arquivo main.go escreveremos todo o código Go para esta tarefa. O arquivo index.html é o modelo que será enviado ao navegador e os do aplicativo estão em assets/styles.css .


Crie um servidor web básico


Vamos começar criando um servidor núcleo que envia o texto "Hello World!" Para o navegador ao executar uma solicitação GET para a raiz do servidor. Altere seu arquivo main.go para ficar assim:


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

A primeira linha do package main - declara que o código no arquivo main.go ao pacote principal. Depois disso, importamos o pacote net/http , que fornece implementações de cliente e servidor HTTP para uso em nosso aplicativo. Este pacote faz parte da biblioteca padrão e está incluído em todas as instalações do Go.


Na função main , http.NewServeMux() cria um novo multiplexador de solicitação HTTP e o atribui à variável mux . Essencialmente, o multiplexador de solicitação corresponde à URL das solicitações recebidas na lista de caminhos registrados e chama o manipulador apropriado para o caminho sempre que uma correspondência é encontrada.


Em seguida, registramos nossa primeira função de manipulador para o caminho raiz / . Essa função de manipulador é o segundo argumento para HandleFunc e sempre possui a func (w http.ResponseWriter, r * http.Request) assinatura func (w http.ResponseWriter, r * http.Request) .


Se você olhar para a função indexHandler , verá que ela possui exatamente essa assinatura, o que a torna um segundo argumento válido para HandleFunc . O parâmetro w é a estrutura que usamos para enviar respostas à solicitação HTTP. Ele implementa o método Write() , que pega uma fatia de bytes e grava os dados combinados como parte da resposta HTTP.


Por outro lado, o parâmetro r representa a solicitação HTTP recebida do cliente. É assim que acessamos os dados enviados pelo navegador da web no servidor. Ainda não o estamos usando aqui, mas definitivamente o usaremos mais tarde.


Por fim, temos o método http.ListenAndServe() , que inicia o servidor na porta 3000 se a porta não estiver configurada pelo ambiente. Sinta-se livre para usar uma porta diferente se 3000 for usado no seu computador.


Em seguida, compile e execute o código que você acabou de escrever:


 go run main.go 

Se você acessar http: // localhost: 3000 no seu navegador, deverá ver o texto "Hello World!".


Navegador corajoso mostrando o texto Hello World


Ir modelos


Vejamos os conceitos básicos de modelos no Go. Se você estiver familiarizado com modelos em outros idiomas, isso deve ser fácil de entender.


Os modelos fornecem uma maneira fácil de personalizar a saída do seu aplicativo da Web, dependendo da rota, sem a necessidade de escrever o mesmo código em locais diferentes. Por exemplo, podemos criar um modelo para a barra de navegação e usá-lo em todas as páginas do site sem duplicar o código. Além disso, também temos a oportunidade de adicionar alguma lógica básica às nossas páginas da web.


O Go fornece duas bibliotecas de modelos em sua biblioteca padrão: text/template e html/template . Ambos fornecem a mesma interface, no entanto, o pacote html/template é usado para gerar saída HTML protegida contra a injeção de código, portanto a usaremos aqui.


Importe este pacote para o seu arquivo main.go e use-o da seguinte maneira:


 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 é uma variável no nível do pacote que indica a definição de um modelo a partir dos arquivos fornecidos. A chamada template.ParseFiles analisa o arquivo index.html na raiz do diretório de nosso projeto e verifica sua validade.


Envolvemos a chamada template.ParseFiles no template.Must para que o código cause pânico quando ocorrer um erro. A razão pela qual entramos em pânico aqui, em vez de tentar lidar com o erro, é porque não faz sentido continuar executando o código se tivermos um modelo inválido. Esse é um problema que precisa ser corrigido antes de tentar reiniciar o servidor.


Na função indexHandler executamos o modelo criado anteriormente fornecendo dois argumentos: onde queremos gravar a saída e os dados que queremos passar para o modelo.


No caso acima, gravamos a saída na interface ResponseWriter e, como não temos dados para passar ao nosso modelo no momento, nil é passado como o segundo argumento.


Pare o processo em execução no seu terminal usando Ctrl-C e inicie-o novamente com go run main.go e atualize seu navegador. Você deverá ver o texto "Demonstração do aplicativo de notícias" na página, como mostrado abaixo:


Navegador corajoso mostrando o texto da demonstração do aplicativo de notícias


Adicione uma barra de navegação à página


Substitua o conteúdo da <body> no seu arquivo index.html, como mostrado abaixo:


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

Em seguida, reinicie o servidor e atualize seu navegador. Você deve ver algo semelhante a este:


Navegador mostrando a barra de navegação sem estilo


Trabalhar com arquivos estáticos


Observe que a barra de navegação que adicionamos acima não possui estilos, apesar de já os termos especificado no <head> nosso documento.


Isso ocorre porque o caminho / realmente corresponde a todos os caminhos que não são processados ​​em outro lugar. Portanto, se você acessar http: // localhost: 3000 / assets / style.css , ainda receberá a página inicial de Demonstração de Notícias em vez do arquivo CSS porque a rota /assets/style.css não foi declarada especificamente.


Mas a necessidade de declarar manipuladores explícitos para todos os nossos arquivos estáticos não é realista e não pode ser dimensionada. Felizmente, podemos criar um manipulador para atender a todos os recursos estáticos.


A primeira coisa a fazer é criar uma instância do objeto do servidor de arquivos, passando no diretório em que todos os nossos arquivos estáticos estão localizados:


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

Em seguida, precisamos dizer ao nosso roteador para usar este objeto de servidor de arquivos para todos os caminhos que começam com o prefixo /assets/ :


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

Agora todos juntos:


 // 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) } 

Reinicie o servidor e atualize o navegador. Os estilos devem ser ativados conforme mostrado abaixo:


Navegador corajoso mostrando a barra de navegação com estilo



Vamos criar uma rota que lide com consultas de pesquisa para artigos de notícias. Usaremos a API de notícias para processar solicitações; portanto, você precisa se registrar para receber uma chave de API gratuita aqui .


Essa rota espera dois parâmetros de consulta: q representa a consulta do usuário e a page usada para rolar pelos resultados. Este parâmetro da page é opcional. Se não estiver incluído no URL, simplesmente assumimos que o número da página dos resultados está definido como "1".


Adicione o seguinte manipulador em indexHandler ao seu arquivo 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) } 

O código acima extrai os parâmetros q e page da URL de solicitação e exibe os dois no terminal.


Em seguida, registre a função searchHandler como manipulador do caminho /search , conforme mostrado abaixo:


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

Lembre-se de importar os pacotes fmt e net/url de cima:


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

Agora reinicie o servidor, insira a consulta no campo de pesquisa e verifique o terminal. Você deve ver sua solicitação no terminal, como mostrado abaixo:




Crie um modelo de dados


Quando fazemos uma solicitação para a News API/everything endpoint, esperamos uma resposta json no seguinte formato:


 { "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..." } ] } 

Para trabalhar com esses dados no Go, precisamos gerar uma estrutura que reflita os dados ao decodificar o corpo da resposta. Claro, você pode fazê-lo manualmente, mas prefiro usar o site JSON-to-Go , o que facilita muito esse processo. Ele gera uma estrutura Go (com tags) que funcionará para este JSON.


Tudo o que você precisa fazer é copiar o objeto JSON e colá-lo no campo marcado com JSON , depois copiar a saída e colá-lo no seu código. Aqui está o que obtemos para o objeto JSON acima:


 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"` } 

Navegador corajoso mostrando a ferramenta JSON to Go


Fiz várias alterações na estrutura AutoGenerated separando o fragmento Articles em sua própria estrutura e atualizando o nome da estrutura. Cole a seguinte declaração de variável tpl em main.go e adicione o pacote de time à sua importação:


 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"` } 

Como você deve saber, o Go exige que todos os campos exportados na estrutura comecem com uma letra maiúscula. No entanto, é comum representar campos JSON usando camelCase ou snake_case , que não começam com uma letra maiúscula.


Portanto, usamos tags de campo de estrutura como json:"id" para exibir explicitamente o campo de estrutura no campo JSON, como mostrado acima. Ele também permite que você use nomes completamente diferentes para o campo de estrutura e o campo json correspondente, se necessário.


Por fim, vamos criar um tipo diferente de estrutura para cada consulta de pesquisa. Adicione isso abaixo da estrutura de Results em main.go :


 type Search struct { SearchKey string NextPage int TotalPages int Results Results } 

Essa estrutura representa cada consulta de pesquisa feita pelo usuário. SearchKey é a própria consulta, o campo NextPage permite rolar pelos resultados, TotalPages - o número total de páginas de resultados da consulta e Results - a página atual dos resultados da consulta.


Envie uma solicitação usando a API de notícias e renderize os resultados


Agora que temos o modelo de dados para o nosso aplicativo, vamos continuar e fazer solicitações à API de notícias e renderizar os resultados na página.


Como a API do News exige uma chave da API, precisamos encontrar uma maneira de transmiti-la em nosso aplicativo sem codificação embutida no código. Variáveis ​​de ambiente são uma abordagem comum, mas decidi usar sinalizadores de linha de comando. O Go fornece um pacote de flag que suporta a análise básica dos sinalizadores de linha de comando, e é isso que vamos usar aqui.


Primeiro, declare uma nova variável apiKey sob a variável tpl :


 var apiKey *string 

Em seguida, use-o na função main seguinte maneira:


 func main() { apiKey = flag.String("apikey", "", "Newsapi.org access key") flag.Parse() if *apiKey == "" { log.Fatal("apiKey must be set") } //    } 

Aqui chamamos o método flag.String() , que nos permite definir um sinalizador de string. O primeiro argumento para esse método é o nome do sinalizador, o segundo é o valor padrão e o terceiro é a descrição de uso.


Depois de definir todos os sinalizadores, você precisa chamar flag.Parse() para flag.Parse() los. Por fim, como o apikey é um componente necessário para este aplicativo, garantimos que o programa falhe se esse sinalizador não for definido durante a execução do programa.


Certifique-se de adicionar o pacote de flag à sua importação, reinicie o servidor e passe o sinalizador apikey necessário, conforme mostrado abaixo:


 go run main.go -apikey=<your newsapi access key> 

Em seguida, vamos continuar e atualizar o searchHandler para que a consulta de pesquisa do usuário seja enviada para newsapi.org e os resultados sejam exibidos em nosso modelo.


Substitua as duas chamadas para o método fmt.Println() no final da função searchHandler seguinte código:


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

Primeiro, criamos uma nova instância da estrutura de Search e configuramos o valor do campo SearchKey para o valor do parâmetro de URL q na solicitação HTTP.


Depois disso, convertemos a variável da page em um número inteiro e atribuímos o resultado ao campo NextPage variável de search . Em seguida, criamos a variável pageSize e configuramos seu valor como 20. Essa variável pageSize representa o número de resultados que a API de notícias retornará em sua resposta. Este valor pode variar de 0 a 100.


Em seguida, criamos o terminal usando fmt.Sprintf() e fazemos uma solicitação GET. Se a resposta da API de notícias não for 200 OK , retornaremos um erro geral do servidor ao cliente. Caso contrário, o corpo da resposta será analisado em search.Results .


Em seguida, calculamos o número total de páginas dividindo o campo TotalResults por pageSize . Por exemplo, se uma consulta retornar 100 resultados e exibirmos apenas 20 por vez, precisaremos rolar cinco páginas para ver todos os 100 resultados dessa consulta.


Depois disso, renderizamos nosso modelo e passamos a variável de search como interface de dados. Isso nos permite acessar dados de um objeto JSON em nosso modelo, como você verá.


Antes de passar para index.html , atualize suas importações, como mostrado abaixo:


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

Vamos continuar e exibir os resultados na página alterando o arquivo index.html seguinte maneira. Adicione isso sob a <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> 

Para acessar o campo de estrutura no modelo, usamos o operador de ponto. Este operador se refere a um objeto de estrutura (neste caso, search ) e, dentro do modelo, simplesmente especificamos o nome do campo (como {{.Results}} ).


O bloco range permite iterar sobre uma fatia no Go e gerar algum HTML para cada elemento da fatia. Aqui, iteramos sobre a fatia das estruturas de Articles contidas no campo Articles e exibimos o HTML a cada iteração.


Reinicie o servidor, atualize o navegador e procure notícias sobre um tópico popular. Você deve obter uma lista de 20 resultados por página, conforme mostrado na captura de tela abaixo.


Navegador mostrando listagens de notícias


Salvar consulta de pesquisa no exterior


Observe que a consulta de pesquisa desaparece da entrada quando a página é atualizada com os resultados. Idealmente, a consulta deve ser mantida até que o usuário realize uma nova pesquisa. Veja como a Pesquisa do Google funciona, por exemplo.


Podemos corrigir isso facilmente, atualizando o atributo value da tag de input em nosso arquivo index.html seguinte maneira:


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

Reinicie seu navegador e faça uma nova pesquisa. A consulta de pesquisa será salva como mostrado abaixo:



Formatar data de publicação


Se você olhar para a data de cada artigo, verá que ela é pouco legível. A saída atual mostra como a API News retorna a data de publicação do artigo. Mas podemos mudar isso facilmente, adicionando um método à estrutura do Article e usando-o para formatar a data em vez de usar o valor padrão.


Vamos adicionar o seguinte código logo abaixo da estrutura do Article em main.go :


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

Aqui, um novo método FormatPublishedDate criado na estrutura Article , e esse método formata o campo PublishedAt no Article e retorna uma string no seguinte formato: 10 2009 .


Para usar esse novo método no seu modelo, substitua .PublishedAt por .FormatPublishedDate no seu arquivo index.html . Em seguida, reinicie o servidor e repita a consulta de pesquisa anterior. Isso produzirá os resultados com uma hora formatada corretamente, como mostrado abaixo:


Navegador corajoso mostrando a data formatada corretamente


Exibe o número total de resultados.


Vamos melhorar a interface do usuário do nosso aplicativo de notícias, indicando o número total de resultados na parte superior da página e exibir uma mensagem caso nenhum resultado tenha sido encontrado para uma consulta específica.


Tudo o que você precisa fazer é adicionar o seguinte código como filho do .container , logo acima do elemento .search-results no seu arquivo 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».


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 } 

, 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> 

, , .


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 

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 config variables


, Heroku :


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

https://__.herokuapp.com , .


Conclusão


News Go -. , Heroku.


, . - , , .


Obrigado pela leitura!

Source: https://habr.com/ru/post/pt475390/


All Articles