En el siguiente post les enseño los pasos para crear un proyecto esqueleto con CRUD de un modelo conectado a MongoDB. No introduciré demasiado en cómo funciona NodeJS como tal, existen varias páginas que ayudan en este aspecto. La motivación es porque la generación automática de CRUD de un modelo en Golang no es tan intuitivo para nuevos usuarios en esta plataforma.
Todo el código de esta entrada pueden encontrarla en: https://github.com/arturoverbel/microservice_compra.
¿Qué es GoLang?
El lenguaje Go es un lenguaje de programación diseñado por Google en Noviembre del 2009. Sus características son: compilado, concurrente, imperativo, estructurado, no orientado a objetos y con recolector de basura.
Go no es orientada a objetos, aunque trabaja con una dinámica parecida a las clases, estas no pueden hacer herencia. el uso de semicolon “;” al final de una instrucción es opcional.
Instalación
- Descargar la última versión de Go en el enlace: https://golang.org/dl
- Descomprimimos
sudo tar -C /usr/local -xzvf goX.X.X.linux-xxx.tar.gz
- Agregaremos la carpeta bin de la carpeta de instalación de GO a la variable de entorno PATH:
export PATH=$PATH:/usr/local/go/bin
- Y comprobamos la versión:
go version
- Ahora procedemos a crear una carpeta donde estén todos los proyectos de Go:
export GOPATH=$HOME/projects_go
- Dentro de esta carpeta crearemos la estructura de carpetas que sugiere GOLANG en su documentación oficial, mi carpeta será:
mkdir -p src/github.com/arturoverbel
Esta es la misma dirección de mi github personal. Aquí será nuestro workspace
Hola mundo
Dentro de la carpeta de trabajo creamos un archivo Go:
cd GOPATH/src/github.com/arturoverbel mkdir holamundo cd holamundo vim holamundo.go
Escribimos el siguiente código:
package main import "fmt" func main(){ fmt.Println("Hola Mundo") }
Ejecutamos el código corriendo:
go run holamundo.go
Instalar módulos
Para poder hacer nuestro servicio conectado a nuestro mongo (daré por instalado mongo en nuestro local), necesitares dos módulos:
Gorilla Mux
Este es un módulo para recibir request y configurar los response de nuestro servicio. mux.Router compara las solicitudes entrantes con una lista de rutas registradas y llama a un controlador para la ruta que coincide con la URL u otras condiciones.
Para instalarlo en nuestro proyecto basta con el comando:
go get github.com/gorilla/mux
Paquete Bson
Fue creado como parte del controlador mgo MongoDB para Go, pero es independiente y puede usarse solo sin el controlador.
Su instalación es con el comango:
go get gopkg.in/mgo.v2/bson
Al instalar estos paquetes, podemos verlos justo al ladode nuestra carpeta personal arturoverbel.
Crear Modelo
Vamos a crear un microservicio de compras, el cuál gestionara los productos que un usuario ha comprado, su valor total y el método de pago.
Creamos una estructura en go y utilizamos bson para relacionar nuestro modelo con la base de datos de mongo. En el archivo model/shopping.go insertamos las siguientes lineas de código:
package model import ( "gopkg.in/mgo.v2/bson" ) // Shopping - Model type Shopping struct { ID bson.ObjectId `bson:"_id" json:"id"` User int `bson:"user" json:"user"` Products []string `bson:"products" json:"products"` Payment string `bson:"payment" json:"payment"` PriceTotal int `bson:"price_total" json:"price_total"` } // ShoppingID - for request type ShoppingID struct { ID string `json:"id"` }
En la estructura del modelo, definimos también el nombre del atributo en la colección de mongo y el nombre cuando la estructura se devuelva en formato json.
Conexión con BD
Ahora creamos un archivo con el path connection/connection.go para implementar el CRUD de la base de datos y que pueda ser consumido por el controlador.
En la primera parte del archivo insertamos:
package connection import ( "errors" "log" "time" "github.com/arturoverbel/microservice_compra/model" mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" ) // INFO - to connect mongo var INFO = &mgo.DialInfo{ Addrs: []string{"127.0.0.1:27017"}, Timeout: 60 * time.Second, Database: "cool_db", Username: "admin", Password: "secret_password", } // DBNAME the name of the DB instance const DBNAME = "cool_db" // DOCNAME the name of the document const DOCNAME = "shoppings" var db *mgo.Database // COLLECTION - name collection on Mongo const ( COLLECTION = "shoppings" )
La primera parte del archivo, además del nombre del paquete y las librerías, definimos la conexión a la BD utilizando mgo. Definimos unas constantes como son el nombre de la base de datos, el documento y la colecctión en mongo.
Ahora definimos las funciones:
Insertar
// Insert - Insert a Shopping func Insert(shopping model.Shopping) error { session, err := mgo.DialWithInfo(INFO) defer session.Close() shopping.ID = bson.NewObjectId() session.DB(DBNAME).C(DOCNAME).Insert(shopping) if err != nil { log.Fatal(err) return err } return nil }
Primero definimos la sesión. Para crear un objeto en nuestro mongo, primero definimos un ID y lo guardamos en el objeto que queremos almacenar. Ahora solo lo pasamos por el método Insert de la sesión y validamos que no haya error.
Encontrar por ID
// FindByID - ... func FindByID(id string) (model.Shopping, error) { var shopping model.Shopping if !bson.IsObjectIdHex(id) { err := errors.New("Invalid ID") return shopping, err } session, err := mgo.DialWithInfo(INFO) if err != nil { log.Fatal(err) return shopping, err } defer session.Close() c := session.DB(DBNAME).C(DOCNAME) oid := bson.ObjectIdHex(id) err = c.FindId(oid).One(&shopping) return shopping, err }
Para encontrar por ID primero tenemos que validar que el ID que nos pasan es valido en el formato de mongo (lineas 4 – 7), luego si lo encontramos y devolvemos la estructura.
Actualizar
// Update - .. func Update(shopping model.Shopping) error { session, err := mgo.DialWithInfo(INFO) if err != nil { log.Fatal(err) return err } defer session.Close() c := session.DB(DBNAME).C(DOCNAME) err = c.UpdateId(shopping.ID, &shopping) return err }
Encontrar por Usuario
// FindByUser - ... func FindByUser(idUser int) ([]model.Shopping, error) { var shoppings []model.Shopping session, err := mgo.DialWithInfo(INFO) if err != nil { log.Fatal(err) return shoppings, err } defer session.Close() c := session.DB(DBNAME).C(DOCNAME) err = c.Find(bson.M{"user": idUser}).All(&shoppings) return shoppings, err }
En este caso devolvemos un array con todas las compras del usuario.
Eliminar
// Delete - ... func Delete(id string) error { if !bson.IsObjectIdHex(id) { err := errors.New("Invalid ID") return err } session, err := mgo.DialWithInfo(INFO) if err != nil { log.Fatal(err) return err } defer session.Close() c := session.DB(DBNAME).C(DOCNAME) oid := bson.ObjectIdHex(id) err = c.RemoveId(oid) return err }
Controladores
Creamos un archivo en la raiz principal llamado main.go, desde aquí vamos a incluir los controladores:
Definimos cada CRUD como controlador:
Encontrar por ID
// FindShoppingByIDController - Encuentra una compra por su ID func FindShoppingByIDController(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) shopping, err := connection.FindByID(params["id"]) if err != nil { respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusOK, shopping) }
El código anterior atrapa un parametro de la URL y utiliza la función de connection, si existe un error responde con error. Para devolver exitosamente un mensaje de error o los datos cargados correctamente, implementamos dos métodos:
func respondWithError(w http.ResponseWriter, code int, message string) { respondWithJSON(w, code, map[string]string{"error": message}) } func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { response, _ := json.Marshal(payload) w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) w.Write(response) }
Encontrar por usuario
// FindShoppingByUserController - Encuentra una compra por su ID func FindShoppingByUserController(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) idUser, err := strconv.Atoi(params["id_user"]) if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid user ID") return } shoppings, err := connection.FindByUser(idUser) if err != nil { respondWithError(w, http.StatusBadRequest, "Error") return } respondWithJSON(w, http.StatusOK, shoppings) }
Crear registro
// CreateShoppingController - Crear una compra func CreateShoppingController(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var shopping model.Shopping if err := json.NewDecoder(r.Body).Decode(&shopping); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request") return } shopping.ID = bson.NewObjectId() if err := connection.Insert(shopping); err != nil { respondWithError(w, http.StatusInternalServerError, err.Error()) return } respondWithJSON(w, http.StatusCreated, shopping) }
Para crear registro, recibimos los valores por método POST y los cargamos en nuestra estructura de Shopping.
Actualizar
// UpdateShoppingController - Actualiza una compra func UpdateShoppingController(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var shopping model.Shopping if err := json.NewDecoder(r.Body).Decode(&shopping); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request") return } if err := connection.Update(shopping); err != nil { respondWithError(w, http.StatusInternalServerError, err.Error()) return } respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"}) }
Eliminar
// DeleteShoppingController - Borrr una compra func DeleteShoppingController(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var shoppingID model.ShoppingID if err := json.NewDecoder(r.Body).Decode(&shoppingID); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request") return } if err := connection.Delete(shoppingID.ID); err != nil { respondWithError(w, http.StatusInternalServerError, err.Error()) return } respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"}) }
Implementar rutas
En el mismo archivo principal, main.go, agregamos nuestras rutas:
package main import ( "encoding/json" "log" "net/http" "strconv" "github.com/arturoverbel/microservice_compra/connection" "github.com/arturoverbel/microservice_compra/model" "github.com/gorilla/mux" "gopkg.in/mgo.v2/bson" ) var prefixPath = "/api/compra" func main() { r := mux.NewRouter() r.HandleFunc(prefixPath, CreateShoppingController).Methods("POST") r.HandleFunc(prefixPath, UpdateShoppingController).Methods("PUT") r.HandleFunc(prefixPath, DeleteShoppingController).Methods("DELETE") r.HandleFunc(prefixPath+"/{id}", FindShoppingByIDController).Methods("GET") r.HandleFunc(prefixPath+"/by-user/{id_user}", FindShoppingByUserController).Methods("GET") log.Printf("Listening...") if err := http.ListenAndServe(":3000", r); err != nil { log.Fatal(err) } }
En el código anterior utilizamos la librería mux para generar nuestros endpoints.
Corriendo
Corremos nuestra aplicación con:
go run main.go
Ahora nos dirigimos a Postman y guardamos un registro con el siguiente request:
Insertar registro
POST – http://127.0.0.1:3000/api/compra
Request:
{ "user": 1, "products": ["1"], "payment": "Cash", "price_total": 20 }
Response – status 201:
{ "id": "5c3bc2a842e51e485a4f58c9", "user": 1, "products": [ "1" ], "payment": "Cash", "price_total": 20 }
Consultar registro
Utilizamos el endpoint:
GET – http://127.0.0.1:3000/api/compra/by-user/1
Responjse – status 200:
[ { "id": "5c3bc2a842e51e485a4f58ca", "user": 1, "products": [ "1" ], "payment": "Cash", "price_total": 20 } ]
Puede revisar todo el postman en la carpeta doc del repositorio o siguiendo el enlace: https://github.com/arturoverbel/microservice_compra/blob/master/doc/microservice_compra.postman_collection.json
Dockerfile
Para correr este ms en un contenedor, se puede usar el Dockerfile:
FROM golang:latest RUN mkdir -p /go/src/github.com/arturoverbel/microservice_compra ADD . /go/src/github.com/arturoverbel/microservice_compra WORKDIR /go/src/github.com/arturoverbel/microservice_compra RUN go get -v RUN go install github.com/arturoverbel/microservice_compra ENTRYPOINT /go/bin/microservice_compra EXPOSE 3000
Conclusiones
Es muy fácil crear un microservicio con Golang, pero muchos desarrolladores tienen que generar sus propias librerías de servicios (utilizando estas existentes) para tener más herramientas integradas de request y response.
El repositorio de este microservicio se encuentra en: https://github.com/arturoverbel/microservice_compra
Comentarios