In this article
How to Build a Rest API in Golang (Detailed Tutorial)
William Imoh Improve this Guide
Building a REST API in Go offers a blend of high performance, ease of deployment, and scalability. Go’s efficient runtime and built-in concurrency model make it an ideal choice for building applications that handle large volumes of requests while still maintaining low latency. With a robust standard library and supportive ecosystem, you can quickly implement REST functionality into your applications.
Since its official release in 2009, Go has gained massive adoption and is gradually becoming the go-to technology for building applications, from small-scale to mission-critical systems. Its adoption surged even further when industry leaders used it to power containerization and orchestration tools like Docker and Kubernetes. Whether you’re building a small service or a mission-critical application, Go provides a powerful foundation for REST API development.
Given these advantages, let’s put Go’s capabilities into practice. In this guide, you will build a basic bookkeeping management API that supports create, read, update, and delete (CRUD) functionalities. You’ll also write unit tests for these functionalities to ensure they work as intended, implement authentication to secure the APIs, and create comprehensive documentation.
Let’s get started building the application.
Step 1: Project set up
To get started, use your terminal to create a project directory.
mkdir go_book_api && cd go_book_api
These commands create a directory called go_book_api
and navigate into it.
Next, initialize a Go module within the project.
go mod init go_book_api
This command creates a go.mod
file for tracking the project dependencies.
Finally, install the required dependencies.
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlite
go get -u github.com/joho/godotenv
Let’s look at what these dependencies are and what they are used for:
Gin is a framework for building web applications.
Gorm is a Go-based Object Relational Mapper (ORM). ORMs make it easy to query and manipulate data from a database. The installation also includes both Postgres and SQLite drivers. You’ll use the Postgres driver to interact with your application data, while SQLite will mock your database interaction when writing unit tests.
godotenv is a library for loading environment variables.
Set up the project database To save your API data, you need to set up your project database on Neon. Neon is a fully managed serverless PostgreSQL database with a generous free tier you can start with.
To set up your database, log into your Neon console and create a project.
On successful creation of the project, copy the database URL.
Alternatively, you can use any managed Postgres service or a local Postgres instance.
Lastly, create .env
in your project directly and add the copied database URL.
DB_URL=<REPLACE THIS WITH YOUR DATABASE URL>
Step 2: Structuring the project
While Go doesn’t enforce how you should structure your project, it is essential you have a good structure when building an application with Go. It makes your codebase easy to maintain and clear for others to understand.
To structure your project, create an api
, cmd
, and tests
folders.
api
is for organizing API development-related files.
cmd
is used to organize the application entry point. This is a convention within the Go community.
tests
are for organizing unit tests.
Step 3: Implement CRUD operations for the bookkeeping application
Create an api/model.go
file to structure the application’s data and response.
package api
import "github.com/gin-gonic/gin"
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Author string `json:"author"`
Year int `json:"year"`
}
type JsonResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Data any `json:"data"`
}
func ResponseJSON(c *gin.Context, status int, message string, data any) {
response := JsonResponse{
Status: status,
Message: message,
Data: data,
}
c.JSON(status, response)
}
The code snippet above creates a Book
and JsonResponse
structs with the required properties using struct tags (e.g., json:"title"
) and a helper function ResponseJSON
to manage API responses.
The struct tag lets you send responses that fit into the JSON naming convention.
Create a new book
To create a new book handler function, create an api/handlers.go
file that will contain the application business logic. It’s important to separate the business logic from the data model, as it helps deliver scalable, maintainable, and testable code.
package api
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func InitDB() {
err := godotenv.Load()
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
dsn := os.Getenv("DB_URL")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// migrate the schema
if err := DB.AutoMigrate(&Book{}); err != nil {
log.Fatal("Failed to migrate schema:", err)
}
}
func CreateBook(c *gin.Context) {
var book Book
//bind the request body
if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return
}
DB.Create(&book)
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
}
The code snippet declares a global variable DB
that holds the database connection and is then initialized inside the InitDB
function. The InitDB
function loads the environment variable to connect to the database and performs automatic schema migration. Finally, create a CreateBook
handler that uses the DB
variable to create a new book, handle errors, and return the appropriate response using the ResponseJSON
helper.
When a function, type, or variable is declared with an initial capital letter, it is exported. This means it is visible and can be accessed by other packages.
Getting the list of books
To retrieve the list of books, create a GetBooks
handler that uses the DB
variable to get the list of available books inside the database.
func GetBooks(c *gin.Context) {
var books []Book
DB.Find(&books)
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
}
Get a single book
To retrieve a book, create a GetBook
handler that uses the DB
variable to get a book matching the specified ID and returns the appropriate response.
func GetBook(c *gin.Context) {
var book Book
if err := DB.First(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return
}
ResponseJSON(c, http.StatusOK, "Book retrieved successfully", book)
}
Update a book
To update a book, create an UpdateBook
handler that uses the DB
variable to get a book matching the specified ID, updates it, and returns the appropriate response.
func UpdateBook(c *gin.Context) {
var book Book
if err := DB.First(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return
}
// bind the request body
if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return
}
DB.Save(&book)
ResponseJSON(c, http.StatusOK, "Book updated successfully", book)
}
Delete a book
To delete a book, create a DeleteBook
handler that uses the DB
variable to get a book matching the specified ID, deletes it, and returns the appropriate response.
func DeleteBook(c *gin.Context) {
var book Book
if err := DB.Delete(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return
}
ResponseJSON(c, http.StatusOK, "Book deleted successfully", nil)
}
Putting it all together
With the handlers set up, you need to create the application entry point and specify the application routes. To do this, create a cmd/main.go
file and add the code snippet below:
package main
import (
"go_book_api/api"
"github.com/gin-gonic/gin"
)
func main() {
api.InitDB()
r := gin.Default()
//routes
r.POST("/book", api.CreateBook)
r.GET("/books", api.GetBooks)
r.GET("/book/:id", api.GetBook)
r.PUT("/book/:id", api.UpdateBook)
r.DELETE("/book/:id", api.DeleteBook)
r.Run(":8080")
}
The snippet above creates a main
function that initializes the database, specifies the routes with the associated handlers, and runs the application on port 8080
.
Step 4: Testing the REST APIs
To test the APIs, you need a Postman or any API testing application of your choice.
In your terminal, start the development server by running the command.
go run cmd/main.go
Then, use the API testing client of your choice to test out the endpoints as shown below:
Create a book endpoint
Use the POST
method and pass in the required data to create a new book.
curl --location 'localhost:8080/book' \
--header 'Content-Type: application/json' \
--data '{
"title": "Go by Example: Programmer'\''s guide to idiomatic and testable code",
"author": "Inanc Gumus",
"year": 2021
}'
Get the list of books
Use the GET
method to get the list of existing books in the database.
curl --location 'localhost:8080/books'
Get a book endpoint
Use the GET
method and add the ID to get the details of an existing book.
curl --location 'localhost:8080/book/3'
Update a book endpoint
Use the PUT
method and specify the ID and the data to update an existing book.
curl --location --request PUT 'localhost:8080/book/3' \
--header 'Content-Type: application/json' \
--data '{
"title": "Go by Example: Programmer'\''s guide to idiomatic and testable code - Updated",
"author": "Inanc Gumus",
"year": 2023
}'
Delete a book
Make a DELETE
request with an already created book ID.
curl --location --request DELETE 'localhost:8080/book/3'
You can also query your Neon database to see the latest changes.
Step 5: Unit testing the API handler functions
Go comes with a testing package that makes it easy to write unit tests, integration tests, functional tests, and end-to-end tests. To begin, create a tests/main_test.go
file and add the code snippet below:
package tests
import (
"bytes"
"encoding/json"
"go_book_api/api"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB() {
var err error
api.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
panic("failed to connect test database")
}
api.DB.AutoMigrate(&api.Book{})
}
func addBook() api.Book {
book := api.Book{Title: "Go Programming", Author: "John Doe", Year: 2023}
api.DB.Create(&book)
return book
}
The snippet above creates helper functions setupTestDB
and addBook
, which initialize a new SQLite in-memory database (:memory:
) specifically for testing purposes and insert a new book record into the test database, respectively.
Ending the test name with
_test.go
is a standard naming convention in the Go ecosystem.
Create a new book unit test
Create a TestCreateBook
test function that sets up a mock HTTP server using Gin, sends a POST request to create a book, and checks that the response matches the expected outcome and the HTTP status code defined in the CreateBook
handler.
func TestCreateBook(t *testing.T) {
setupTestDB()
router := gin.Default()
router.POST("/book", api.CreateBook)
book := api.Book{
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
}
jsonValue, _ := json.Marshal(book)
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusCreated {
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil {
t.Errorf("Expected book data, got nil")
}
}
Get the list of books unit test
Similar to the TestCreateBook
test function, create a TestGetBooks
that tests the GetBooks
handler functionality of retrieving a list of books via a GET request.
func TestGetBooks(t *testing.T) {
setupTestDB()
addBook()
router := gin.Default()
router.GET("/books", api.GetBooks)
req, _ := http.NewRequest("GET", "/books", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if len(response.Data.([]interface{})) == 0 {
t.Errorf("Expected non-empty books list")
}
}
Get a book unit test
Create a TestGetBook
test function that tests the GetBook
handler functionality of retrieving a single book that matches the specified ID via a GET request.
func TestGetBook(t *testing.T) {
setupTestDB()
book := addBook()
router := gin.Default()
router.GET("/book/:id", api.GetBook)
req, _ := http.NewRequest("GET", "/book/"+strconv.Itoa(int(book.ID)), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["id"] != float64(book.ID) {
t.Errorf("Expected book ID %d, got nil or wrong ID", book.ID)
}
}
Update a book unit test
Create a TestUpdateBook
test function that tests the UpdateBook
handler functionality of updating an existing book through a PUT request.
func TestUpdateBook(t *testing.T) {
setupTestDB()
book := addBook()
router := gin.Default()
router.PUT("/book/:id", api.UpdateBook)
updateBook := api.Book{
Title: "Advanced Go Programming", Author: "Demo Author name", Year: 2021,
}
jsonValue, _ := json.Marshal(updateBook)
req, _ := http.NewRequest("PUT", "/book/"+strconv.Itoa(int(book.ID)), bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["title"] != "Advanced Go Programming" {
t.Errorf("Expected updated book title 'Advanced Go Programming', got %v", response.Data)
}
}
Delete a book unit test
Create a TestDeleteBook
test function that tests the DeleteBook
handler functionality of deleting an existing book through a DELETE request.
func TestDeleteBook(t *testing.T) {
setupTestDB()
book := addBook()
router := gin.Default()
router.DELETE("/book/:id", api.DeleteBook)
req, _ := http.NewRequest("DELETE", "/book/"+strconv.Itoa(int(book.ID)), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Message != "Book deleted successfully" {
t.Errorf("Expected delete message 'Book deleted successfully', got %v", response.Message)
}
//verify that the book was deleted
var deletedBook api.Book
result := api.DB.First(&deletedBook, book.ID)
if result.Error == nil {
t.Errorf("Expected book to be deleted, but it still exists")
}
}
Finally, you can run your test in the development environment using the command below:
GIN_MODE=release go test tests/main_test.go -v
GIN_MODE=release
is a command in Gin that tells it to mimic the production environment and run the test in an optimized mode.
Adding authentication to the REST APIs
An integral part of building a robust API is the authentication mechanism. It verifies the identity of a user or system using your API. When you have authentication set up in your API, you can:
- Prevent unauthorized access to sensitive resources or data.
- Control the level of access.
- Track and log who accessed the API and what actions they performed.
- Tailored responses or experiences for authenticated users.
In Go, you can implement authentication using any of the following methods:
- JSON Web Tokens (JWT).
- OAuth 2.0.
- Session-based authentication.
- API Keys.
For this project, you’ll use JWT (JSON Web Tokens) to secure the API. The process begins with a user providing their credentials, such as a username and password. The system then verifies these credentials and issues a token, which the user can use to access protected endpoints.
To get started, install the JWT package.
go get -u github.com/golang-jwt/jwt/v5
Create a middleware
Middlewares are functions or modules that intercept and process HTTP requests before they reach the final route handler or after the response leaves the handler. In this case, you want to check that the request has the right token before accessing any of the endpoints.
To do this, first update your .env
file with a secret token. The token ensures the security, authenticity, and integrity of the tokens.
SECRET_TOKEN=<REPLACE WITH A RANDOM LONG STRING>
Next, create a middleware.go
file inside the api
folder and add the snippet below:
package api
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// Secret key for signing JWT
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
ResponseJSON(c, http.StatusUnauthorized, "Authorization token required", nil)
c.Abort()
return
}
// parse and validate the token
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
ResponseJSON(c, http.StatusUnauthorized, "Invalid token", nil)
c.Abort()
return
}
// Token is valid, proceed to the next handler
c.Next()
}
}
The snippet above creates a JWTAuthMiddleware
function that uses the JWT package to intercept incoming requests and validate their JWT tokens before allowing the request to proceed to the handler.
Next, update the handlers.go
file with a GenerateJWT
function that generates a JWT token-based default username and password.
package api
import (
//other imports
"github.com/golang-jwt/jwt/v5"
)
func GenerateJWT(c *gin.Context) {
var loginRequest LoginRequest
if err := c.ShouldBindJSON(&loginRequest); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid request payload", nil)
return
}
if loginRequest.Username != "admin" || loginRequest.Password != "password" {
ResponseJSON(c, http.StatusUnauthorized, "Invalid credentials", nil)
return
}
expirationTime := time.Now().Add(15 * time.Minute)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": expirationTime.Unix(),
})
// Sign the token
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
ResponseJSON(c, http.StatusInternalServerError, "Could not generate token", nil)
return
}
ResponseJSON(c, http.StatusOK, "Token generated successfully", gin.H{"token": tokenString})
}
In a real-world application, the credentials used to generate the token will be specific to users and not the default ones.
Lastly, update the main.go
file inside the cmd
folder to add a public route to generate a token and use the middleware to protect the API routes.
package main
import (
"go_book_api/api"
"github.com/gin-gonic/gin"
)
func main() {
api.InitDB()
r := gin.Default()
// Public routes
r.POST("/token", api.GenerateJWT)
// protected routes
protected := r.Group("/", api.JWTAuthMiddleware())
{
protected.POST("/book", api.CreateBook)
protected.GET("/books", api.GetBooks)
protected.GET("/book/:id", api.GetBook)
protected.PUT("/book/:id", api.UpdateBook)
protected.DELETE("/book/:id", api.DeleteBook)
}
r.Run(":8080")
}
Testing the REST API
If you restart the server and make a request to any of the endpoints without a token, you’ll get unauthorized.
curl --location 'localhost:8080/books'
To test the API with a token, make a request to the token generation endpoint.
curl --location 'localhost:8080/token' \
--header 'Content-Type: application/json' \
--data '{
"username": "admin",
"password": "password"
}'
Then, use the generated token to make a request.
curl --location 'localhost:8080/books' \
--header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzM3MDU3Njl9.ULspm6GR9Q0zqZWHifdFEeLZqgtw7k2FDDhSOpwcw4U'
Updating the unit test
Since the API route is now protected, you also need to update the unit test to capture the authentication mechanism.
package tests
import (
// other imports
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
func setupTestDB() {
// setupTestDB goes here
}
func addBook() api.Book {
// add book code goes here
}
func generateValidToken() string {
expirationTime := time.Now().Add(15 * time.Minute)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": expirationTime.Unix(),
})
tokenString, _ := token.SignedString(jwtSecret)
return tokenString
}
func TestGenerateJWT(t *testing.T) {
router := gin.Default()
router.POST("/token", api.GenerateJWT)
loginRequest := map[string]string{
"username": "admin",
"password": "password",
}
jsonValue, _ := json.Marshal(loginRequest)
req, _ := http.NewRequest("POST", "/token", bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["token"] == "" {
t.Errorf("Expected token in response, got nil or empty")
}
}
The snippet above imports the required dependency, creates a generateValidToken
helper and TestGenerateJWT
function that generates a token and tests the token generation endpoint, respectively.
Next, use the generateValidToken
helper function to modify the request object for each test by adding authentication to the route, generating a token, and adding it to the request header. For example, the test for creating a book will look like this:
func TestCreateBook(t *testing.T) {
setupTestDB()
router := gin.Default()
protected := router.Group("/", api.JWTAuthMiddleware()) // add
protected.POST("/book", api.CreateBook) // add
token := generateValidToken() // add
book := api.Book{
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
}
jsonValue, _ := json.Marshal(book)
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
req.Header.Set("Authorization", token) // add
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusCreated {
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
}
var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil {
t.Errorf("Expected book data, got nil")
}
}
Adding documentation to the REST APIs
Documentation is an integral part of your APIs. It helps you communicate intent and help others understand, maintain, and utilize your APIs effectively. Documentation can be in the form of the following:
- Technical documentation that guides developers on APIs, configurations, and architecture of the system.
- User manual that helps end-users understand how to use the product.
- Internal documentation that captures internal processes, workflows, and other operational details.
- Code comments that provide inline explanations of specific sections of the codes.
In Go, you have access to a variety of tools when it comes to generating, maintaining, and displaying documentation. Below are some of the widely used ones:
- GoDoc
- pkg.go.dev
- go doc
- godocdown
- Swagger/OpenAPI
In this project, you’ll use GoDoc to add documentation.
To get started, first update the Go version based on your operating system. Then, install the GoDoc package using the command below:
go get golang.org/x/tools/cmd/godoc
Next, modify the handlers.rs
file and add comments to describe what each helper function and handlers do.
/*
Package api provides RESTful handlers and middleware for managing a library of books.
It includes functionality for creating, reading, updating, and deleting books, as well as generating JWT tokens for authentication.
*/
package api
import (
"log"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
// InitDB initializes the database connection using environment variables.
// It loads the database configuration from a .env file and migrates the Book schema.
func InitDB() {
err := godotenv.Load()
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
dsn := os.Getenv("DB_URL")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Migrate the schema
if err := DB.AutoMigrate(&Book{}); err != nil {
log.Fatal("Failed to migrate schema:", err)
}
}
// CreateBook handles the creation of a new book in the database.
// It expects a JSON payload with book details and responds with the created book.
func CreateBook(c *gin.Context) {
var book Book
if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return
}
DB.Create(&book)
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
}
// GetBooks retrieves all books from the database.
// It responds with a list of books.
func GetBooks(c *gin.Context) {
var books []Book
DB.Find(&books)
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
}
// other handlers goes below
Finally, test the documentation by running the command below:
godoc -http=:6060
Open your browser and navigate to the documentation URL:
http://localhost:6060/pkg/go_book_api/api/
Next steps
Go’s simplicity, built-in concurrency, cloud-native features, and performance make it a popular choice among developers. The growing ecosystem and community support also ensure that you have the right tools and libraries to build and maintain RESTful APIs efficiently and any other application you want to build.
Apart from the Gin framework, Go also has popular web frameworks like Echo, Revel, and many more for building APIs. To stay up-to-date with the latest changes, check out the Go roadmap.