Controller Request Mapping in Golang

Background

I’ve been developing web applications using Java and Spring Framework and in 2019, I started using Golang in some of my projects. So far my experience with Golang is great but I miss the annotation that Spring framework provides particularly the @RequestMapping for URL mapping, @Bean and @Autowired for dependency injection.

@RequestMapping(value = “/v1/get-products”, method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public Callable getProducts() {
     return responsePayloader.generate(false, new ArrayList<>());
}

Most of the examples online and from some books that I read declares the URL request mappings in main.go. This is troublesome when multiple developers working on the project because it generates a lot of git merge conflicts.

Objectives

To avoid any misunderstand let us define some objectives:

  1. Ability to define URL mappings to its corresponding controller instead of putting on main.go or other place
  2. Load all the URL mappings from all controllers using a bootstrapping mechanism
  3. Reduce git merge conflict related to URL mappings

My Solution

I decided as a solution is to create a bootstrapping mechanism and an interface for the request mapping. Java’s annotation is actually an interface which made me thought of copying similar concept. The project is structured like the image shown below.

controllers/z.go – contains the initializer, methods and variables that can be shared across your controllers. I like to think of it as the equivalent of Spring Framework’s @Bean.

package controllers

//var (
//cookieStore   *sessions.CookieStore
//viewBox       *rice.Box
//staticBox     *rice.Box
//db            *sqlx.DB
//servicesHub   *services.Hub
//)

//New ...
func New() *Hub {
	hub := new(Hub)

	// Load the controllers we specified in controllers/z_controller_loader.go
	// This is something similar to old days using web.xml in SpringFramework
	hub.LoadControllers()

	return hub
}

// Hub ...
type Hub struct {
	Controllers []interface{}
}

controllers/z_controller_binder.go – is where the RequestMapping interface declaration and implementation located. The BindRequestMapping method is responsible for the binding of the request url to the Golang multiplexer or router.

package controllers

import (
	"fmt"
	"log"
	"reflect"

	"github.com/go-zoo/bone"
)

// RequestMapping this interface will handle your request mapping declaration on each controller
type RequestMapping interface {
	RequestMapping(router *bone.Mux)
}

// RequestMapping implementation code of the interface
func (z *Hub) RequestMapping(router *bone.Mux, requestMapping RequestMapping) {
	requestMapping.RequestMapping(router)
}

// BindRequestMapping this method binds your request mapping into the mux router
func (z *Hub) BindRequestMapping(router *bone.Mux) {
	log.Println("Binding RequestMapping for:")
	for _, v := range z.Controllers {
		z.RequestMapping(router, v.(RequestMapping))
		rt := reflect.TypeOf(v)
		log.Println(rt)
	}

	log.Println("Binded RequestMapping are the following: ")
	for _, v := range router.Routes {
		for _, m := range v {
			log.Println(m.Method, " : ", m.Path)
		}
	}
	fmt.Println("")
}

controllers/z_controller_loader.go – is the place where we declare all our controllers. You might be wondering isn’t it the same issue were we declare all request url in main.go? Well, not quite. In this setup, we are only declaring here the controllers and not the request mappings. This causes less git merge conflicts when working with multiple developers.

package controllers

// LoadControllers add the controllers in this method
func (z *Hub) LoadControllers() {
	z.Controllers = make([]interface{}, 0)
	z.Controllers = append(z.Controllers, NewProductController())
	z.Controllers = append(z.Controllers, &CustomerController{})
}

The controllers can now use the interface and define their Request mappings.

package controllers

import (
	"github.com/go-zoo/bone"
	"github.com/johnpili/go-controller-request-mapping/models"
	"github.com/psi-incontrol/go-sprocket/sprocket"
	"github.com/shopspring/decimal"
	"net/http"
)

// ProductController ...
type ProductController struct {
	products []models.Product
}

// RequestMapping ...
func (z *ProductController) RequestMapping(router *bone.Mux) {
	router.GetFunc("/v1/get-products", z.GetProducts)
	router.GetFunc("/v1/get-product/:code", z.GetProduct)
}

// NewProductController ...
func NewProductController() *ProductController {
	//region SETUP DUMMY PRODUCTS
	p := make([]models.Product, 0)

	p = append(p, models.Product{
		Code:         "0001",
		Name:         "Product 1",
		PricePerUnit: decimal.NewFromFloat32(99.01),
	})

	p = append(p, models.Product{
		Code:         "0002",
		Name:         "Product 2",
		PricePerUnit: decimal.NewFromFloat32(25.99),
	})

	p = append(p, models.Product{
		Code:         "0003",
		Name:         "Product 3",
		PricePerUnit: decimal.NewFromFloat32(1.25),
	})

	p = append(p, models.Product{
		Code:         "0004",
		Name:         "Product 4",
		PricePerUnit: decimal.NewFromFloat32(2.50),
	})
	//endregion

	return &ProductController{
		products: p,
	}
}

// GetProducts ...
func (z *ProductController) GetProducts(w http.ResponseWriter, r *http.Request) {
	sprocket.RespondOkayJSON(w, z.products)
}

// GetProduct ...
func (z *ProductController) GetProduct(w http.ResponseWriter, r *http.Request) {
	code := bone.GetValue(r, "code")
	if len(code) == 0 {
		sprocket.RespondBadRequestJSON(w, nil)
		return
	}

	for _, item := range z.products {
		if item.Code == code {
			sprocket.RespondOkayJSON(w, item)
			return
		}
	}

	sprocket.RespondNotFoundJSON(w, nil)
}

Using this approach the main.go is now cleaner without those URL mappings.

package main

import (
	"flag"
	"github.com/go-zoo/bone"
	"github.com/johnpili/go-controller-request-mapping/controllers"
	"github.com/johnpili/go-controller-request-mapping/models"
	"github.com/psi-incontrol/go-sprocket/sprocket"
	"github.com/shopspring/decimal"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

var (
	configuration models.Config
)

func main() {
	pid := os.Getpid()
	err := ioutil.WriteFile("application.pid", []byte(strconv.Itoa(pid)), 0666)
	if err != nil {
		log.Fatal(err)
	}

	var configLocation string
	flag.StringVar(&configLocation, "config", "config.yml", "Set the location of configuration file")
	flag.Parse()

	err = sprocket.LoadYAML(configLocation, &configuration)
	if err != nil {
		log.Fatal(err)
	}

	decimal.MarshalJSONWithoutQuotes = true

	controllersHub := controllers.New()
	router := bone.New()
	//router.Get("/static/", staticFileServer)
	controllersHub.BindRequestMapping(router)

	// CODE FROM https://medium.com/@mossila/running-go-behind-iis-ce1a610116df
	port := strconv.Itoa(configuration.HTTP.Port)
	if os.Getenv("ASPNETCORE_PORT") != "" { // get enviroment variable that set by ACNM
		port = os.Getenv("ASPNETCORE_PORT")
	}

	//csrfProtection := csrf.Protect(
	//	[]byte(configuration.System.CSRFKey),
	//	csrf.Secure(false),
	//)

	httpServer := &http.Server{
		Addr:         ":" + port,
		ReadTimeout:  900 * time.Second,
		WriteTimeout: 900 * time.Second,
		Handler: router,
	}

	if configuration.HTTP.IsTLS {
		log.Printf("Server running at https://localhost:%s/\n", port)
		log.Fatal(httpServer.ListenAndServeTLS(configuration.HTTP.ServerCert, configuration.HTTP.ServerKey))
		return
	}
	log.Printf("Server running at http://localhost:%s/\n", port)
	//log.Fatal(http.ListenAndServe(":"+port, router)) // Start HTTP Server

	log.Fatal(httpServer.ListenAndServe())
}

Running the application will load all controllers and its request mappings

Conclusion

In conclusion, there might be other way to do this using framework like Gorilla or Gin that I am not aware however it is fun experience for me create a solution in Golang. I hope this blog helps developers coming from Java / Spring Framework.
As of this writing Golang version 1.16 is yet to be released and I’m looking forward for new features it will bring.

Source Code
https://github.com/johnpili/go-controller-request-mapping

Leave a Reply