En este artículo realizaremos una API rest básica con el lenguaje de programación golang, utilizando gorilla/mux
Introducción
Para quienes no lo conozcan, golang es un lenguaje creado por Google allá por el 2009, y se está extendiendo como una nueva alternativa a otros lenguajes ya asentados como Java, Python o C/C++ entre otros.
Destaca por varias características:
- Compilación estática esto tiene dos ventajas:
- Mayor velocidad al compilar proyectos muy extensos, tardan cuestión de segundos.
- Portabilidad: se pueden compilar binarios para multiples plataformas en el proceso de build.
- Modelo de concurrencia: el modelo de concurrencia basado en goroutines es generalmente más eficiente que el trabajo con hilos, ya que se multiplexan a través de hilos del sistema.
A día de hoy, hay muchos productos/proyectos que ya lo están utilizando, entre otros:
Como material de apoyo para el seguimiento del artículo, recomiendo comenzar por lo básico:
- Golang Tour: Una guia interactiva con la que se aprenden los conceptos básicos del lenguaje.
- Libro online An Introduction to Programming in Go, donde se hace un resumen de la mayoría de funcionalidades básicas del lenguaje.
Este artículo está pensado para crear APIs RestFul y vamos a centrarnos en esto únicamente, partiendo que ya se tienen nociones del lenguaje. No utilizaremos ningún gestor de BBDD, si no que los datos se almacenarán en memoria.
Setup inicial
De momento vamos a usar la dependencia de gorilla/mux, para ello con Golang instalado correctamente ejecutamos el siguiente comando en el terminal (en el raiz del proyecto y con el GOPATH configurado):
$ go get github.com/gorilla/mux
Definición de la API
Tipos
Realizaremos un único endpoint para la gestión de la entidad User con sus operaciones CRUD. Para ello, crearemos un paquete en el directorio raíz con el mismo nombre y en el archivo users/users.go escribiremos lo siguiente:
package users
type User struct {
Id int `json:"id"`
Name string `json:"name,omitempty"`
SurName string `json:"surname,omitempty"`
Age byte `json:"age,omitempty"`
}
Puesto que nuestra API trabaja con JSON para el intercambio de mensajes, vamos a renombrar los campos en las tags (en minúsculas) e indicar que campos deben llegar siempre en el mensaje JSON y cuales son opcionales (indicando omitempty en el tag).
Creación del router
El siguiente paso sería crear la propia definición de la API. Como es un ejemplo sencillo, de momento vamos a dejar las llamadas en el propio main.go:
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
var Users map[int]User
func main() {
Users = make(map[int]User)
log.Println("Default users: ", Users)
router := mux.NewRouter()
router.HandleFunc("/users", CreateUser).Methods("POST")
router.HandleFunc("/users", GetUsers).Methods("GET")
router.HandleFunc("/users/{id}", GetUser).Methods("GET")
router.HandleFunc("/users/{id}", DeleteUser).Methods("DELETE")
log.Println("Listening on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
Como podemos ver, es necesario crear los handler para cada operación:
- CreateUser: crea un usuario y lo almacena. Como se indica en la definición, es una llamada
POST
. - GetUsers: devuelve los usuarios que se hayan registrado previamente con la operación CreateUser. Es una llamada
GET
. - GetUser: devuelve el usuario con el
id
especificado en el path de la llamada. Es una llamadaGET
. - DeleteUser: elimina un usuario del sistema. Es una llamada
DELETE
.
Adicionalmente, almacenamos en el mapa Users
los usuarios creados, es decir, nos hace de respaldo en memoria.
Cada una de estas operaciones deberá devolver códigos HTTP de retorno, que explicaremos más adelante en la definición de cada método.
Creación de los Handler
Creación de usuarios
El siguiente fragmento de código implementa la creación de un usuario en el mapa Users
:
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil{
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if _, ok := Users[user.Id]; ok {
http.Error(w, "Already exists an user with the same id", http.StatusConflict)
return
}else{
Users[user.Id] = user
}
log.Println("User: ", user, "added")
}
La implementación es lo más básica posible, con un chequeo de que el payload recibido tiene un formato json correcto, devolviendo un 400 en caso erroneo.
También se comprueba si ya existe un usuario con el mismo id
, retornando un 409 si existe.
Listado de usuarios
El siguiente fragmento de código devuelve un array con todos los usuarios insertados mediante el método anterior:
func GetUsers(w http.ResponseWriter, r *http.Request) {
userList := make([]User, 0)
for _,value := range Users {
userList = append(userList, value)
}
json.NewEncoder(w).Encode(userList)
}
No hay mucho que comentar, puesto que la codificación no deberia fallar, ya que es a partir de datos en memoria.
Obtención de un usuario
Si queremos obtener un usuario, primero habrá que comprobar que el payload tiene un id
numérico (retornando un 400 en caso de no ser un número) y si éste existe en el mapa de usuarios (retornando un 404 en caso que no exista), por lo demás todo bastante sencillo:
func GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if user, ok := Users[id]; ok {
json.NewEncoder(w).Encode(user)
} else {
http.Error(w, "Not Found", http.StatusNotFound)
}
}
Borrado de usuario
Por último, el borrado de usuarios, realiza los mismos chequeos que el anterior método y termina por borrarlo del mapa:
func DeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if user, ok := Users[id]; ok {
delete(Users, id)
log.Println("User: ", user, "removed")
} else {
http.Error(w, "Not Found", http.StatusNotFound)
}
}
Resaltar que algunas APIs devuelven el JSON de la entidad/recurso eliminado. En este caso, no se retornará.
Resultado final y pruebas
El código principal quedaría como sigue:
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
. "rest-api/users"
"strconv"
)
var Users map[int]User
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil{
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if _, ok := Users[user.Id]; ok {
http.Error(w, "Already exists an user with the same id", http.StatusConflict)
return
}else{
Users[user.Id] = user
}
log.Println("User: ", user, "added")
}
func GetUsers(w http.ResponseWriter, r *http.Request) {
userList := make([]User, 0)
for _,value := range Users {
userList = append(userList, value)
}
json.NewEncoder(w).Encode(userList)
}
func GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if user, ok := Users[id]; ok {
json.NewEncoder(w).Encode(user)
} else {
http.Error(w, "Not Found", http.StatusNotFound)
}
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if user, ok := Users[id]; ok {
delete(Users, id)
log.Println("User: ", user, "removed")
} else {
http.Error(w, "Not Found", http.StatusNotFound)
}
}
func main() {
Users = make(map[int]User)
log.Println("Default users: ", Users)
router := mux.NewRouter()
router.HandleFunc("/users", CreateUser).Methods("POST")
router.HandleFunc("/users", GetUsers).Methods("GET")
router.HandleFunc("/users/{id}", GetUser).Methods("GET")
router.HandleFunc("/users/{id}", DeleteUser).Methods("DELETE")
log.Println("Listening on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
Para ejecutarlo simplemente vamos al terminal y ejecutamos (desde el directorio raiz del proyecto):
$ go run main.go
Si queremos hacer pruebas, se pueden hacer llamadas con curl por ejemplo (desde otro terminal):
- Creación:
$ curl -X POST \
http://localhost:8080/users \
-d '{
"id":1,
"name": "Carlos"
}'
- Listado:
$ curl http://localhost:8080/users
- Obtener usuario:
$ curl http://localhost:8080/users/1
$ curl http://localhost:8080/users/2
- Borrado:
$ curl -X DELETE http://localhost:8080/users/1
$ curl -X DELETE http://localhost:8080/users/1
Conclusiones
Como se puede observar, realizar una API Rest en golang es extremadamente sencillo, y si, no he implementado el método PUT de forma deliberada, ya que en próximos artículos trataré de exponer el problema desde otro punto de vista más orientado a TDD; cómo se podría haber implementado la API partiendo de tests, ya que el testing de golang para APIs es muy completo.
Este cambio implicará un refactor importante, ya que no se codificará una línea de código que no esté testeada.
Adicionalmente os contaré más cosas sobre el gestor de paquetería a usar, integración con BBDD, etc …
Quiero agradecer y dedicar este artículo a dos personas que me han ayudado a montar el blog (a parte de mi hermano, que ya conoceis):
- Julia Maroto, aunque el diseño del blog tomó un diseño diferente al inicial, he mantenido el icono que nos hiciste :)
- Yeray Medina, que me contó el mundillo de Hugo y nos cambió la forma de enfocar el blog de una forma más estática y mantenible
A los dos y a mucha más gente que nos lee, muchas gracias ;)