Controller RequestMapping in Golang

John Pili
5 min readFeb 6, 2021

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.

--

--

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.