Đầ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 ServerDatabase 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 UsernamePassword: 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)

About the Author

Ha Trung Vi

View all author's posts

Leave a Comment

Your email address will not be published. Required fields are marked *

Bài viết khác

Web Security

Khái niệm Web Security Web Security là tập hợp các nguyên tắc, biện pháp và công nghệ nhằm bảo vệ website, ứng dụng web và dữ liệu khỏi các hành vi truy cập trái phép, tấn công độc hại hoặc khai thác lỗ hổng. Nó không chỉ bao gồm việc ngăn chặn hacker, mà còn […]

Markdown

Markdown là một ngôn ngữ đánh dấu nhẹ (lightweight markup language) dùng để định dạng văn bản thuần túy (plain text), thường được sử dụng trong các tài liệu như README, bài viết blog, tài liệu hướng dẫn, và cả trong GitHub, Stack Overflow, hoặc các trình soạn thảo như VS Code, Obsidian… Markdown được […]

CSS

CSS (Cascading Style Sheets – tạm dịch: Tập tin định kiểu tầng) là ngôn ngữ dùng để mô tả cách trình bày (giao diện) của một tài liệu HTML. Nói đơn giản, CSS giúp làm đẹp trang web: chỉnh màu sắc, font chữ, bố cục, khoảng cách, hiệu ứng chuyển động, v.v. CSS được phát […]

HTML

HTML (HyperText Markup Language) là ngôn ngữ đánh dấu siêu văn bản, được dùng để xây dựng cấu trúc của một trang web. Nói cách khác, HTML cho trình duyệt biết nội dung nào sẽ hiển thị và hiển thị như thế nào (như tiêu đề, đoạn văn, hình ảnh, liên kết…). Một tài liệu […]

Browser

Browser (Web Browser, Trình duyệt web) là phần mềm trên máy tính, điện thoại hoặc thiết bị thông minh, cho phép người dùng truy cập, hiển thị và tương tác với các trang web, tài nguyên Internet. Về bản chất, trình duyệt gửi các yêu cầu (HTTP/HTTPS request) đến máy chủ web, nhận về mã […]

Tìm hiểu DNS

DNS là gì? DNS (Domain Name System) là một dịch vụ phân giải tên miền, giúp chuyển đổi các tên miền (ví dụ: www.ducphat.com) thành địa chỉ IP (ví dụ: 93.184.216.34) và ngược lại. Thay vì phải nhớ dãy số IP, chúng ta chỉ cần nhập tên miền, DNS sẽ tìm kiếm địa chỉ IP […]