Introduction
Go community believes that we need no framework to develop web services. And I agree with that. When you work without using any framework, you learn the ins and outs of development. But once you learn how things work, you must reside in a community and don't reinvent the wheel.
I have created POCs in Go before and I had to deal with HTTP headers, serializing/deserializing, error handling, database connections, and whatnot. But now I've decided to join the Gin community as it is one of the widely accepted in the software development community.
Although I'm writing this post as a standalone article and would keep things simple here. But I want to continue building on these examples to have authentication, authorization, databases (including Postgres, ORM), swagger, and GraphQL covered. Will be interlinking the posts when I create them.
Why Gin
There are numerous reasons you may want to use Gin. If you ask me, I'm a big fan of Gin's sensible defaults.
Another thing I like about Gin is that it's an entire framework. You don't need a separate multiplexer and a separate middleware library and so on. On top of that, there are many common things already available that you don't have to reinvent. It does enhance our productivity. Although I'm not using it in production, I already have started to feel it.
Hello World in Gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
router.GET("/ping", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.Run()
}
Let's get familiar with Gin a little bit.
router := gin.New()
This creates an instance of Engine
. gin.Engine
is the core of gin. It also acts as a router and that is why we have put that Engine instance into a variable called router
.
router.GET("/ping", func(ctx *gin.Context) {
This binds a route /ping
to a handler. In the example above, I've created an anonymous function, but it could be a separate function as well. The thing to note here is the parameter to this function. *gin.Context
. Context
is yet another important construct besides Engine. Context
has almost 100 methods attached to it. A newcomer should be spending most of their time understanding this Context
struct and their methods.
Let's now look at the next few lines:
ctx.JSON(http.StatusFound, gin.H{
"message": "pong",
})
One of the *gin.Context
method is JSON
. This method is used to send a JSON response to the client. Meaning that it automatically sets response's Content-Type
to application/json
. JSON method takes an HTTP status code and a map of response. gin.H
is an alias to map[string]interface{}
. So basically we can create an object which can have string key and whatever value we want.
Next is:
router.Run()
Engine.Run simply takes our router along with the route handler and binds it to http.Server. The default port is 8080
, but if you want, you can have another address passed here.
The Book Store API
I've already done a POC on bookstore before, at that time, I wanted to prototype a connection between MongoDB and Go. But this time my goal is to have Postgres and GraphQL incorporated.
So first of all, I'd like you to set up a directory structure like this:
$ tree
.
├── db
│ └── db.go
├── go.mod
├── go.sum
├── handlers
│ └── books.go
├── main.go
└── models
└── book.go
And let's start filling up those files.
db/db.go
package db
import "github.com/santosh/gingo/models"
// Books slice to seed book data.
var Books = []models.Book{
{ISBN: "9781612680194", Title: "Rich Dad Poor Dad", Author: "Robert Kiyosaki"},
{ISBN: "9781781257654", Title: "The Daily Stotic", Author: "Ryan Holiday"},
{ISBN: "9780593419052", Title: "A Mind for Numbers", Author: "Barbara Oklay"},
}
Instead of going into the complexity of setting up a database right now, I've decided to use an in-memory database for this post. In this file, I've seeded db.Books
slice with some books.
If models.Book
makes, you curious, the next file is that only.
models/book.go
package models
// Book represents data about a book.
type Book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
Nothing fancy here, we only have 3 fields as of now. All of them are strings and with struct tags.
Let us see our main.go before we go onto handlers.go.
main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/santosh/gingo/handlers"
)
func setupRouter() *gin.Engine {
router := gin.Default()
router.GET("/books", handlers.GetBooks)
router.GET("/books/:isbn", handlers.GetBookByISBN)
// router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
// router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
router.POST("/books", handlers.PostBook)
return router
}
func main() {
router := setupRouter()
router.Run(":8080")
}
Almost similar to the hello world example we saw above. But this time we have gin.Default()
instead of gin.New()
. The Default
comes with defaults which most of us would like to have. Like logging middleware.
Frankly speaking, I haven't used much of Gin's middleware yet. But it's damn simple to create your middlewares. I'll put some links at the bottom of the post for your exploration. But for now, let's look at our handlers.
handlers/books.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/santosh/gingo/db"
"github.com/santosh/gingo/models"
)
// GetBooks responds with the list of all books as JSON.
func GetBooks(c *gin.Context) {
c.JSON(http.StatusOK, db.Books)
}
// PostBook takes a book JSON and store in DB.
func PostBook(c *gin.Context) {
var newBook models.Book
// Call BindJSON to bind the received JSON to
// newBook.
if err := c.BindJSON(&newBook); err != nil {
return
}
// Add the new book to the slice.
db.Books = append(db.Books, newBook)
c.JSON(http.StatusCreated, newBook)
}
// GetBookByISBN locates the book whose ISBN value matches the isbn
func GetBookByISBN(c *gin.Context) {
isbn := c.Param("isbn")
// Loop over the list of books, look for
// an book whose ISBN value matches the parameter.
for _, a := range db.Books {
if a.ISBN == isbn {
c.JSON(http.StatusOK, a)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "book not found"})
}
// func DeleteBookByISBN(c *gin.Context) {}
// func UpdateBookByISBN(c *gin.Context) {}
The real juice is in this handlers file. This might need some explanation.
handlers.GetBooks
, which is bound to GET /books
dumps the entire book slice.
handlers.GetBookByISBN
, which is bound to GET /books/:isbn
does the same thing, but it also accepts isbn
as a URL parameter. This handler scans the entire slice and returns the matched book. Scanning a large slice would not be the most optimal solution, but don't forget that we'll be implementing a real database while we continue to develop this bookstore.
The most interesting one here is handlers.PostBook
, which is bound to POST /books
. c.BindJSON
is the magic method, which takes in the JSON from the request and stores it into previously created newBook
struct. Later on
Tests
We need a little change here at the moment. We need to remove these contents from main.go:
@@ -1,17 +1,9 @@
package main
-import (
- "github.com/gin-gonic/gin"
- "github.com/santosh/gingo/handlers"
-)
+import "github.com/santosh/gingo/routes"
func main() {
- router := gin.Default()
- router.GET("/books", handlers.GetBooks)
- router.GET("/books/:isbn", handlers.GetBookByISBN)
- // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
- // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
- router.POST("/books", handlers.PostBook)
+ router := routes.SetupRouter()
router.Run(":8080")
}
And put it into a new file.
routes/roures.go
package routes
import (
"github.com/gin-gonic/gin"
"github.com/santosh/gingo/handlers"
)
func SetupRouter() *gin.Engine {
router := gin.Default()
router.GET("/books", handlers.GetBooks)
router.GET("/books/:isbn", handlers.GetBookByISBN)
// router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
// router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
router.POST("/books", handlers.PostBook)
return router
}
I have changes that make sense to you. We did this because we need to start the server from our tests.
Next, we create a books_test.go
in handlers.
handlers/books_test.go
package handlers_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/santosh/gingo/models"
"github.com/santosh/gingo/routes"
"github.com/stretchr/testify/assert"
)
func TestBooksRoute(t *testing.T) {
router := routes.SetupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/books", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "9781612680194")
assert.Contains(t, w.Body.String(), "9781781257654")
assert.Contains(t, w.Body.String(), "9780593419052")
}
func TestBooksbyISBNRoute(t *testing.T) {
router := routes.SetupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/books/9781612680194", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Rich Dad Poor Dad")
}
func TestPostBookRoute(t *testing.T) {
router := routes.SetupRouter()
book := models.Book{
ISBN: "1234567891012",
Author: "Santosh Kumar",
Title: "Hello World",
}
body, _ := json.Marshal(book)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/books", bytes.NewReader(body))
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Contains(t, w.Body.String(), "Hello World")
}
Also, again, pretty much self-explanatory. I don't think the above code needs any explanation. We are testing for response codes and response bodies for a specific string.
Let's also run the tests and check how it goes:
$ go test ./... -cover
? github.com/santosh/gingo [no test files]
? github.com/santosh/gingo/db [no test files]
ok github.com/santosh/gingo/handlers (cached) coverage: 83.3% of statements
? github.com/santosh/gingo/models [no test files]
? github.com/santosh/gingo/routes [no test files]
Exercise
Yeah, let's this blog post more interesting by adding some interactivity. I have some tasks for you, which you need to solve on your own. Please have a try on them. They are:
- Implement
DeleteBookByISBN
andUpdateBookByISBN
handlers and enable them. - Write tests for handlers mentioned above.
- Our tests are very basic. So are our handlers. We are not doing any error handling. Add error handling to handlers and write tests to validate them.
Conclusion
We have seen how simple is it to create a hello world application in Gin. But this journey does not end here. I'll come back with more tutorials next time. Until then, goodbye.
Related Link
Top comments (0)