Go - a revolução‎ > ‎Codelab‎ > ‎

Criando Aplicativos Web [em Go]

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 editHandler 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 &gt;). 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: renderTemplate 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)
Comments