Đầ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

model quan hệ trong go-pg

1. Giới thiệu Go-pg sử dụng công nghệ ORM (tức Object-relation mapping) giúp ánh xạ bảng cơ sở dữ liệu vào trong struct Điều đấy có nghĩa là với mỗi struct trong golang có thể dùng làm đại diện để truy vấn đến bảng trong postgresql và trả ra đối tượng struct với giá trị […]

GORM

1. ORM là gì? Trước hết để hiểu được thư viện Gorm chúng ta cần tìm hiểu về ORM, một công cụ quan trọng và thường xuyên được áp dụng trong quản trị cơ sở dữ liệu. ORM là tên viết tắt của cụm từ “Object Relational Mapping” đây là tên gọi chỉ việc ánh […]

REST API cơ bản trong Golang

Cấu trúc project Chúng ta hãy tạo cấu trúc thư mục như hình bên dưới, project này có tên GolangRestApi, có thể clone về với đường link sau: Github Sau khi clone về, đổi tên project thành GolangRestApi. Vào GOPATH, copy vào thư mục src: Code Rest Api Golang entities/user.go Khai báo cấu trúc của […]

Golang

Golang là gì? Go hay còn gọi là Golang là ngôn ngữ lập trình mã nguồn mở, được thiết kế tại Google bởi Robert Griesemer, Rob Pike, and Ken Thompson. Go có cú pháp giống với C và tất nhiên nó là ngôn ngữ lập trình biên dịch (compiled programming language) Cú pháp của ngôn […]

Elasticsearch

Elasticsearch là gì? Elasticsearch là một search engine (công cụ tìm kiếm) rất mạnh mẽ. Elasticsearch cũng có thể coi là một document oriented database, nó chứa dữ liệu giống như một database và thực hiện tìm kiếm trên những dữ liệu đó. Đại khái là thay vì bạn tìm kiếm trên file, trên các […]

Testing

Testing là gì? Thường thì mọi người hiểu khái niệm test chỉ là chạy test, chạy phần mềm nhưng đó chỉ là một phần không phải tất cả các hoạt động test. Các hoạt động test tồn tại trước và sau khi chạy PM bao gồm: lên kế hoạch và kiểm soát, chọn điều kiện […]