Go: RESTful - um exemplo completo com persistência

De Aulas

Afluentes: Sistemas Distribuídos e Mobile

Modelo DAO (Data Access Object)

Base de Dados

Para nosso exemplo de CRUD, vamos precisar de um local para armazenar nossas informações de forma persistente. Para isso, criamos uma tabela chamada users no Postgres. Abaixo temos a clausula CREATE TABLE da nossa tabela.

create table users (
    id integer not null,
    name varchar(50) not null,
    email varchar(50) not null,
    primary key (id)
);

Veja que ela é bastante simples, mas já serve como exemplo. Temos os atributros id do tipo integer e dois atributos, name e email, do tipo string, ou melhor, VARCHAR. O id é usado como chave primária.

Para nosso exemplo, criamos a base de dados chamada backendcrud. Fique a vontade para mudar no nome da base de dados, mas lembre-se de alterar no código fonte do arquivo main.go também.

Conforme nosso modelo DAO, essa é nossa FONTE DE DADOS.

database.go

Essa classe é meio que um padrão para a conexão com o banco de dados. O constructor, NewDatabase inicializa o objeto, configurando as informações de conexão passadas como parâmetro e retornando o próprio objeto na função (ver mais sobre a criação de objetos em Go). A função Close fecha a conexão com a base de dados.

Agora, a função que mais vamos utilizar é a Get. Toda vez que formos fazer uma operação no nosso banco, vamos usar essa função para pegarmos a nossa conexão. Caso ainda não tenha sido aberta uma conexão, essa conexão é feita e retornada ao usuário, caso contrário, apenas retorna a conexão aberta. Dessa forma, não precisamos ficar abrindo e fechando conexão toda vez que precisarmos fazer uma operação, nem ficar abrindo um monte de conexões, o que seria um problema. :D

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/lib/pq"
)

// Database class
type Database struct {
	connection *sql.DB
	dbtype     string
	info       string
	err        error
}

// NewDatabase Constructor
func NewDatabase(dbhost string, dbtype string, dbname string, dbuser string, dbpass string) *Database {
	db := new(Database)
	db.dbtype = dbtype
	db.info = fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", dbuser, dbpass, dbname)
	return db
}

// Get return connection
func (db *Database) Get() *sql.DB {
	if db.connection == nil {
		db.connection, db.err = sql.Open(db.dbtype, db.info)
	}
	return db.connection
}

// Close function
func (db *Database) Close() {
	db.connection.Close()
}

user.go

A classe User é a classe que representa o nosso objeto da tabela. Observe também que também temos informações nos atributos para fazer a serialização dos objetos para o formato JSON. Os atributos que serão serializados devem ser públicos, ou seja, iniciar com letra maiúscula.

A função NewUser serve para criar um objeto com os dados passados como parâmetro.

package main

type User struct {
	Id    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func NewUser(id int, name string, email string) *User {
	user := new(User)
	user.Id = id
	user.Name = name
	user.Email = email
	return user
}

Seguindo nosso modelo DAO essa classe cria nossos OBJETOS DE TRANSFERÊNCIA.

userdao.go

A classe UserDAO é, no DAO, nosso OBJETO DE ACESSO, responsável pelas operações com os OBJETOS DE TRANSFERÊNCIA e utilizado pelo OBJETO DE APLICAÇÃO.

Nessa classe temos todas as operações CRUD.

  • CREATE: representada aqui pelo método Insert. Ao chamarmos o método, passamos como parâmetro um OBJETO DE TRANSFERÊNCIA do tipo Users. Esse objeto é então gravado na base de dados caso o Id ainda não exista (isso porque é chave primária).
  • READ: a leitura dos dados é feita por dois métodos:
    • método GetAll que retorna uma lista de todos os dados armazenados na base (o que não é aconselhável caso a base seja grande. O interessante é fazer filtros para trazer parte dos dados apenas. Fiz assim por ser um exemplo simples em sala de aula, buscando diminuir a quantidade de código do exemplo).
    • método Get que retorna apenas um objeto. Veja que passamos o Id do objeto que queremos retornar.
  • UPDATE: Usamos o método Update para alterar as informações de um registro. Passamos como parâmetro o OBJETO DE TRANSFERÊNCIA que queremos alterar. Veja que ele irá alterar as informações do registro conforme as informações do objeto pelo Id desse objeto.
  • DELETE: O método Delete, exclui o registro com o Id passado como parâmetro.
package main

import "fmt"

type UserDAO struct {
	db    *Database
	users []*User
}

func NewUserDAO(db *Database) *UserDAO {
	userdao := new(UserDAO)
	userdao.db = db
	userdao.users = make([]*User, 0)
	return userdao
}

func (userdao *UserDAO) GetAll() []*User {
	userdao.users = make([]*User, 0)
	db := userdao.db.Get()
	rows, err := db.Query("select * from users")
	if err != nil {
		fmt.Println(err.Error())
	}
	for rows.Next() {
		var id int
		var name string
		var email string
		rows.Scan(&id, &name, &email)
		userdao.users = append(userdao.users, NewUser(id, name, email))
	}
	return userdao.users
}

func (userdao *UserDAO) Insert(user *User) {
	db := userdao.db.Get()
	query := "insert into users (id, name, email) values ($1, $2, $3)"
	_, err := db.Query(query, user.Id, user.Name, user.Email)
	if err != nil {
		fmt.Println(err.Error())
	}
}

func (userdao *UserDAO) Get(id string) *User {
	db := userdao.db.Get()
	query := "select * from users where id=$1"
	rows, err := db.Query(query, id)
	if err != nil {
		fmt.Println(err.Error())
	}
	if rows.Next() {
		var id int
		var name string
		var email string
		rows.Scan(&id, &name, &email)
		return NewUser(id, name, email)
	}
	return nil
}

func (userdao *UserDAO) Update(user *User) string {
	db := userdao.db.Get()
	query := "update users set name=$2, email=$3 where id=$1"
	_, err := db.Query(query, user.Id, user.Name, user.Email)
	if err != nil {
		fmt.Println(err.Error())
		return err.Error()
	}
	return "ok"
}

func (userdao *UserDAO) Delete(id string) string {
	db := userdao.db.Get()
	query := "delete from users where id=$1"
	_, err := db.Query(query, id)
	if err != nil {
		fmt.Println(err.Error())
		return err.Error()
	}
	return "ok"
}

main.go

Finalmente temos agora nosso main. Esse arquivo é o daemon da nossa aplicação, que, conforme já vimos, irá ficar ouvindo uma porta para receber, tratar e responder às requisições dos clientes.

O que temos de diferente dos exemplos anteriores é que agora, ao invés de alterarmos uma variável ou array ou objeto na memória, estamos usando o DAO para fazer o CRUD na nossa base de dados.

Outra coisa é que precisamos configurar as mensagens de retorno. Isso porque:

  • Precisamos informar ao cliente que tipo de dados estamos enviando;
  • Precisamos informar ao CORS que estamos liberando algumas coisas.

Por exemplo, em todas as funções que tratam as operações do serviço web, informamos que estamos enviando dados no formato JSON e estamos permitindo que a origem do serviço web possa ser em qualquer lugar (asterisco). Na verdade, aqui estamos contornando uma questão de segurança porque muitas vezes estamos desenvolvendo o serviço e o cliente (full stack) no nosso próprio computador, então precisamos testar as coisas. Para fazer o deployment da aplicação real, é melhor pensar na segurança e retringir coisas.

w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")

Outra coisa que adicionamos aqui, é o tratamento do método OPTIONS sem e com parâmetro. Nele, adicionamos as seguintes configurações:

w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")

em que, além das duas configurações dos outros métodos, colocamos mais duas para informar as operações que o servidor permite e que tipo de conteúdos também.

Esse método é importante porque o CORS, antes de requisitar operações como DELETE e PUT, por exemplo, envia uma requisição OPTIONS para verificar o que pode fazer no servidor. Se não tiver as informações de autorização, o CORS já retorna o erro na requisição e não efetua os métodos.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	db := NewDatabase("localhost", "postgres", "backendcrud", "saulo", "1234")
	defer db.Close()
	dao := NewUserDAO(db)

	router := http.NewServeMux()

	router.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		dao.Insert(NewUser(user.Id, user.Name, user.Email))
	})

	router.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		users := dao.GetAll()
		json.NewEncoder(w).Encode(users)
	})

	router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		id := r.PathValue("id")
		user := dao.Get(id)
		json.NewEncoder(w).Encode(user)
	})

	router.HandleFunc("PUT /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		out := dao.Update(NewUser(user.Id, user.Name, user.Email))
		json.NewEncoder(w).Encode(out)
	})

	router.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		id := r.PathValue("id")
		out := dao.Delete(id)
		json.NewEncoder(w).Encode(out)
	})

	// Vamos precisar do OPTIONS porque o PUT primeiro manda um OPTION
	// Assim, se for localhost, avisamos o CORS que permitimos local
	router.HandleFunc("OPTIONS /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
	})

	// O DELETE também chama o OPTIONS, mas envia também uma informação
	router.HandleFunc("OPTIONS /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
	})

	err := http.ListenAndServe("localhost:8080", router)
	if err != nil {
		fmt.Println(err.Error())
	}
}

Alternativa

Uma forma alternativa é usar a biblioteca cors como usamos nos outros exemplos. Dessa forma não precisamos controlar o OPTIONS manualmente.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/rs/cors"
)

func main() {
	db := NewDatabase("localhost", "postgres", "backendcrud", "saulo", "1234")
	defer db.Close()
	dao := NewUserDAO(db)

	router := http.NewServeMux()

	router.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		dao.Insert(NewUser(user.Id, user.Name, user.Email))
	})

	router.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		users := dao.GetAll()
		json.NewEncoder(w).Encode(users)
	})

	router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		id := r.PathValue("id")
		user := dao.Get(id)
		json.NewEncoder(w).Encode(user)
	})

	router.HandleFunc("PUT /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		out := dao.Update(NewUser(user.Id, user.Name, user.Email))
		json.NewEncoder(w).Encode(out)
	})

	router.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		id := r.PathValue("id")
		out := dao.Delete(id)
		json.NewEncoder(w).Encode(out)
	})

	c := cors.AllowAll()
	r := c.Handler(router)

	err := http.ListenAndServe("localhost:8080", r)
	if err != nil {
		fmt.Println(err.Error())
	}
}

Criação do Projeto

Antes de mais nada, precisamos instalar o Go. Para isso, siga as indicações em: Go: Instalação e Configuração

Primeiro precisamos criar nossa base de dados no postgres. É importante que o usuário tenha permissão de criação de databases.

$ createdb backendcrud
$ psql backendcrud
backendcrud=> create table users (id integer not null, name varchar(50) not null, email varchar(50) not null, primary key (id));
backendcrud=> \q

Depois criamos nosso projeto no Go. Lembre-se de colocar em uma subpasta dentro de algo como aulas.com dentro da subpasta src do GOPATH. Por exemplo:

/home/saulo/goprojects/src/aulas.com

sendo que nosso GOPATH seria /home/saulo/goprojects

Depois disso, dentro do local especificado, digitar os comandos para a criação do projeto.

$ mkdir backendcrud
$ cd backendcrud
$ go mod init

Depois de criado o projeto, vamos adicionar a biblioteca para usar o postgres.

go get github.com/lib/pq

Colocamos então todos os arquivos fonte que mostramos aqui, configuramos, compilamos e rodamos nosso serviço web:

$ go mod tidy
$ go build
$ ./backendcrud

Como aqui estou no linux, a chamada da aplicação foi assim. No windows, basta chamar o arquivo backendcrud.exe criado durante a compilação. go mod tidy

Manual de Utilização

Diferente do padrão SOAP, não temos instruções como usar nosso serviço web no próprio serviço, então, ao usar Rest, precisamos informar aos usuários do serviço como o mesmo pode ser utilizado. Segue então nosso breve manual:

Dados do cadastro de usuários
Id - inteiro
Name - string
email - string
Listar usuários
GET /users
Retornar informações de um usuário pelo ID
GET /users/ID
Enviar informações de um novo usuário para armazenar remotamente
POST /users

Conteúdo JSON do corpo da mensagem (exemplo)

{
   "id": 123,
   "name": "Misato",
   "email": "misato@tokyo3.jp"
}
Alterar informações
PUT /users

Conteúdo JSON do corpo da mensagem (exemplo)

{
   "id": 123,
   "name": "Katsuragi Misato",
   "email": "misato@tokyo3.com.jp"
}

Veja que o Id deve existir para poder alterar

Excluir um registro

Precisa passar o id do registro que se quer excluir

DELETE /users/ID