Đầu tiên, chúng ta sẽ viết một đoạn mã nhỏ bằng chữ Hello World.
Bước 1: Tạo thư mục để chứa mã nguồn của dự án Todo App
mkdir go-rest-api
Bước 2: Khởi tạo Go Modules
go mod init TodoApp go get -u github.com/gin-gonic/gin
Bước 3: Tạo tệp main.go và viết đầu tiên chương trình Hello World
package main import ( "fmt" ) func main() { fmt.Println("Hello World!")
Bước 4: Chạy file main.go sử dụng câu lệnh
go run main.go
Bạn sẽ nhìn thấy output như sau:
Hello World!
Tiếp theo mình sẽ sử dụng Docker và Docker Compose để thiết lập môi trường phát triển. Như vậy, những người khác hoặc chính bạn sau này đều có thể xây dựng môi trường chạy dự án chỉ bằng một câu lệnh “docker-compile up” mà không cần quan tâm là ở máy chúng ta đã cài đặt Go hay chưa. Chúng ta cùng bắt đầu nhé.
Thiết lập Docker
Đầu tiên chúng ta hãy nhìn tổng quan về dự án kiến trúc mà chúng ta sẽ thực hiện. Kiến trúc sẽ như sau:
- Sử dụng Nginx làm proxy ngược: khi có yêu cầu gọi vào API, yêu cầu sẽ chuyển vào Nginx, sau đó được chuyển tiếp sang phần phụ trợ
- Backend sử dụng khung Gin, tương tác với Cơ sở dữ liệu sử dụng MinIO và trả lời phản hồi về cho Nginx
- Nginx gửi phản hồi trả về cho người dùng
Từ kiến trúc này, chúng ta sẽ cấu trúc folder như sau:
Bước 1: Tạo dockerfile cho backend trong TodoApp thư mục / .docker / backend /
Từ dockerfile này, sau quá trình build chúng ta sẽ có được docker image cho backend. Chạy docker image này chúng ta sẽ có container backend sẵn sàng xử lý các request được gửi đến.
FROM golang:1.18.1-alpine3.15 WORKDIR /usr/src/app ENV MINIO_SERVER_ACCESS_KEY=minioadmin ENV MINIO_SERVER_SECRET_KEY=minioadmin CMD go run main.go
Bước 2: Tạo tệp nginx.conf trong TodoApp thư mục / .docker / nginx /
Ở file nginx.conf này, chúng ta sẽ cấu hình Nginx trở thành một reverse proxy.
# Determine the formatting of the log that will be print to the access.log file log_format testlog '$remote_addr - $remote_user [$time_local] ' '"$request" $status $bytes_sent ' '"$http_referer" $http_user_agent $request_body $gzip_ratio ' '"$request_time $upstream_connect_time $upstream_header_time $upstream_response_time '; # Write the reverse proxy server { # Determine where to output the log access_log /var/log/nginx/access.log; # expose port 80 listen 80; # if the root route get access it will return the default nginx html page location /api { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; # do not forget to include the scheme which is http proxy_pass http://backend:8080; } }
Bước 3: Tạo tập tin docker – compos.yml trong TodoApp /
Sử dụng Docker Compose, chúng sẽ sẽ kết nối các service lại theo kiến trúc như đã chia sẻ ở trên. Từ đó, mỗi khi cần chạy dự án, chúng ta chỉ cần sử dụng lệnh “docker-compose up” là được.
version: '3.7' services: db: image: 'bitnami/minio:2022.4.16-debian-10-r9' ports: - '9002:9000' - '9001:9001' environment: - MINIO_ROOT_USER=minioadmin - MINIO_ROOT_PASSWORD=minioadmin networks: - TodoApp backend: build: context: . dockerfile: ./.docker/backend/dockerfile volumes: - ./backend:/usr/src/app depends_on: - db ports: - 8080:8080 networks: - TodoApp nginx: image: nginx:1.21.6-alpine ports: - 80:80 volumes: - ./.docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - backend networks: - TodoApp networks: TodoApp: driver: bridge
Bước 4: Chuyển main.go, go.mod, go.sum vào thư mực TodoApp/backend/
Bước 5: Chạy dự án sử dụng câu lệnh
docker-compose up
Bước 6: Bạn sẽ thấy output như sau
Hello World!
Thiết lập Route
a. Route là gì
Route tạm dịch là một tuyến đường. Nó sẽ thực hiện nhiệm vụ kết nối giữa client và server.
b. Tại sao phải dùng Route?
Route luôn là một phần quan trọng của hệ thống website. Tất cả các request khi qua Route đều được kiểm tra và xử lý. Sử dụng hệ thống định tuyến cho phép chúng ta cấu trúc ứng dụng của mình theo cách tốt hơn và tường minh hơn.
c. Tạo router
Cách mặc định để tạo một Route trong Gin như sau:
router := gin.Default()
router.GET(“/api/todos”, func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
“message”: “Hello World!”,
})
File main.go lúc này sẽ như sau:
package main import ( "net/http" "time" "github.com/gin-gonic/gin" ) func main() { time.Sleep(3 * time.Second) router := gin.Default() router.GET("/api/todos", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Hello World!", }) }) router.Run(":8080") }
Chạy lệnh
Truy cập url: localhost:8080/api/todos/
Kết quả sẽ giống như sau:
Vậy là chúng ta đã vừa tạo thành công một route để xử lý request sử dụng Gin. Hiện tại, mỗi khi người dùng gọi vào API /api/todos thì sẽ nhận được response là { “message”: “Hello World” }. Ở phần tiếp theo chúng ta sẽ cùng nhau hoàn thiện một API hoàn chỉnh cho việc quản lý các Todo.
Todo Model
a. Đầu tiên chúng ta cần mô hình hóa một Todo.
Thuộc tính của một todo gồm:
+Id string
+Name string
(Note: Tên thuộc tính phải viết hoa chữ cái đầu. Nếu không sẽ bị lỗi)
b. Tạo file JSON
Mình sẽ sử dụng MinIO như một Database nên những trao đổi dữ liệu giữa Server và Database sẽ là file json.
func CreateJson(id, name string) (jsonData []byte, todo Todo){ todo = Todo{ Id: id, Name: name, } jsonData, err :=json.Marshal(todo) if err != nil { log.Fatal(err) } return jsonData, todo }
c. Lấy tất cả dữ liệu Todo
Vì chúng ta giao tiếp với database bằng dữ liệu json. Nên để nhận được dữ liệu theo cấu trúc Todo. Chúng ta phải chuyển đổi dữ liệu.
func GetAllTodos(jsonFiles []io.Reader) (temp TodoList){ for i:=0; i < len(jsonFiles); i++{ data := StreamToByte(jsonFiles[i]) var todo Todo json.Unmarshal(data, &todo) temp.TodoList = append(temp.TodoList, todo) } return temp }
Lúc này ta sẽ có file Todo.go ở thư mục TodoApp/backend/Models như sau:
package models import ( "bytes" "encoding/json" "io" "log" ) type TodoList struct{ TodoList []Todo `json:"TodoList"` } type Todo struct { Id string `json:"Id"` Name string `json:"Name"`" } func CreateJson(id, name string) (jsonData []byte, todo Todo){ todo = Todo{ Id: id, Name: name, } jsonData, err :=json.Marshal(todo) if err != nil { log.Fatal(err) } return jsonData, todo } func StreamToByte(stream io.Reader) []byte { buf := new(bytes.Buffer) buf.ReadFrom(stream) return buf.Bytes() } func GetATodo(jsonFile io.Reader) (temp Todo){ data := StreamToByte(jsonFile) json.Unmarshal(data, &temp) return temp } func GetAllTodos(jsonFiles []io.Reader) (temp TodoList){ for i:=0; i < len(jsonFiles); i++{ data := StreamToByte(jsonFiles[i]) var todo Todo json.Unmarshal(data, &todo) temp.TodoList = append(temp.TodoList, todo) } return temp }
Như vậy là chúng ta đã có một Model cơ bản. Tiếp theo chúng ta cần một cầu nối giữa MinIO và Backend của chúng ta. Thông thường, nếu như sử dụng các framework như PHP Laravel với hệ cơ sở dữ liệu là MySQL, Postgre,… framework sẽ cung cấp sẵn các connector cho chúng ta config và sử dụng. Ở đây mình sẽ viết một connector đơn giản để giao tiếp với MinIO.
Database connector
Vì đây là một connector đơn giản nên mình sẽ hard-coded accessKeyID, secretAccessKey,... Thông thường những thông tin này nên sử dụng biến môi trường để lưu trữ và code của chúng ta sẽ đọc từ biến môi trường để config. File MinIODB.go trong thư mục TodoApp/backend/Database/ sẽ như sau:
package Config import ( "context" "fmt" "io" "log" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/sirupsen/logrus" ) const endpoint = "db:9000" const accessKeyID = "minioadmin" const secretAccessKey = "minioadmin" //Create connect func ConnectDB()(c *minio.Client,err error){ useSSL := false minioClient, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL, }) if err != nil { log.Fatalln(err) } return minioClient, err } //Set permission func SetPermission(client *minio.Client, bucketName string) error{ policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Principal": {"AWS": ["*"]},"Resource": ["arn:aws:s3:::`+ bucketName +`/*"],"Sid": ""}]}` err := client.SetBucketPolicy(context.Background(), bucketName, policy) if err != nil { fmt.Println(err) return err } return err } //Create bucket func CreateBucket(client *minio.Client, bucketName string) error{ ctx := context.Background() err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: "ap-northeast-1"}) if err != nil{ exists, errBucketExists :=client.BucketExists(ctx, bucketName) if errBucketExists != nil { logrus.Errorf("[UploadImage] check bucket exists error: %s", err) return err } if !exists { logrus.Errorf("[UploadImage] make bucket error: %s", err) return err } } return err } //Upload data to MinIO func UploadData(client *minio.Client, bucketName, objectName string, data io.Reader) error { _, err := client.GetBucketPolicy(context.Background(), bucketName) if err != nil { log.Fatalln(err) } n, err := client.PutObject(context.Background(), bucketName, objectName, data, -1, minio.PutObjectOptions{ContentType: "application/octet-stream"}) if err != nil { fmt.Println(err) return err } fmt.Println("Successfully uploaded bytes: ", n) return err } //Get a data from MinIO func GetDataTodo(client *minio.Client, bucketName, objectName string) (file io.Reader) { _, err := client.GetBucketPolicy(context.Background(), bucketName) if err != nil { log.Fatalln(err) } file, err = client.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{}) if err != nil { fmt.Println(err) return } return file } //Get all data from MinIO func GetDataTodoList(client *minio.Client, bucketName string) (file []io.Reader) { _, err := client.GetBucketPolicy(context.Background(), bucketName) if err != nil { log.Fatalln(err) } objectCh := client.ListObjects(context.Background(), bucketName, minio.ListObjectsOptions{ Recursive: true, }) for object := range objectCh { file = append(file, GetDataTodo(client, bucketName, object.Key)) } return file }
Controllers
Cuối cùng chúng ta sẽ tạo ra file controller thực hiện nhiệm vụ liên kết giữa server được xây dựng bằng Go và database MinIO.
File TodoController.go trong thư mục TodoApp/backend/http/Controllers/ sẽ như sau:
package Controllers import ( "TodoApp/Database" "TodoApp/models" "bytes" "github.com/minio/minio-go/v7" ) func GetAllTodos(Client *minio.Client, bucketName string) (res models.TodoList) { todoList := Config.GetDataTodoList(Client, bucketName) res = models.GetAllTodos(todoList) return res } func AddTodo(Client *minio.Client, bucketName, objectName, id, name string)(res models.Todo){ jsonData,res := models.CreateJson(id, name) data := bytes.NewReader(jsonData) err := Config.UploadData(Client, bucketName,objectName, data) if err !=nil{ panic(err) } return res } func UploadJson(Client *minio.Client, bucketName, objectName, id, name string){ jsonFile,_:=models.CreateJson(id, name) data := bytes.NewReader(jsonFile) err := Config.UploadData(Client, bucketName,objectName, data) if err !=nil{ panic(err) } }
Update file main.go
Dữ liệu example sẽ được upload lên MinIO để trực quan hơn.
File main.go lúc này sẽ như sau:
package main import ( "TodoApp/Database" "TodoApp/http/Controllers" "TodoApp/models" "log" "time" "github.com/gin-gonic/gin" ) func main() { time.Sleep(3 * time.Second) Client, err := Config.ConnectDB() if err != nil { log.Println(err) } err = Config.CreateBucket(Client, "todolist") if err != nil { log.Println(err) } //example data Controllers.UploadJson(Client, "todolist","todo1.json","1","go to school") Controllers.UploadJson(Client, "todolist","todo2.json","2","go to canteen") Controllers.UploadJson(Client, "todolist","todo3.json","3","come back home") //route router := gin.Default() router.GET("/api/todos", func(c *gin.Context) { ab := Controllers.GetAllTodos(Client, "todolist") c.JSON(200, ab) }) router.POST("/api/todos/:uid/:id", func(c *gin.Context) { var todo models.Todo err := c.BindJSON(&todo) if err != nil { log.Fatal(err) } res := Controllers.AddTodo(Client, "todolist", c.Param("id")+".json", c.Param("id"), todo.Name) c.JSON(200, res) }) router.Run(":8080") }
a. Cấu trúc thư mục:
b. Chạy lệnh
docker-compose up
Truy cập url: localhost:8080/api/todos/
Kết quả sẽ như sau:
Thêm một Todo bằng phương thức POST qua URL:
localhost:8080/api/todos/ +id
Mình dùng extension: Thunder Client trên Visual Studio để test.
Kết quả:
Truy cập lại url: localhost:8080/api/todos/, danh sách Todolist đã có thêm Todo mà chúng ta vừa tạo bằng POST request.
c. Truy cập localhost:9001 để xem dữ liệu trong MinIO
Đăng nhập với Username và Password: minioadmin
Truy cập vào bucket todolist chúng ta sẽ sẽ nhìn thấy như thế này:
(https://www.marketenterprise.vn/blog/rest-api-voi-golang-gin-minio.html)