Lately I’ve been working with two different technology stacks almost in parallel, in both cases we were using them to develop REST services.
During this time I’ve come up with some conclusions and opinions I’d like to share.
A disclaimer, few months ago, I had several years of experience with Java and 0 days of professional experience with Golang.
Actual project examples
Few months ago I created an API to extract and structure COVID-19 data from ECDC website. I developed it in Spring Boot (REST).
Few months later I had the luck of work on my first professional project in Go and I decided to create a port of the API to extract COVID-19 data in Go, just for learning and for fun.
Now we have two REST services, almost functionally identical, but developed in two different tech stacks, so we can easily compare some relevant aspects of both.
Java + Spring Boot (REST) | Go + Gin framework |
---|---|
Note
|
I actually created that COVID-19 data REST API to be the data source for the COVID19-Stats App, a PWA built with Svelte, but that’s another topic. |
The Ecosystems
If you want to create a REST service just in plain Java you will have extra work to do, in Golang a little bit less. That’s why we use framework, because they’ve already solved many common problems for us.
For this comparison I am going to use Spring Boot (REST) for Java and Gin framework for Go, but in both languages there are a lot of production ready nice options.
Routing
Go - Without framework
Go uses the concept of HTTP multiplexer or router. You can specify routes using patterns and link those routes to handlers. The router will decide which handler has to execute the request based on the path received.
package main
import (
"log"
"net/http"
)
func main() {
router := http.NewServeMux()
router.Handle("/redirect", http.RedirectHandler("https://carlosvin.github.io/", 307))
router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
})
log.Println("Listening...")
http.ListenAndServe(":3000", router)
}
Source code is already quite simple, but there might more complex routing use cases.
Go - Gin Framework
Happily there are frameworks that help us to keep our base code simple, for example when we need to extract path parameters, which is quite common use case in REST APIs, we can use a routing library, I’ve used Gorilla Mux and Gin framework and I liked more Gin framework.
import (
"github.com/carlosvin/covid-rest-go/handlers"
"github.com/carlosvin/covid-rest-go/readers"
"github.com/gin-gonic/gin"
)
func main() {
// ...
r := gin.Default()
r.GET("/countries", router.Countries)
r.GET("/countries/:code", router.Country)
r.GET("/countries/:code/dates", router.CountryDates)
r.GET("/countries/:code/dates/:date", router.CountryDate)
r.Run()
}
func (r *routerImpl) Countries(c *gin.Context) {
c.JSON(200, r.countries())
}
Java + Spring.io
The Spring Boot (REST) is based on the concept of Controller, it is implemented using annotations on the class and methods.
@Validated
@RestController (1)
@RequestMapping("/countries") (2)
public class CountriesController {
// Some source code is not shown, you can find the complete example in the repository
@GetMapping("/{country}/dates/{isoDateStr}") (3)
public DateStatsDto getDateByCountry(@Size(min = 2, max = 2) @PathVariable String country, @Size(min = 10, max = 20) @PathVariable String isoDateStr) throws NotFoundException {
return new DateStatsDto(service.getDate(country, DateUtils.convert(isoDateStr)));
}
}
-
Declare the class as Controller so it is registered in Spring Boot (REST)
-
Controller base path definition
-
Handler definition for a nested path under the main controller path. Spring Boot (REST) makes easy to extract path variables defined in the route, you can directly use them as method arguments.
Validations
Go - Gin Framework
Gin framework uses an external validation package validator, besides that it is fully integrated with Gin framework.
type User struct {
Name string `validate:"required"` (1)
Email string `validate:"required,email"`
}
err := validate.Struct(user) (2)
validationErrors := err.(validator.ValidationErrors) (3)
Java + Spring.io
You can enable the validation in the controller level, then in the handlers you can also specify the type of validation. Let’s explain it using the previous example:
@Validated (1)
@RestController
@RequestMapping("/countries")
public class CountriesController {
// Some source code is not shown, you can find the complete example in the repository
@GetMapping("/{country}/dates/{isoDateStr}")
public DateStatsDto getDateByCountry(
@Size(min = 2, max = 2) @PathVariable String country, (2)
@Size(min = 10, max = 20) @PathVariable String isoDateStr) throws NotFoundException {
return new DateStatsDto(service.getDate(country, DateUtils.convert(isoDateStr)));
}
}
-
Declare the class as Controller so it is registered in Spring Boot (REST)
-
@Size
validates that the input argument country has 2 characters
The validation system is more powerful than you can see in this code snippet, for example adding @Valid
annotation opens the door to complex types validation.
Filtering and Middleware
Different approaches, pretty much the same end result.
I will elaborate this topic in following days.
Dependency injection / IoC
Spring IoC
Spring IoC is the most complete and powerful systems I’ve ever used for IoC, actually, the first time I used Spring professionally was just to deal with IoC. It supports XML configuration files or Java annotations, I like annotations more, here a simple example from Spring IoC documentation:
@Repository
public class JpaMovieFinder implements MovieFinder { (1)
// implementation elided for clarity
}
//
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired (2)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
-
JpaMovieFinder
is instantiated by Spring IoC -
With
@Autowired
annotation Spring IoC knows that has to injectmovieFinder
argument. It should be a class implementingMovieFinder
Go
Neither Go nor Gin framework has any IoC solution, but you can still apply Dependency Injection technique to decouple your components and improve the testability of your system.
package main
import "fmt"
// Greeter interface to greet the caller
type Greeter interface {
greet()
}
type greeterHello struct{}
func (g *greeterHello) greet() { (3)
fmt.Println("Hello!")
}
type greeterHi struct{}
func (g *greeterHi) greet() { (4)
fmt.Println("Hi!")
}
// App Application representation
type App struct {
greeters []Greeter (1)
}
func (app *App) startup() {
for _, v := range app.greeters {
v.greet()
}
}
func main() {
greeters := []Greeter{ (2)
&greeterHello{},
&greeterHi{},
&greeterHello{}}
app := &App{greeters}
app.startup()
}
/*
<1> `App` accepts an array of `Greeter`
<2> During `App` instantiation we pass different implementations of `Greeter`
<3> Greeter implementation that prints *Hello!*
<4> Greeter implementation that prints *Hi!*
*/
It is more verbose, but there is an advantage, there is nothing hidden, everything is explicit and you have full control of instantiation order.
As soon as you use Dependency Injection, I don’t have any strong opinion about using IoC system or doing Dependency Injection manually.
Testing
Unit tests
For unit tests there are no big differences.
Go comes with a standard library for testing and benchmarking.
For Java there are many well-known unit testing frameworks, but Spring already has quite big support for unit testing.
Integration tests
Go
There are no support for Integration Tests in Go, you will have to implement everything by yourself, although it is not difficult, here you can find a simple example.
Spring
On the other hand, Spring has a great testing support.
I’ve used MockMvc in the covid-rest project.
@Autowired
private MockMvc mockMvc; (1)
@Test
void getCountries() throws Exception {
this.mockMvc.perform(get("/countries")) (2)
.andDo(print()).andExpect(status().isOk()) (3)
.andExpect(jsonPath("$.*", hasSize(144)))
.andExpect(jsonPath("$.ES.confirmedCases",comparesEqualTo(9191)))
.andExpect(jsonPath("$.ES.deathsNumber", comparesEqualTo(309)))
.andExpect(jsonPath("$.ES.countryCode", comparesEqualTo("ES")))
.andExpect(jsonPath("$.ES.countryName", comparesEqualTo("Spain")))
.andExpect(jsonPath("$.ES.path", comparesEqualTo("/countries/ES")))
.andExpect(jsonPath("$.VC.countryName", comparesEqualTo("Saint Vincent and the Grenadines")))
.andDo(document("countries/list", preprocessResponse(prettyPrint(), new CropPreprocessor())));
}
-
The Spring test runner injects the MockMvc object.
-
We use MockMvc to call to the endpoint we have created.
-
Then we validate the endpoint response: status code and body.
Performance
Besides the languages specific differences, the main difference is the performance. The CPU consumption in Go is smaller, but about the memory the difference is really significant, the order of 30 times smaller fingerprint.
Memory
Here I’ve found a surprising difference, just by checking the memory consumption in my laptop.
-
Gin framework
15.6MB
-
Spring Boot (REST)
465.9MB
Speed
Following the TechEmpower benchmarks:
-
Gin framework is in 193 position, 9.9%.
-
Spring Boot (REST) is in 284 position, 4%.
Following the The Benchmarker results:
-
Gin framework: position 33.
-
Spring Boot (REST): position 68.
Conclusions
If I were you, I’d choose Go if:
-
If you value the explicit over implicit, keep in mind that there is a cost, you will most likely have to write more lines of code.
-
If you value the simplicity, Go has a quite reduced set of keywords, which reduces the learning curve and simplifies the code reviews.
-
If RAM memory usage is critical for your project, actually I’d just keep away from Spring Boot (REST).
-
If the project you are going to work on is a distributed system, specially if it is based on HTTP.