in Go, Microservices

Building Microservice Applications with Go

Hi everyone,

In this article, with Go we will learn how to develop a microservice application.

Note : Repository design pattern was not implemented in this project. The terms “repository” were used only as a naming convention.

Firstly, let’s look at the file structure of the project;

The technologies we use in this project are;

The purpose of this article;

  • Development microservice applications with Go
  • Learning the Goji with Go
  • Understanding the use of MongoDB with Go

Preparation;

  • Start MongoDB service

Lets start!

First, we prepare the model layer;

models/Page.go

package models

type Page struct {
	Title          string
	Author         string
	Header         string
	PageDefinition string
	Content        string
	URI            string
}

models/Book.go

package models

type Book struct {
	ISBN    string   `json:"isbn"`
	Title   string   `json:"title"`
	Authors []string `json:"authors"`
	Price   string   `json:"price"`
}

Next, we are preparing the common layer;

common/JsonProcesses.go

package common

import (
	"fmt"
	"net/http"
)

func ErrorWithJSON(w http.ResponseWriter, message string, code int) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)
	fmt.Fprintf(w, "{message: %q}", message)
}

func ResponseWithJSON(w http.ResponseWriter, json []byte, code int) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)
	w.Write(json)
}

Now, we are preparing the repository layer to do database operations;

repos/PageRepository.go

package repos

// General preparation.
import (
	"encoding/json"
	"log"
	"net/http"

	"goji.io/pat"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	comm "../common"
	m "../models"
)

// Bring all pages.
func AllPages(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()   // Create a copy of the session.
		defer session.Close() // End the session when the process is complete.
		c := session.DB("store").C("pages")
		var pages []m.Page
		err := c.Find(bson.M{}).All(&pages) // MongoDB query : Bring all pages.
		if err != nil {
			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed get all pages: ", err)
			return
		}
		respBody, err := json.MarshalIndent(pages, "", "  ") // Format the data.
		if err != nil {
			log.Fatal(err)
		}
		comm.ResponseWithJSON(w, respBody, http.StatusOK) // Back to results.
	}
}

func AddPage(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		var page m.Page
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&page)
		if err != nil {
			comm.ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)
			return
		}
		c := session.DB("store").C("pages")
		err = c.Insert(page) // MongoDB query : Add data.
		if err != nil {
			if mgo.IsDup(err) { // Is there a duplicate error? (IsDup)
				comm.ErrorWithJSON(w, "This page already exists", http.StatusBadRequest)
				return
			}
			// For any error...
			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed insert page: ", err)
			return
		}
		// The output out this process will be JSON.
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Location", r.URL.Path+"/"+page.Title)
		// Write header : "Created" status message.
		w.WriteHeader(http.StatusCreated)
	}
}

func PageByID(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		title := pat.Param(r, "title")
		c := session.DB("store").C("pages")
		var page m.Page
		err := c.Find(bson.M{"title": title}).One(&page)
		if err != nil {
			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed find page: ", err)
			return
		}
		if page.Title == "" {
			comm.ErrorWithJSON(w, "Page not found", http.StatusNotFound)
			return
		}
		respBody, err := json.MarshalIndent(page, "", "  ")
		if err != nil {
			log.Fatal(err)
		}
		comm.ResponseWithJSON(w, respBody, http.StatusOK)
	}
}

func UpdatePage(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		title := pat.Param(r, "title")
		var page m.Page
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&page)
		if err != nil {
			comm.ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)
			return
		}
		c := session.DB("store").C("pages")
		err = c.Update(bson.M{"title": title}, &page)
		if err != nil {
			switch err {
			default:
				comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
				log.Println("Failed update page: ", err)
				return
			case mgo.ErrNotFound:
				comm.ErrorWithJSON(w, "Page not found", http.StatusNotFound)
				return
			}
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func DeletePage(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		title := pat.Param(r, "title") // Will be deleted with "title" data(So not with ID).
		c := session.DB("store").C("pages")
		err := c.Remove(bson.M{"title": title})
		if err != nil {
			switch err {
			default:
				comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
				log.Println("Failed delete page: ", err)
				return
			case mgo.ErrNotFound:
				comm.ErrorWithJSON(w, "Page not found", http.StatusNotFound)
				return
			}
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

repos/BookRepository.go

package repos

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

	"goji.io/pat"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	comm "../common"
	m "../models"
)

func AllBooks(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		c := session.DB("store").C("books")
		var books []m.Book
		err := c.Find(bson.M{}).All(&books)
		if err != nil {
			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed get all books: ", err)
			return
		}
		respBody, err := json.MarshalIndent(books, "", "  ")
		if err != nil {
			log.Fatal(err)
		}
		comm.ResponseWithJSON(w, respBody, http.StatusOK)
	}
}

func AddBook(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()

		var book m.Book
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&book)
		if err != nil {
			comm.ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)
			return
		}

		c := session.DB("store").C("books")

		err = c.Insert(book)
		if err != nil {
			if mgo.IsDup(err) {
				comm.ErrorWithJSON(w, "Book with this ISBN already exists", http.StatusBadRequest)
				return
			}

			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed insert book: ", err)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Location", r.URL.Path+"/"+book.ISBN)
		w.WriteHeader(http.StatusCreated)
	}
}

func BookByISBN(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		isbn := pat.Param(r, "isbn")
		c := session.DB("store").C("books")
		var book m.Book
		err := c.Find(bson.M{"isbn": isbn}).One(&book)
		if err != nil {
			comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
			log.Println("Failed find book: ", err)
			return
		}
		if book.ISBN == "" {
			comm.ErrorWithJSON(w, "Book not found", http.StatusNotFound)
			return
		}
		respBody, err := json.MarshalIndent(book, "", "  ")
		if err != nil {
			log.Fatal(err)
		}
		comm.ResponseWithJSON(w, respBody, http.StatusOK)
	}
}

func UpdateBook(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		isbn := pat.Param(r, "isbn")
		var book m.Book
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&book)
		if err != nil {
			comm.ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)
			return
		}
		c := session.DB("store").C("books")
		err = c.Update(bson.M{"isbn": isbn}, &book)
		if err != nil {
			switch err {
			default:
				comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
				log.Println("Failed update book: ", err)
				return
			case mgo.ErrNotFound:
				comm.ErrorWithJSON(w, "Book not found", http.StatusNotFound)
				return
			}
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func DeleteBook(s *mgo.Session) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		session := s.Copy()
		defer session.Close()
		isbn := pat.Param(r, "isbn")
		c := session.DB("store").C("books")
		err := c.Remove(bson.M{"isbn": isbn})
		if err != nil {
			switch err {
			default:
				comm.ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
				log.Println("Failed delete book: ", err)
				return
			case mgo.ErrNotFound:
				comm.ErrorWithJSON(w, "Book not found", http.StatusNotFound)
				return
			}
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

And we complete the project…

main.go

package main

import (
	"net/http"

	"goji.io"
	"goji.io/pat"
	"gopkg.in/mgo.v2"

	repos "./repos"
)

func main() {

	session, err := mgo.Dial("localhost")
	if err != nil {
		panic(err)
	}
	defer session.Close()

	session.SetMode(mgo.Monotonic, true)

	mux := goji.NewMux()

	// Page API
	mux.HandleFunc(pat.Get("/pages"), repos.AllPages(session))
	mux.HandleFunc(pat.Post("/pages"), repos.AddPage(session))
	mux.HandleFunc(pat.Get("/pages/:title"), repos.PageByID(session))
	mux.HandleFunc(pat.Put("/pages/:title"), repos.UpdatePage(session))
	mux.HandleFunc(pat.Delete("/pages/:title"), repos.DeletePage(session))

	// Book API
	mux.HandleFunc(pat.Get("/books"), repos.AllBooks(session))
	mux.HandleFunc(pat.Post("/books"), repos.AddBook(session))
	mux.HandleFunc(pat.Get("/books/:isbn"), repos.BookByISBN(session))
	mux.HandleFunc(pat.Put("/books/:isbn"), repos.UpdateBook(session))
	mux.HandleFunc(pat.Delete("/books/:isbn"), repos.DeleteBook(session))

	http.ListenAndServe("localhost:8080", mux)
}

Now we can test the application!

First we have to run the application

go run main.go

No collections are currently available in the database.

Create a new ‘page’ data;

And MongoDB…

I’m adding another record;

{
        "Title": "Trainings",
        "Author": "Cihan Özhan",
        "Header": "CihanOzhan.Com - Trainings",
        "PageDefinition": "Kişisel blog sitemdeki eğitimlerimi listelediğim sayfadır.",
        "Content": "I have been researching, training and consulting on software for a long time. I share some of these researches as video training.",
        "URI": "http://www.cihanozhan.com/trainings"
}

And I call the Page API

Get and AllPages;

Now Put and Update;

Look at MongoDB…

Example queries for filtering;

  • GET : http://localhost:8080/pages/Trainings
  • GET : http://localhost:8080/pages/About

Delete operation with API;

Research Homework;

  • You use BookRepository.go
  • Develop your UserRepository.go

Good luck!
Cihan Özhan

 

Write a Comment

Comment