Join us

GoFr vs Chi: A Comparison of Golang Frameworks

GoFr vs Chi

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.

Introduction to GoFr & Chi Frameworks:

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.

Building a RESTful Service with GoFr and Chi

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.

GoFr Implementation

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
}

Chi Implementation

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)
}
}

Observations: Beyond the Code

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:

  • Database Interaction: GoFr Provides inbuilt support for database interaction. The connection can be easily set up by providing suitable configurations, simplifying the development process. In Chi, users need to manually set up database connections and handle interactions. It lacks native support for database connectivity.
  • Data Binding and HTTP Status Codes: GoFr automatically binds data using 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.
  • Logging: GoFr offers structured logging with various log levels like 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.
  • Code Clarity: Both GoFr and Chi promote clean code principles. GoFr’s context-based approach and built-in functionalities can lead to concise code. Chi’s reliance on external libraries might require additional context to understand database interactions within the handler functions.

Testing the Performance of REST API’s

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:

  • Machine: MacBook Pro with M2 chip, 8 GB RAM
  • Go Version: go1.22.0 (darwin/arm64)
  • Chi Version: github.com/go-chi/chi/v5 v5.0.12
  • GoFr Version: gofr.dev v1.9.0
  • Test Scenario: 100 concurrent connections sending 10,000 GET requests to the /books/4 endpoint (simulating retrieving a book by ID) for each framework.
  • Server Ports: GoFr server running locally on port 9000; Chi server running on port 3000

Performance Test Command:

bombardier -c 100 -n 10000 -m GET -l http://localhost:{port}/books/4

Results

Analysis

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.

Conclusion

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:

  • Database Support: Streamlined database interactions with built-in SQL functionality.
  • Observability: Gain valuable insights into your application’s health with pre-configured metrics, distributed tracing, and dynamic logging.
  • Standardization and Readability: Maintain a clear and consistent codebase with GoFr’s defined conventions.

Ready to Give GoFr a Try?

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! 🚀👏


Only registered users can post comments. Please, login or signup.

Start blogging about your favorite technologies, reach more readers and earn rewards!

Join other developers and claim your FAUN account now!

Avatar

Umang Mundhra

SDE 2, gofr.dev

@umang01-hash
Accomplished skilled software developer with a strong background in backend development. Currently employed as Software Development Engineer 2 (Golang) at gofr.dev!
User Popularity
6

Influence

579

Total Hits

2

Posts