From Java Spring to Go: HTTP Servers
/ 3 min read
Remember when you first discovered Spring’s @RestController
and thought, “Wow, this annotation is magic!”? Well, prepare yourself for Go’s take on HTTP handling, where the magic show turns into a delightfully straightforward rock concert.
The Tale of Two Servers
In Spring Boot land, we’re used to our controllers looking like a Christmas tree of annotations:
@RestController
@RequestMapping("/api/coffee")
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@GetMapping("/{id}")
public ResponseEntity<Coffee> getCoffee(@PathVariable Long id) {
return ResponseEntity.ok(coffeeService.getCoffee(id));
}
@PostMapping
@Valid
public ResponseEntity<Coffee> brewCoffee(@RequestBody CoffeeRequest request) {
// Spring does its validation dance
return ResponseEntity.status(201)
.body(coffeeService.brewCoffee(request));
}
}
Now, let’s see how Go handles this with Chi (my personal favorite after trying to juggle multiple routers like a caffeinated circus performer):
type CoffeeHandler struct {
service *CoffeeService
// No annotations were harmed in the making of this handler
}
func NewCoffeeHandler(service *CoffeeService) *CoffeeHandler {
return &CoffeeHandler{service: service}
}
func (h *CoffeeHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Get("/{id}", h.GetCoffee)
r.Post("/", h.BrewCoffee)
// Look Ma, readable routing!
return r
}
func (h *CoffeeHandler) GetCoffee(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
coffee, err := h.service.GetCoffee(id)
if err != nil {
// No more ResponseEntity.status().body()...
http.Error(w, "Coffee not found, try tea instead", http.StatusNotFound)
return
}
// Good ol' JSON encoding
json.NewEncoder(w).Encode(coffee)
}
Middleware: The Great Expectations
Spring Security’s annotations are like having an overprotective parent:
@PreAuthorize("hasRole('BARISTA')")
@PostMapping("/special-blend")
public ResponseEntity<Coffee> makeSpecialBlend() {
// Only certified baristas allowed!
}
Go’s middleware is more like having a bouncer who’s also a professional engineer:
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserKey)
if user == nil || !user.(User).IsAdmin {
http.Error(w, "Nice try, but only admins can make special blend",
http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// In your router
r.With(AdminOnly).Post("/special-blend", h.MakeSpecialBlend)
The Context Conundrum
Spring’s request scope is like a magical bag that somehow holds everything you need. Go’s Context is more like a well-organized toolbelt - everything is there, but you have to clip it on yourself:
type key string
const UserKey key = "user"
func (h *CoffeeHandler) BrewCoffee(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserKey).(User)
// If this panics, you probably forgot to attach your toolbelt (middleware)
var req BrewRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "I can't read your coffee order, try semaphore",
http.StatusBadRequest)
return
}
coffee, err := h.service.Brew(req, user)
if err != nil {
// Look at all these explicit error cases!
switch {
case errors.Is(err, ErrOutOfBeans):
http.Error(w, "We need more beans!", http.StatusServiceUnavailable)
case errors.Is(err, ErrTooMuchCaffeine):
http.Error(w, "Maybe switch to decaf?", http.StatusBadRequest)
default:
http.Error(w, "The coffee gods are angry", http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(coffee)
}
The Real Talk™
After building HTTP servers in both Spring and Go, here’s what I’ve learned:
- Spring’s annotations are like having autocorrect - great until they “correct” something into gibberish
- Go’s explicit error handling makes you face your API’s failure cases head-on
- Middleware composition in Go is like building with Lego - simple pieces that fit together perfectly
- The lack of magic means junior developers can actually understand what’s happening