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

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