Hướng dẫn này giới thiệu những kiến thức căn bản để viết một RESTful web service API bằng Go và Gin Web Framework (Gin).
Bạn sẽ thu được nhiều nhất từ hướng dẫn này nếu đã quen cơ bản với Go và bộ công cụ của nó. Nếu đây là lần đầu bạn tiếp xúc với Go, hãy xem Hướng dẫn: Bắt đầu với Go để có phần giới thiệu nhanh.
Gin đơn giản hoá nhiều công việc lập trình liên quan đến việc xây dựng ứng dụng web, trong đó có web service. Trong hướng dẫn này, bạn sẽ dùng Gin để định tuyến request (yêu cầu từ client), lấy thông tin chi tiết của request, và marshal JSON cho response (phản hồi trả về).
Trong hướng dẫn này, bạn sẽ xây dựng một RESTful API server với hai endpoint (đầu mối truy cập của API). Dự án ví dụ của bạn sẽ là một kho dữ liệu về các đĩa nhạc jazz cổ.
Hướng dẫn gồm các phần sau:
- Thiết kế các endpoint của API.
- Tạo thư mục cho mã nguồn của bạn.
- Tạo dữ liệu.
- Viết handler (hàm xử lý request) để trả về tất cả các mục.
- Viết handler để thêm một mục mới.
- Viết handler để trả về một mục cụ thể.
Lưu ý: Xem thêm các hướng dẫn khác tại Tutorials.
Để thử phần này dưới dạng hướng dẫn tương tác hoàn thành trong Google Cloud Shell, hãy bấm nút bên dưới.
Điều kiện cần
- Go. Chúng tôi khuyến nghị dùng phiên bản Go mới nhất để theo hướng dẫn này. Xem hướng dẫn cài đặt tại Installing Go.
- Một công cụ để soạn mã nguồn. Bất kỳ trình soạn thảo văn bản nào bạn có cũng dùng được.
- Một cửa sổ dòng lệnh. Go hoạt động tốt với mọi terminal trên Linux và Mac, và với PowerShell hoặc cmd trên Windows.
- Công cụ curl. Trên Linux và Mac, công cụ này thường đã được cài sẵn. Trên Windows, nó có sẵn từ bản Windows 10 Insider build 17063 trở lên. Với các phiên bản Windows cũ hơn, có thể bạn cần cài đặt nó. Xem thêm tại Tar and Curl Come to Windows.
Thiết kế các endpoint của API
Bạn sẽ xây dựng một API cung cấp quyền truy cập vào một cửa hàng bán đĩa than cổ. Vì vậy bạn cần cung cấp các endpoint để client có thể lấy về và thêm album cho người dùng.
Khi phát triển một API, thông thường bạn bắt đầu bằng việc thiết kế các endpoint. Người dùng API của bạn sẽ thành công hơn nếu các endpoint dễ hiểu.
Dưới đây là các endpoint bạn sẽ tạo trong hướng dẫn này.
/albums
GET– Lấy danh sách tất cả album, trả về dưới dạng JSON.POST– Thêm một album mới từ dữ liệu request gửi lên dưới dạng JSON.
/albums/:id
GET– Lấy một album theo ID của nó, trả về dữ liệu album dưới dạng JSON.
Tiếp theo, bạn sẽ tạo một thư mục cho mã nguồn của mình.
Tạo thư mục cho mã nguồn của bạn
Để bắt đầu, hãy tạo một dự án cho đoạn mã bạn sắp viết.
-
Mở cửa sổ dòng lệnh và chuyển về thư mục home của bạn.
Trên Linux hoặc Mac:
$ cdTrên Windows:
C:\> cd %HOMEPATH% -
Dùng dòng lệnh, tạo một thư mục cho mã nguồn của bạn với tên là web-service-gin.
$ mkdir web-service-gin $ cd web-service-gin -
Tạo một module để bạn có thể quản lý các phụ thuộc.
Chạy câu lệnh
go mod init, truyền cho nó đường dẫn của module chứa mã nguồn của bạn.$ go mod init example/web-service-gin go: creating new go.mod: module example/web-service-ginCâu lệnh này tạo ra tệp go.mod, nơi các phụ thuộc bạn thêm vào sẽ được liệt kê để theo dõi. Để biết thêm về việc đặt tên module bằng module path, xem Managing dependencies.
Tiếp theo, bạn sẽ thiết kế các cấu trúc dữ liệu để xử lý dữ liệu.
Tạo dữ liệu
Để mọi thứ đơn giản trong hướng dẫn này, bạn sẽ lưu dữ liệu trong bộ nhớ. Một API điển hình hơn sẽ tương tác với một cơ sở dữ liệu.
Lưu ý rằng lưu dữ liệu trong bộ nhớ nghĩa là tập hợp các album sẽ mất đi mỗi lần bạn dừng server, rồi được tạo lại khi bạn khởi động nó.
Viết mã nguồn
-
Dùng trình soạn thảo văn bản, tạo một tệp tên là main.go trong thư mục web-service. Bạn sẽ viết mã Go của mình trong tệp này.
-
Trong main.go, ở đầu tệp, dán vào khai báo package sau đây.
package mainMột chương trình độc lập (khác với một thư viện) luôn nằm trong package
main. -
Bên dưới khai báo package, dán vào khai báo sau của struct
album. Bạn sẽ dùng nó để lưu dữ liệu album trong bộ nhớ.Các struct tag như
json:"artist"chỉ định tên của một trường sẽ là gì khi nội dung của struct được tuần tự hoá thành JSON. Nếu không có chúng, JSON sẽ dùng tên trường viết hoa của struct – một phong cách không phổ biến lắm trong JSON.// album represents data about a record album. type album struct { ID string `json:"id"` Title string `json:"title"` Artist string `json:"artist"` Price float64 `json:"price"` } -
Bên dưới khai báo struct bạn vừa thêm, dán vào slice sau gồm các struct
albumchứa dữ liệu mà bạn sẽ dùng để khởi đầu.// albums slice to seed record album data. var albums = []album{ {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, }
Tiếp theo, bạn sẽ viết mã để hiện thực endpoint đầu tiên của mình.
Viết handler để trả về tất cả các mục
Khi client gửi request tới GET /albums, bạn muốn trả về tất cả các album dưới dạng JSON.
Để làm điều này, bạn sẽ viết những phần sau:
- Logic để chuẩn bị một response
- Mã để ánh xạ đường dẫn của request tới logic của bạn
Lưu ý rằng thứ tự này ngược với thứ tự chúng được thực thi lúc chạy, nhưng bạn đang thêm các phụ thuộc trước, rồi mới tới đoạn mã phụ thuộc vào chúng.
Viết mã nguồn
-
Bên dưới đoạn mã struct bạn đã thêm ở phần trước, dán vào đoạn mã sau để lấy danh sách album.
Hàm
getAlbumsnày tạo JSON từ slice các structalbum, rồi ghi JSON đó vào response.// getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) }Trong đoạn mã này, bạn:
-
Viết một hàm
getAlbumsnhận một tham sốgin.Context. Lưu ý rằng bạn có thể đặt cho hàm này bất kỳ tên nào – cả Gin lẫn Go đều không yêu cầu một định dạng tên hàm cụ thể.gin.Contextlà phần quan trọng nhất của Gin. Nó mang theo thông tin chi tiết của request, kiểm tra tính hợp lệ và tuần tự hoá JSON, cùng nhiều việc khác. (Dù tên gọi tương tự, nó khác với packagecontextcó sẵn của Go.) -
Gọi
Context.IndentedJSONđể tuần tự hoá struct thành JSON và thêm nó vào response.Đối số đầu tiên của hàm là HTTP status code mà bạn muốn gửi tới client. Ở đây, bạn truyền hằng số
StatusOKtừ packagenet/httpđể biểu thị200 OK.Lưu ý rằng bạn có thể thay
Context.IndentedJSONbằng lời gọiContext.JSONđể gửi JSON gọn hơn. Trên thực tế, dạng có thụt lề dễ làm việc hơn nhiều khi gỡ lỗi và khác biệt về kích thước thường là nhỏ.
-
-
Gần đầu tệp main.go, ngay bên dưới khai báo slice
albums, dán đoạn mã dưới đây để gán hàm handler cho một đường dẫn endpoint.Đoạn này thiết lập một liên kết trong đó
getAlbumsxử lý các request tới đường dẫn endpoint/albums.func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }Trong đoạn mã này, bạn:
-
Khởi tạo một Gin router (bộ định tuyến) bằng
Default. -
Dùng hàm
GETđể liên kết phương thức HTTPGETvà đường dẫn/albumsvới một hàm handler.Lưu ý rằng bạn đang truyền tên của hàm
getAlbums. Điều này khác với việc truyền kết quả của hàm, tức là truyềngetAlbums()(để ý cặp ngoặc đơn). -
Dùng hàm
Runđể gắn router vào mộthttp.Servervà khởi động server.
-
-
Gần đầu tệp main.go, ngay bên dưới khai báo package, import các package bạn cần để hỗ trợ đoạn mã vừa viết.
Những dòng mã đầu tiên sẽ trông như thế này:
package main import ( "net/http" "github.com/gin-gonic/gin" ) -
Lưu tệp main.go.
Chạy mã nguồn
-
Bắt đầu theo dõi module Gin như một phụ thuộc.
Tại dòng lệnh, dùng
go getđể thêm module github.com/gin-gonic/gin làm phụ thuộc cho module của bạn. Dùng đối số dấu chấm với nghĩa “lấy các phụ thuộc cho mã nguồn trong thư mục hiện tại.”$ go get . go get: added github.com/gin-gonic/gin v1.7.2Go đã phân giải và tải về phụ thuộc này để thoả mãn khai báo
importmà bạn đã thêm ở bước trước. -
Từ dòng lệnh trong thư mục chứa main.go, chạy mã nguồn. Dùng đối số dấu chấm với nghĩa “chạy mã nguồn trong thư mục hiện tại.”
$ go run .Khi mã nguồn đang chạy, bạn có một HTTP server đang chạy mà bạn có thể gửi request tới.
-
Từ một cửa sổ dòng lệnh mới, dùng
curlđể gửi một request tới web service đang chạy của bạn.$ curl http://localhost:8080/albumsCâu lệnh này sẽ hiển thị dữ liệu mà bạn đã nạp sẵn cho service.
[ { "id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99 } ]
Bạn đã khởi động được một API! Ở phần tiếp theo, bạn sẽ tạo thêm một endpoint với mã xử lý request POST để thêm một mục.
Viết handler để thêm một mục mới
Khi client gửi một request POST tới /albums, bạn muốn thêm album được mô tả trong body của request vào dữ liệu album hiện có.
Để làm điều này, bạn sẽ viết những phần sau:
- Logic để thêm album mới vào danh sách hiện có.
- Một chút mã để định tuyến request
POSTtới logic của bạn.
Viết mã nguồn
-
Thêm mã để đưa dữ liệu album vào danh sách album.
Ở đâu đó sau các câu lệnh
import, dán vào đoạn mã sau. (Cuối tệp là một vị trí tốt cho đoạn mã này, nhưng Go không bắt buộc thứ tự khai báo các hàm.)// postAlbums adds an album from JSON received in the request body. func postAlbums(c *gin.Context) { var newAlbum album // Call BindJSON to bind the received JSON to // newAlbum. if err := c.BindJSON(&newAlbum); err != nil { return } // Add the new album to the slice. albums = append(albums, newAlbum) c.IndentedJSON(http.StatusCreated, newAlbum) }Trong đoạn mã này, bạn:
- Dùng
Context.BindJSONđể binding body của request vàonewAlbum. - Nối struct
albumđược khởi tạo từ JSON vào slicealbums. - Thêm status code
201vào response, cùng với JSON biểu diễn album bạn vừa thêm.
- Dùng
-
Sửa hàm
maincủa bạn để nó bao gồm hàmrouter.POST, như sau.func main() { router := gin.Default() router.GET("/albums", getAlbums) router.POST("/albums", postAlbums) router.Run("localhost:8080") }Trong đoạn mã này, bạn:
-
Liên kết phương thức
POSTtại đường dẫn/albumsvới hàmpostAlbums.Với Gin, bạn có thể liên kết một handler với một tổ hợp phương thức HTTP và đường dẫn. Bằng cách này, bạn có thể định tuyến riêng các request gửi tới cùng một đường dẫn dựa trên phương thức mà client đang dùng.
-
Chạy mã nguồn
-
Nếu server vẫn đang chạy từ phần trước, hãy dừng nó lại.
-
Từ dòng lệnh trong thư mục chứa main.go, chạy mã nguồn.
$ go run . -
Từ một cửa sổ dòng lệnh khác, dùng
curlđể gửi một request tới web service đang chạy của bạn.$ curl http://localhost:8080/albums \ --include \ --header "Content-Type: application/json" \ --request "POST" \ --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'Câu lệnh này sẽ hiển thị các header và JSON của album vừa được thêm.
HTTP/1.1 201 Created Content-Type: application/json; charset=utf-8 Date: Wed, 02 Jun 2021 00:34:12 GMT Content-Length: 116 { "id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99 } -
Như ở phần trước, dùng
curlđể lấy về danh sách album đầy đủ, nhờ đó bạn có thể xác nhận rằng album mới đã được thêm vào.$ curl http://localhost:8080/albums \ --header "Content-Type: application/json" \ --request "GET"Câu lệnh này sẽ hiển thị danh sách album.
[ { "id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99 }, { "id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99 } ]
Ở phần tiếp theo, bạn sẽ thêm mã để xử lý một request GET cho một mục cụ thể.
Viết handler để trả về một mục cụ thể
Khi client gửi một request tới GET /albums/[id], bạn muốn trả về album có ID khớp với tham số đường dẫn id.
Để làm điều này, bạn sẽ:
- Thêm logic để lấy về album được yêu cầu.
- Ánh xạ đường dẫn tới logic đó.
Viết mã nguồn
-
Bên dưới hàm
postAlbumsbạn đã thêm ở phần trước, dán đoạn mã sau để lấy về một album cụ thể.Hàm
getAlbumByIDnày sẽ trích xuất ID trong đường dẫn của request, rồi tìm album khớp với nó.// getAlbumByID locates the album whose ID value matches the id // parameter sent by the client, then returns that album as a response. func getAlbumByID(c *gin.Context) { id := c.Param("id") // Loop over the list of albums, looking for // an album whose ID value matches the parameter. for _, a := range albums { if a.ID == id { c.IndentedJSON(http.StatusOK, a) return } } c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) }Trong đoạn mã này, bạn:
-
Dùng
Context.Paramđể lấy tham số đường dẫnidtừ URL. Khi ánh xạ handler này tới một đường dẫn, bạn sẽ đặt một placeholder cho tham số đó trong đường dẫn. -
Lặp qua các struct
albumtrong slice, tìm struct có giá trị trườngIDkhớp với giá trị tham sốid. Nếu tìm thấy, bạn tuần tự hoá structalbumđó thành JSON và trả về như một response với mã HTTP200 OK.Như đã nói ở trên, một service thực tế nhiều khả năng sẽ dùng một truy vấn cơ sở dữ liệu để thực hiện việc tra cứu này.
-
Trả về lỗi HTTP
404bằnghttp.StatusNotFoundnếu không tìm thấy album.
-
-
Cuối cùng, sửa hàm
maincủa bạn để nó bao gồm một lời gọi mới tớirouter.GET, trong đó đường dẫn giờ là/albums/:id, như trong ví dụ sau.func main() { router := gin.Default() router.GET("/albums", getAlbums) router.GET("/albums/:id", getAlbumByID) router.POST("/albums", postAlbums) router.Run("localhost:8080") }Trong đoạn mã này, bạn:
- Liên kết đường dẫn
/albums/:idvới hàmgetAlbumByID. Trong Gin, dấu hai chấm đứng trước một mục trong đường dẫn biểu thị rằng mục đó là một tham số đường dẫn.
- Liên kết đường dẫn
Chạy mã nguồn
-
Nếu server vẫn đang chạy từ phần trước, hãy dừng nó lại.
-
Từ dòng lệnh trong thư mục chứa main.go, chạy mã nguồn để khởi động server.
$ go run . -
Từ một cửa sổ dòng lệnh khác, dùng
curlđể gửi một request tới web service đang chạy của bạn.$ curl http://localhost:8080/albums/2Câu lệnh này sẽ hiển thị JSON của album có ID mà bạn đã dùng. Nếu không tìm thấy album, bạn sẽ nhận được JSON kèm một thông báo lỗi.
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
Kết luận
Xin chúc mừng! Bạn vừa dùng Go và Gin để viết một RESTful web service đơn giản.
Các chủ đề tiếp theo được gợi ý:
- Nếu bạn mới làm quen với Go, bạn sẽ tìm thấy những thực hành tốt hữu ích được mô tả trong Effective Go và How to write Go code.
- Go Tour là phần giới thiệu từng bước rất tốt về các kiến thức nền tảng của Go.
- Để biết thêm về Gin, xem tài liệu package Gin Web Framework hoặc tài liệu Gin Web Framework.
Mã nguồn hoàn chỉnh
Phần này chứa mã nguồn của ứng dụng mà bạn xây dựng với hướng dẫn này.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
// albums slice to seed record album data.
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop through the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
