Join us
When embarking on a web development journey, whether building a simple website or a complex application, developers often face a fundamental question: “Do I need a framework?” Let’s explore the need for frameworks and how they significantly impact the development process.
Think of a framework like a pre-built kitchen. It has all the essential tools you need — ovens for baking, mixers for blending, and cutting boards for chopping. You can focus on creating your culinary masterpieces (the microservices) without getting bogged down in building the kitchen from scratch (writing all the low-level code).
Frameworks are essential for several reasons:
Imagine constructing a building without a blueprint. Chaos would ensue! Similarly, frameworks provide a structured foundation for your application. They enforce conventions, ensuring consistency across your codebase. Developers know where to find specific functionality, making it easier for you and your team to collaborate and maintain the codebase.
Code is read many more times than it’s written. Frameworks promote clean, standardized code. Frameworks often come with best practices built in, guiding developers to write better, more secure, and more efficient code.
In this article, we will delve into the world of two popular Go frameworks, GoFr and Chi. We will explore their features, ease of use, and performance. By the end, you will have a clearer understanding of which framework might be the best fit for your next microservice project.
GoFr steps into the ring as an opinionated web framework for Golang. It champions the philosophy of “less is more” offering a streamlined approach to building production-grade microservices. GoFr prioritizes features critical for microservices running in real-world scenarios. Built-in support for various databases along with data migrations, messaging queues like Kafka, mqtt, pub-sub, built-in middlewares for authentication and authorization, and out of the box observability simplifies development and streamlines deployment. It offers features like structured logging, predefined metrics, and distributed tracing, allowing you to gain deep insights into your microservices’ health and performance.
Chi enters the ring as a lightweight and flexible router framework for building Golang HTTP services. Unlike GoFr, Chi takes a more minimalist approach, focusing on core routing functionalities and composability. Imagine Chi as a sleek chef’s knife. It’s a single, sharp tool that excels at a specific task — efficiently routing requests in your API. While Chi don’t natively support a lot of features like database connections, observability setup etc. but comes with set of additional packages to integrate.
Now that we’ve explored the philosophies of GoFr and Chi, let’s see how they translate into building a simple REST API. For this example, we’ll create a books
service that allows us to perform CRUD operations (Create, Read, Update, Delete) on books stored in a PostgreSQL database. Our goal is to compare the ease of ease of use, external database support, logging, and observability features provided by these frameworks.
Please find the full code for the following example here.
package handlers
import (
"database/sql"
"errors"
"strconv"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http"
"github.com/gofr-rest-api/models"
)
type handler struct{}
func New() handler {
return handler{}
}
// GetBookByID retrieves a book by its ID from the database
func (h handler) GetBookByID(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
var book models.Book
row := ctx.SQL.QueryRowContext(ctx, "SELECT id, title, author FROM books WHERE id = $1", id)
err := row.Scan(&book.ID, &book.Title, &book.Author)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
return nil, err
}
return book, nil
}
// GetAllBooks retrieves all books from the database
func (h handler) GetAllBooks(ctx *gofr.Context) (interface{}, error) {
var books []models.Book
rows, err := ctx.SQL.QueryContext(ctx, "SELECT id, title, author FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
// Iterate over the rows and append each book to the books slice
for rows.Next() {
var book models.Book
if err := rows.Scan(&book.ID, &book.Title, &book.Author); err != nil {
return nil, err
}
books = append(books, book)
}
if err := rows.Err(); err != nil {
return nil, err
}
return books, nil
}
// AddBook adds a new book to the database
func (h handler) AddBook(ctx *gofr.Context) (interface{}, error) {
var (
book models.Book
id int
)
err := ctx.Bind(&book)
if err != nil {
return nil, err
}
// Insert the new book into the database and return the generated ID
row := ctx.SQL.QueryRowContext(ctx, "INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id",
book.Title, book.Author)
err = row.Scan(&id)
if err != nil {
return nil, err
}
book.ID = id
return book, nil
}
// UpdateBook updates an existing book in the database
func (h handler) UpdateBook(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
var book models.Book
err := ctx.Bind(&book)
if err != nil {
return nil, err
}
result, err := ctx.SQL.ExecContext(ctx, "UPDATE books SET title = $1, author = $2 WHERE id = $3",
book.Title, book.Author, id)
if err != nil {
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
// Return a 404 error if no rows were affected (i.e., book not found)
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
book.ID, _ = strconv.Atoi(id)
return book, nil
}
// DeleteBook deletes a book from the database
func (h handler) DeleteBook(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
result, err := ctx.SQL.ExecContext(ctx, "DELETE FROM books WHERE id = $1", id)
if err != nil {
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
return nil, nil
}
package handlers
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/chi-rest-api/models"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
// New creates a new handler instance
type handler struct {
DB *gorm.DB
}
func New(db *gorm.DB) handler {
return handler{db}
}
// GetBookByID retrieves a book by its ID from the database
func (h handler) GetBookByID(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
http.Error(w, "Book not found", http.StatusNotFound)
return
}
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, book)
}
// GetAllBooks retrieves all books from the database
func (h handler) GetAllBooks(w http.ResponseWriter, r *http.Request) {
var books []models.Book
result := h.DB.Find(&books)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, books)
}
// AddBook adds a new book to the database
func (h handler) AddBook(w http.ResponseWriter, r *http.Request) {
var book models.Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
result := h.DB.Create(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusCreated, book)
}
// UpdateBook updates an existing book in the database
func (h handler) UpdateBook(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusNotFound)
return
}
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
book.ID, _ = strconv.Atoi(id) // Ensure ID remains unchanged
result = h.DB.Save(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, book)
}
// DeleteBook deletes a book from the database
func (h handler) DeleteBook(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusNotFound)
return
}
result = h.DB.Delete(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// respondJSON writes the response as JSON with the given status code
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
We’ve seen how GoFr and Chi differ in their approaches to building our sample REST API. Now, let’s explore some key considerations that go beyond the code itself:
ctx.Bind
and handles sending appropriate status codes by checking the returned error. Chi doesn’t natively support data binding or common HTTP status codes requiring developers to write additional code to manage these aspects.DEBUG
,ERROR
, INFO
etc. which helps in better monitoring and troubleshooting of applications. Chi does not have inbuilt support for logging, necessitating the integration of external libraries for logging purposes.
To evaluate the performance of both frameworks, we utilized the Bombardier
tool, a popular HTTP load testing framework. The tests were conducted to measure the handling of concurrent requests for both the Chi and GoFr implementations. The configuration for the performance tests was as follows:
/books/4
endpoint (simulating retrieving a book by ID) for each framework.bombardier -c 100 -n 10000 -m GET -l http://localhost:{port}/books/4
The test results reveal that GoFr outperformed Chi in handling concurrent requests. GoFr achieved a higher average requests per second (RPS) of 2551.29
compared to Chi’s 1936.44
. Additionally, GoFr exhibited lower average latency of 39.08ms
compared to Chi’s 51.26ms
. This translates to GoFr delivering responses faster on average. In terms of throughput, GoFr processed data at a rate of 1.23MB/s
compared to Chi’s 441.98KB/s.
This head-to-head showdown has explored the strengths and weaknesses of GoFr and Chi for building microservices in GoLang. We’ve seen how GoFr’s opinionated approach simplifies development with built-in features for database interactions, routing, and more. Chi, on the other hand, provides a more lightweight and customizable routing solution.
Performance under pressure emerged as a key differentiator. Our load tests revealed that GoFr handled concurrent requests more efficiently, achieving higher request throughput and lower response latency compared to Chi.
But performance isn’t the only factor to consider. GoFr goes beyond routing, offering out-of-the-box features like:
GoFr offers a compelling combination of ease of use, built-in features, and impressive performance. Its streamlined development experience and built-in functionalities like database support and observability can empower you to build robust and scalable microservices in Golang.
Do checkout GoFr and it’s Github Repo and support it by giving it a ⭐.
Thank you for reading, Happy coding, and may your microservices thrive! 🚀👏
Join other developers and claim your FAUN account now!
SDE 2, gofr.dev
@umang01-hashInfluence
Total Hits
Posts
Only registered users can post comments. Please, login or signup.