Nota do Tradutor
Essa página é uma tradução livre de Writing Web Applications por Daniel Mazza. Os comentários em colchete e a presente nota são meus. Para facilitar a correlação, o código (exceto, talvez, os comentários) serão mantidos em inglês, bem como algumas palavras no texto (em itálico).
[Você pode usar essa tradução exata ou alteradamente onde quiser, apenas assinale o tradutor.]
Introdução
Cobertura desse codelab:
- Criando uma estrutura de dados com métodos load e save
- Usando a package
http
para construir aplicativos web
- Usando a package
template
para processar templates HTML
- Usando a package
regexp
para validar o input do usuário
- Usando closures
Conhecimento necessário:
- Experiência em programação
- Compreensão básica de tecnologias web (HTTP, HTML)
- Algum conhecimento de linha-de-comando UNIX
Primeiros passos
Atualmente, você precisa de uma máquina Linux, OS X, ou FreeBSD para rodar Go. Se você não tem acesso a uma, você pode configurar uma Linux Virtual Machine (usando VirtualBox ou similar) ou uma Virtual Private Server.
Instale Go (veja as Instruções de Instalação) [ou no meu site, Instalação].
Crie um novo diretório para esse codelab e cd [change directory, mude de diretório] para ele:
$ mkdir ~/gowiki
$ cd ~/gowiki
Crie um arquivo chamado wiki.go
. abra-o em seu editor favorito e adicione as seguintes linhas:
package main
import (
"fmt"
"io/ioutil"
)
Ambas fmt
e ioutil
são built-in packages que usaremos. Posteriormente, quando implementarmos funcionalidade, adicionaremos mais packages nessa declaração import
.
Estruturas de dados [Data Structures]
Comecemos definindo as data structures. Uma wiki consiste de uma série de páginas conectadas, cada uma das quais tem um title [título] e um body [corpo] (o conteúdo da página).
type page struct {
title string
body []byte
}
O tipo []byte
significa um slice de bytes [uma porção/fatia de bytes]. (Veja Effective Go para mais sobre slices.) O elemento body é um []byte
em vez de string
porque é o tipo esperado pelas bibliotecas io
que usaremos, como você verá abaixo.
A struct page
descreve como os dados de page serão armazenados na memória. Mas e quanto à armazenagem persistente? Nós podemos endereçá-la em criando um método save
em page
:
func (p *page) save() os.Error {
filename := p.title + ".txt"
return ioutil.WriteFile(filename, p.body, 0600)
}
Esse registro de método se lê: "Esse é um método chamado save
que toma como recebedor p
, um ponteiro para page
. Não recebe nenhum parâmetro e retorna um valor do tipo os.Error
."
Esse método salvará o body
da page
num arquivo de texto. Por simplicidade, usaremos o title
como nome do arquivo.
O método save
retorna um valor os.Error
porque esse é o tipo de retorno de WriteFile
(uma função padrão que escreve byte slice em um arquivo). O método save
retorna o valor do erro para deixar a aplicação manipulá-lo se algo der errado enquanto se escreve no arquivo. Se tudo der certo, page.save()
retornará nil
(o valor-zero para ponteiros, interfaces e alguns outros tipos).
A constante de inteiro octal 0600
, [o zero ao início define ser octal] passada como terceiro parâmetro para WriteFile, indica que o arquivo deve ser criado com permissão de leitura e escrita para o usuário atual apenas. (Veja a man page open(2)
do Unix para detalhes.)
Nós queremos carregar páginas também:
func loadPage(title string) *page {
filename := title + ".txt"
body, _ := ioutil.ReadFile(filename)
return &page{title: title, body: body}
}
A função loadPage
constrói o nome do arquivo a partir de title
, lê o conteúdo do arquivo para uma nova page
e retorna um ponteiro para essa nova page
.
Funções podem retornar múltiplos valores. A função da biblioteca padrão io.ReadFile
retorna []byte
e os.Error
. Em loadPage
, o erro não começará a ser manipulado agora; o "identificador vazio" representado pelo símbolo sublinhado (_
) é usado para jogar fora o valor do retorno do erro (em essência, assinalando o valor a nada).
Mas o que acontece de ReadFile
encontrar um erro? Por exemplo, o arquivo pode não existir. Não devemos ignorar tais erros. Vamos modificar a função para retornar *page
e os.Error
.
func loadPage(title string) (*page, os.Error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &page{title: title, body: body}, nil
}
Quem chamou a função pode agora checar o segundo parâmetro; se for nil
, então o carregamento da página foi bem-sucedido. Se não, será o os.Error que poderá ser manipulado por quem chamou (veja a documentação da package os para mais detalhes).
Nesse ponto, temos uma estrutura simples de dados e a habilidade para salvar e carregar esses dados no (e do) arquivo. Vamos escrever a função main
[principal] para testar o que fizemos.
func main() {
p1 := &page{title: "TestPage", body: []byte("This is a sample page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.body))
}
Depois de compilar e executar esse código, um arquivo chamado TestPage.txt
deverá ser criado contendo o conteúdo de p1
. O arquivo deverá ser, então, lido para a struct [estrutura] p2
e seu conteúdo de body
ser escrito na tela.
Você pode compilar e rodar o programa assim:
$ 8g wiki.go
$ 8l wiki.8
$ ./8.out
This is a sample page.
(Os comandos 8g
e 8l
são aplicáveis à arquitetura GOARCH=386
. Se você estiver num sistema amd64
, substitua 6's em lugar dos 8's.) [6g
, 6l
e 6.out
]
Clique aqui para ver o código escrito até agora.
Introduzindo a package http (um interlúdio)
Eis um exemplo completo de um simples servidor web.
package main
import (
"fmt"
"http"
)
func handler(c *http.Conn, r *http.Request) {
fmt.Fprintf(c, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
A função main
começa com uma chamada para http.HandleFunc
, que informa ao package http
para processar todas as requisições para o web root [raiz] ("/") com handler
.
Ele então chama http.ListenAndServe
, especificando que deve escutar na porta 8080 em qualquer interface (":8080"
). (Não se preocupe com o segundo parâmetro, nil
, por enquanto.) Esta função irá bloquear até que o programa seja encerrado.
A função de handler
é do tipo http.HandlerFunc
. Toma http.Conn e http.Request como seus argumentos.
Um http.Conn
é a saída do servidor de uma conexão HTTP; em escrevendo nele, enviamos os dados para o cliente HTTP.
Um http.Request
é uma estrutura de dados que representa a solicitação do cliente HTTP. A string r.URL.Path
é o componente do caminho da URL pedida. O seguinte [1:]
significa "criar uma sub-slice do Path
do primeiro caracter até o final". Isto retira o caracter inicial "/" do nome do caminho.
Se você executar este programa e acessar a URL:
http://localhost:8080/monkeys
O programa vai apresentar uma página contendo:
Hi there, I love monkeys!
Usando http
para servir páginas wiki
Para usar a package http
, é preciso que seja importada:
import (
"fmt"
"http"
"io/ioutil"
)
Vamos criar um manipulador para ver a página wiki.
const lenPath = len("/view/")
func viewHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
fmt.Fprintf(c, "<h1>%s</h1><div>%s</div>", p.title, p.body)
}
Primeiro, essa função extrái o título do artigo de r.URL.Path
, o componente de caminho da URL solicitada. A contante global lenPath
é o comprimento do componente inicial "/view/"
do caminho solicitado. O Path é re-fatiado [re-sliced] com [lenPath:]
para deletar os primeiros 6 caracteres da string. isso é porque o caminho vai invariavelmente começar com "/view/"
, o que não é parte do título da página.
A função, então, carrega os dados da página, formata a página com a string HTML simples, e a escreve em c
, o http.Conn
.
Perceba, novamente, o uso de _
para ignorar o valor de retorno de os.Error da função loadPage. isso é feito aqui por simplicidade e é geralmente considerada uma prática ruim. Nos atentaremos a isso posteriormente.
Para usar esse manipulador, nós criamos a função main que inicializa http
usando viewHandler
para manejar qualquer requisição no caminho /view/
.
func main() {
http.HandleFunc("/view/", viewHandler)
http.ListenAndServe(":8080", nil)
}
Clique aqui para ver o código que escrevemos até agora.
Vamos criar algum conteúdo de página (como test.txt), compilar nosso código e tentar servir nossa página wiki:
$ echo "Hello world" > test.txt
$ 8g wiki.go
$ 8l wiki.8
$ ./8.out
Com este servidor web rodando, uma visita a http://localhost:8080/view/test deve mostrar uma página chamada "test" contendo as palavras "Hello world".
Editando páginas
Uma wiki não é uma wiki se você não tiver o poder de editar páginas. Vamos criar dois manipuladores: um chamado editHandler para mostrar um formulário 'edit page' [de edição página] e o outro chamado saveHandler para salvar a página enviada pelo formulário.
Primeiro, vamos adicioná-las a main()
:
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
http.ListenAndServe(":8080", nil)
}
A função editHandle
r carrega a página (ou, se ainda não existe, cria uma estrutura vazia em page
) e mostra-a num formulário HTML.
func editHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
fmt.Fprintf(c, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<textarea name=\"body\">%s</textarea><br>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.title, p.title, p.body)
}
Essa função funcionará bem, mas toda essa codificação-embutida HTML é feia. É claro que existe uma maneira melhor.
A package template
A package template
é parte da biblioteca padrão de Go. Podemos usar template
para mantar o HTML num arquivo separado permitindo que alteremos o layout de nossa página de edição sem modificarmos o código go subjacente.
Primeiro, temos de adicionar template
à lista de importação:
import (
"http"
"io/ioutil"
"os"
"template"
)
Vamos criar um arquivo template contendo um formulário HTML. Abra um novo arquivo chamado edit.html
e adicione as seguintes linhas:
<h1>Editing {title}</h1>
<form action="/save/{title}" method="POST">
<div><textarea name="body" rows="20" cols="80">{body|html}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>
Modifique editHandler
para usar template, em vez de codificação-embutida HTML.
func editHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
t, _ := template.ParseFile("edit.html", nil)
t.Execute(p, c)
}
A função template.ParseFile
lerá o conteúdo de edit.html
e retornará *template.Template
.
O método t.Execute
substitui todas as ocorrências de {title}
e {body}
com os valores de p.title
e p.body
e escreve o HTML resultante em http.Conn
.
Repare que usamos {body|html}
no template abaixo. A parte |html
requer do mecanismo template que passe o valor de body
pelo formatador html
antes de sua saída [outpitting it], o que escapa caracteres HTML (substituindo por exemplo >
com >
). Isso previne que os dados do usuário corrompam a forma do HTML.
Agora que removemos a expressão fmt.Sprintf
, podemos remover o "fmt"
da lista de importação [import
].
Já que trabalhamos com templates, vamos criar um template para nosso viewHandler
chamado view.html
:
<h1>{title}</h1>
<p>[<a href="/edit/{title}">edit</a>]</p>
<div>{body}</div>
Modifique o viewHandler
de acordo:
func viewHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
t, _ := template.ParseFile("view.html", nil)
t.Execute(p, c)
}
Repare que usamos quase o mesmo código no template em ambos os manipuladores. Vamos remover essa duplicação em movendo o código do template para sua própria função:
func viewHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
renderTemplate(c, "edit", p)
}
func editHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(c, "view", p)
}
func renderTemplate(c *http.Conn, tmpl string, p *page) {
t, _ := template.ParseFile(tmpl+".html", nil)
t.Execute(p, c)
}
Os manipuladores são agora menores e mais simples.
Manejando páginas inexistentes
E se você visitar view/UmaPaginaQueNaoExiste
? O programa falhará. Isso porque ignora o valor do erro retornado de loadPage
. Em vez disso, se solicitarmos uma página inexistente, devemos redirecionar o cliente para a página de edição então o conteúdo pode ser criado:
func viewHandler(c *http.Conn, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(c, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(c, "view", p)
}
A função http.Redirect
adiciona um código de status HTTP de http.StatusFound
(302) e a Location
header [cabeçalho de localização] para a resposta HTTP.
Salvando páginas
A função saveHandler
processará os dados de formulário enviados.
func saveHandler(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
p.save()
http.Redirect(c, "/view/"+title, http.StatusFound)
}
O título da página (provido na URL) e no único dado de formulário, body
, são armazenados numa nova página. O método save()
é, então, chamado para escrever os dados em um arquivo, e o cliente é redirecionado para a página /view/
.
O valor retornado por FormValue
é de tipo string
. Temos de converter o valor para []byte
antes de ele caber na page
struct [estrutuda de dados da página]. Usamos []byte(body)
para a conversão.
Tratamento de erro
Em diversos lugares em nosso programa os erros estão sendo ignorados. Essa é uma prática ruim, no mínimo porque quando um erro ocorrer o programa irá falhar. Uma solução melhor é tratar os erros e retornar uma mensagem de erro para o usuário. Assim, se alguma coisa der errado, o servidor irá continuar funcionando e o usuário será notificado.
Primeiro, vamos tratar os erros em renderTemplate
:
func renderTemplate(c *http.Conn, tmpl string, p *page) {
t, err := template.ParseFile(tmpl+".html", nil)
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
return
}
err = t.Execute(p, c)
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
}
}
A função http.Error
envia o código da resposta HTTP especificada (nesse caso, "Internet Server Error") e mensagem de erro. Desde já, a decisão de por isso numa função separada valeu a pena.
Agora, vamos consertar saveHandler
:
func saveHandler(c *http.Conn, r *http.Request, title string) {
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err := p.save()
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(c, "/view/"+title, http.StatusFound)
}
Quaiquer erros que ocorrerem durante p.save()
serão reportados ao usuário.
Template caching
Há uma ineficiência nesse código: renderTemplat
e chama ParseFile
toda vez que uma página é renderizada. Uma melhor abordagem seria chamar ParseFile
uma vez para cada template na inicialização do programa e armazenar o valor resultante de *Template
em uma estutura de dados para uso posterior.
Primeiro nós criamos um map global chamado templetes
no qual armazenaremos nossos valores *Template
, chaveados ["vinculados"] por string
(o nome do template):
var templates = make(map[string]*template.Template)
Então, criamos a função init
que será chamada antes de main
na inicialização do programa. A função template.MustParseFile
é um invólocro de ParseFile
que não retorna um código de erro; em vex disso, aciona panic [pânico, função de emergência que fecha o programa] se encontrar um erro. O panic é apropriada aqui; se templates não podem ser carregados a única coisa sensata a fazer é sair do programa.
func init() {
for _, tmpl := range []string{"edit", "view"} {
templates[tmpl] = template.MustParseFile(tmpl+".html", nil)
}
}
Um loop for
é usado com uma expressão range
para iterar por uma array constante contendo os nomes dos templates que queremos que sejam interpretados. Se quisermos adicionar mais templates ao programa devemos adicioná-los a essa array.
Nós então modicamos nossa função renderTemplate
para chamar o método Execute
no Template
apropriado dem templates
.
func renderTemplate(c *http.Conn, tmpl string, p *page) {
err := templates[tmpl].Execute(p, c)
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
}
}
Validação
Como você pode ter observado, esse programa tem uma grave falha de segurança: o usuário pode fornecer um caminho arbitrário para ser lido/escrito no servidor. Para atenuar isso, podemos escrever uma função para validar o título com uma expressão regular [
regex(p)].
Primeiro, adicione "regexp"
à lista de importação [import
]. Então, podemos criar uma variável global para armazenar nossa validação regexp:
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+quot;)
A função regexp.MustCompile
analisará e compilará a expressão regular e retornar regexp.Regexp
. MustCompile
, como template.MustParseFile
, é distinta de Compile
na medida em que aciona panic se a compilação da expressão falhar, enquanto Compile
retorna um os.Error
como segundo parâmetro.
Agora, vamos escrever uma função que extrái a string de título da URL solicitada e testa-a contra nossa expressão titleValidator
:
func getTitle(c *http.Conn, r *http.Request) (title string, err os.Error) {
title = r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(c, r)
err = os.NewError("Invalid Page Title")
}
return
}.
Se o título for válido, será retornado justamente com um valor nil
de erro. Se o título for inválido, a função escreverá o erro "404 Not Found" [não encontrado] para a conexão HTTP e retornará um erro para o manipulador.
Vamos colocar uma chamada para getTitle
em cada um dos manipuladores:
func viewHandler(c *http.Conn, r *http.Request) {
title, err := getTitle(c, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
http.Redirect(c, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(c, "view", p)
}
func editHandler(c *http.Conn, r *http.Request) {
title, err := getTitle(c, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(c, "edit", p)
}
func saveHandler(c *http.Conn, r *http.Request) {
title, err := getTitle(c, r)
if err != nil {
return
}
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err = p.save()
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(c, "/view/"+title, http.StatusFound)
}
Introduzindo Funções Literais e Closures
[Entenda closure como "clausura" - mas achei por bem deixar em inglês - , trata-se de uma função que envolve (enclausura) algo definido fora dela. Funções literais (function literals) são funções anônimas que podem ser assinaladas a uma variável e, então, eventualmente chamadas]
Capturando a condição do erro em cada manipulador introduz muito código repetido. E se pudermos envolver cada um dos manipuladores em uma função que faz essa validação e conferência de erro? Funçõe literais de Go provêm uma poderosa abstraçnao funcional que pode nos ajudar aqui.
Primeiro, reescrevemos a definição da função de cada um dos manipuladores para aceitar uma string de título:
func viewHandler(c, *http.Conn, r *http.Request, title string)
func editHandler(c, *http.Conn, r *http.Request, title string)
func saveHandler(c, *http.Conn, r *http.Request, title string)
Agora vamos definir uma função de invólucro que tem uma função do tipo acima, e retorna uma função do tipo de http.HandlerFunc
(adequado para ser passado para a função http.HandleFunc
):
func makeHandler(fn func (*http.Conn, *http.Request, string)) http.HandlerFunc {
return func(c *http.Conn, r *http.Request) {
// Here we will extract the page title from the Request,
// and call the provided handler 'fn'
}
}
A função retornada é chamada de closure [encerramento, clausura], porque envolve valores definidos fora dela. Neste caso, a variável fn
(o único argumento para makeHandler
) é delimitada pela closure. A variável fn
será um dos nossos manipuladores: save, edit ou view.
Agora podemos pegar o código do getTitle
e usá-lo aqui (com algumas pequenas modificações):
func makeHandler(fn func(*http.Conn, *http.Request, string)) http.HandlerFunc {
return func(c *http.Conn, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(c, r)
return
}
fn(c, r, title)
}
}
A closure retornada por makeHandler
é umas função que toma um http.Conn
e um http.Request
(Noutras palavras, um http.HandlerFunc
). A closure extrái o title
do path solicitado e valida-o como titleValidator
regexp. Se title
for inválido, um erro será escrito na Conn
usando a função http.NotFound
. Se title
for válido, o manipulador "enclausurado" fn
será chamado com a Conn
, Request
e title
como argumentos.
Agora nós podemos envolver as funções de manipulador com makeHandler
em main
, antes que elas sejam registradas com o package http
:
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
http.ListenAndServe(":8080", nil)
}
Finalmente, remova as chamadas para getTitle das funções do manipulador, tornando-as muito mais simples:
func viewHandler(c *http.Conn, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(c, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(c, "view", p)
}
func editHandler(c *http.Conn, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(c, "edit", p)
}
func saveHandler(c *http.Conn, r *http.Request, title string) {
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err := p.save()
if err != nil {
http.Error(c, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(c, "/view/"+title, http.StatusFound)
}
Experimente!
Clique aqui para ver a listagem final do código.
Recompile o código e execute o app:
$ 8g wiki.go
$ 8l wiki.8
$ ./8.out
Visitando http://localhost:8080/UmaNovaPagina
deve apresentar a página de formulário de edição. Você poderá, então, entrar com algum texto, clicar em "Salvar" e será redirecionado para a página recém-criada.
Outras tarefas
Aqui estão algumas tarefas simples que você pode querer resolver por conta própria:
- Armazene templates em
tmpl/
e dados da página em data/
.
- Adicione um manipulador para fazer o endereço root redirecionar para
/view/PaginaInicial
.
- Arrume a página de templates HTML, tornando-os válidos e adicione algumas regras CSS.
- Implemente inter-ligando página convertendo instâncias
[NomeDaPagina]
para <a href="/view/NomeDaPagina">NomeDaPagina</a>
. (dica: você pode usar regexp.ReplaceAllFunc
para fazer isso)