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:
- Ability to define URL mappings to its corresponding controller instead of putting on main.go or other place
- Load all the URL mappings from all controllers using a bootstrapping mechanism
- 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