Controller RequestMapping 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.

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.

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

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("")
}

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

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)
}

The main.go is cleaner

Running the application will load all controllers and its request 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

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

Originally published at https://johnpili.com on February 6, 2021.

--

--

--

I am currently working as a software development manager in Malaysia. I manage and help my team with technical software design and implementation.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

7 Things Every Translation Management Software Should Have

I/O scheduling in Linux

Building a starter FPGA Mining Rig for noobs!

Command Line Notes

Farewell, macOS!

AWS Introduction | what is AWS

Complete Guide To NFT Drops On WAX

Please, don’t let your software SUC!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
John Pili

John Pili

I am currently working as a software development manager in Malaysia. I manage and help my team with technical software design and implementation.

More from Medium

Golang Intellij Idea setup

The main idea of ​​implementing a binary tree with Golang

Testing in Go

Testing in Golang(Part 1)— Unit Tests