Go is a very popular programming language in recent years. It has simplified syntax and great run-time efficiency. Nowadays, Go has been used extensively to develop backend applications. This article will explain how to create a backend application with Go, implementing REST APIs and connecting to the database.
The complete code for this chapter can be found in .
Table of Contents
Creating a Go Project
First, we use the following commands to create a Go project called GoRestDbExample. The package name is github.com/xhhuango/waynestalk/gorestdbexample
.
% mkdir GoRestDbExample % cd GoRestDbExample GoRestDbExample % go mod init github.com/xhhuango/waynestalk/gorestdbexample
Create user.go and declare User struct
. json
is a struct field tag, specifying the name of this field in JSON when encoding struct into JSON. For example, we want Username
field to be username
in JSON instead of capital U. In addition, “-” means to ignore this field.
package main type User struct { Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
Create main.go and add main() to print a user.
package main import "fmt" func main() { fmt.Printf("%v", User{ Username: "bruce@gmail.com", Password: "123456", Name: "Bruce", Age: 24, }) }
Use the following command to execute this project.
GoRestDbExample % go run github.com/xhhuango/waynestalk/gorestdbexample {bruce@gmail.com 123456 Bruce 24}
Building a RESTful Web Service with Gin
Go’s built-in net/http package can implement REST APIs. However, many third-party packages also provide routers. In this article, we will use gin-gonic/gin to build a RESTful Web Service. Use the following instructions to install Gin.
GoRestDbExample % go get -u github.com/gin-gonic/gin
Gin can build a RESTful Web Service in just 2 or 3 lines. Modified main.go as below to build a web service on port 7788, but it has not yet implemented any REST APIs.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() if err := r.Run("localhost:7788"); err != nil { panic(err) } }
HTTP Requests
Create handler.go, and its code is as follows. JSON() converts user
slice into JSON and outputs it to the response body.
package main import ( "github.com/gin-gonic/gin" "net/http" ) var users = []User{ {Username: "bruce@gmail.com", Password: "123", Name: "Bruce", Age: 24}, {Username: "monika@gmail.com", Password: "123", Name: "Monika", Age: 18}, } func ListUsers(c *gin.Context) { c.JSON(http.StatusOK, users) }
Then, add ListUsers() to the router in main.go. We call GET() to indicate that this is a GET Request and the path is /users. Gin also provides POST(), PUT(), DELETE(), etc.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
After executing the project, use curl command to send an HTTP GET Request.
% curl --location --request GET 'localhost:7788/users' [{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18}]
Path Variables
Obtaining path variables with Gin is very simple. In handler.go, add GetUser() as below. We can use ShouldBindUri() to get path variables. param.Name
use field tag uri
to specify the binding path name.
func GetUser(c *gin.Context) { var param struct { Name string `uri:"name"` } if err := c.ShouldBindUri(¶m); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } for _, u := range users { if u.Name == param.Name { c.JSON(http.StatusOK, u) return } } c.JSON(http.StatusNotFound, gin.H{ "error": fmt.Sprintf("user %s not found", param.Name), }) }
Remember to add GetUser() to router in main.go. When setting the router, the path variable will have a semicolon prefix.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
Query Parameters
The way to obtain query parameters is similar to that of path variables.
Change ListUsers() to the following. In this code, we need to obtain query parameter age
. Different from path variables with uri
, query parameters uses field tag form
to specify the binding name, and call ShouldBindQuery() to get query parameters.
func ListUsers(c *gin.Context) { var query struct { Age uint `form:"age"` } if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } for _, u := range users { if u.Age == query.Age { c.JSON(http.StatusOK, u) return } } c.JSON(http.StatusNotFound, gin.H{ "error": fmt.Sprintf("user with age %d not found", query.Age), }) }
% curl --location --request GET 'localhost:7788/users?age=18' {"username":"monika@gmail.com","name":"Monika","age":18}
Request Body
The last one is response body passed by POST Requests. In handler.go, we add CreateUser() to add users. Gin uses field tag json
to specify the binding name, and then uses ShoulddBindJSON() to get the data.
func CreateUser(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` Name string `json:"name"` Age uint `json:"age"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } users = append(users, User{ Username: req.Username, Password: req.Password, Name: req.Name, Age: req.Age, }) c.JSON(http.StatusOK, users) }
Add CreateUser() to the router.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) r.POST("/users", CreateUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
Use curl to test POST /users.
% curl --location --request POST 'localhost:7788/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "peter@gmail.com", "password": "123", "name": "Peter", "age": 30 }' [{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18},{"username":"peter@gmail.com","name":"Peter","age":30}]
Validation
Gin also provides binding verification. You can validate whether the data exists or whether the format is correct when binding. It uses struct tag binding
to specify binding rules. For example, if you want to specify that a certain field must be provided, you can add binding:"required"
as follows.
func GetUser(c *gin.Context) { var param struct { Name string `uri:"name" binding:"required"` } ... }
In addition, you can also specify the data format, such as email, or the data must be greater than a certain value.
func CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required,email"` Password string `json:"password" binding:"required"` Name string `json:"name" binding:"required"` Age uint `json:"age" binding:"required,min=10"` } ... }
The above is just a list of some binding settings. In fact, there are many more settings. However, these settings can not be not found on Gin’s official website, because Gin uses go-playground/validator package to implement validation. Therefore, we can check the list of available settings on validator’s website.
Connecting Databases with GORM
Most of backend applications also connect to databases. Let’s connect a database for our RESTful web service. Go has many third-party database packages. In this article, we use go-gorm/gorm to connect a database. GORM is an ORM library used by quite a few people. In addition, according to the database you use, you have to install the corresponding driver.
Before connecting to a database, let’s modify User
first. Add gorm.Model
to User
.
package main import "gorm.io/gorm" type User struct { gorm.Model Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
gorm.Model
contains 4 fields, where ID
is tagged as a primary key.
package gorm import "time" type Model struct { ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt DeletedAt `gorm:"index"` }
You can also define primary key field by yourself without using gorm.Model
, as follows.
package main import "gorm.io/gorm" type User struct { UserID uint64 `json:"userId" gorm:"primarykey"` Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
SQLite
SQLite is a simple but very convenient database. Use the following commands to install GORM and SQLite driver.
GoRestDbExample % go get -u gorm.io/gorm GoRestDbExample % go get -u gorm.io/driver/sqlite
Modify main.go as follows. Call gorm.Open() to open ./test.db database. If the file does not exist, it will be created automatically. After that, call AutoMigrate() to create a table based on User
.
package main import ( "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" ) var db *gorm.DB func main() { _db, err := gorm.Open(sqlite.Open("./test.db"), &gorm.Config{}) if err != nil { panic(err) } db = _db if err := db.AutoMigrate(&User{}); err != nil { panic(err) } r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) r.POST("/users", CreateUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
In addition, SQLite also supports In-Memory database. It does not create files, but creates a temporary database in memory. In the development stage, it is quite convenient when the table columns are still uncertain. In addition, it is also very useful when writing test programs. To use In-Memory database, you just need to change the file name to :memory:
.
func main() { _db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { panic(err) } db = _db if err := db.AutoMigrate(&User{}); err != nil { panic(err) } ... }
Query
Querying with GORM is quite simple. Create repository.go and paste the following code. Call Where() to set the WHERE conditions, and then call Find() to query data.
func findUsers(age uint) ([]User, error) { var users []User if result := db.Where("Age > ?", age).Find(&users); result.Error != nil { return nil, result.Error } return users, nil }
In addition to using Where() to set conditions, GORM also supports using struct to set conditions, as follows.
func findByName(name string) (*User, error) { var user User if result := db.Where(&User{Name: name}).First(&user); result.Error != nil { return nil, result.Error } return &user, nil }
Insertion
It is also very simple to insert data with GORM. Create a User
, fill in all the data to be added, and then call Create() to insert it. If the insertion is successful, the primary key field in the User
will automatically be assigned the primary key.
func insertUser(user *User) error { if result := db.Create(user); result.Error != nil { return result.Error } return nil }
Conclusion
This article is just a rough introduction to Gin and GORM, they have far more features than these. If you want to understand them in depth, you must read their documents yourself. Fortunately, their documents are very complete, and there are many related resources on the Internet.