Bài 03: Kiến trúc dữ liệu: Tối ưu /api (OpenAPI/gRPC) và /web (Static/Templates)
1. Mục tiêu bài học
-
Phân định ranh giới giữa giao ước API (API Contracts -
/api) và giao diện hiển thị phía client (/web). -
Tổ chức các file định nghĩa giao tiếp đa giao thức (Protocol Buffers, OpenAPI/Swagger specs, JSON schemas).
-
Xây dựng luồng tích hợp UI: Quản lý SPA bundle (React/Vue) hoặc Server-Side Rendering (Go html/template).
2. Nội dung chi tiết
-
Phần 1: Thư mục
/api– Nơi lưu trữ giao ước hệ thống-
Định nghĩa bản chất:
/apikhông chứa mã nguồn xử lý HTTP handler mà chứa tài liệu và định nghĩa schema trung lập (Protobuf.proto, OpenAPI.yaml, GraphQL.graphql). -
Quy tắc đánh phiên bản (Versioning): Đặt thư mục theo cấu trúc
/api/proto/v1/,/api/openapi/v2/. -
Chiến lược code generation: Đặt file sinh mã (
*.pb.go, Swagger models) ở đâu? Phân tích 2 trường hợp: sinh trực tiếp vào/apivs đưa vào/internal/generated.
-
-
Phần 2: Thư mục
/web– Xử lý giao diện người dùng-
Sự khác biệt cốt lõi giữa
/assets(tài nguyên backend thuần túy: email template, fonts, seed data) và/web(giao diện web phục vụ người dùng cuối). -
Hai kịch bản kiến trúc phổ biến:
-
Kịch bản A: Go render trực tiếp bằng server-side template (
/web/template,/web/static). -
Kịch bản B: Go làm host chứa SPA bundle đã build sẵn từ frontend team (
/web/app/dist).
-
-
-
Phần 3: Tích hợp và phục vụ tài nguyên web
- Cấu hình router phân luồng HTTP:
/api/*cho backend services và/*cho static assets/templates.
- Cấu hình router phân luồng HTTP:
3. Cấu trúc thư mục minh họa
Plaintext
ecommerce-core/
├── api/
│ ├── openapi/
│ │ └── v1/
│ │ └── product.yaml # OpenAPI Specification
│ └── proto/
│ └── v1/
│ └── product.proto # gRPC Definition
├── web/
│ ├── static/
│ │ ├── css/
│ │ │ └── style.css
│ │ └── js/
│ │ └── app.js
│ └── template/
│ └── index.html # Server-side HTML template
├── cmd/
│ └── server/
│ └── main.go
├── go.mod
└── go.sum
4. Ví dụ code minh họa
api/proto/v1/product.proto (Định nghĩa giao ước gRPC):
Protocol Buffers
syntax = "proto3";
package product.v1;
option go_package = "ecommerce-core/internal/gen/product/v1;productv1";
service ProductService {
rpc GetProduct (GetProductRequest) returns (GetProductResponse);
}
message GetProductRequest {
string id = 1;
}
message ProductResponse {
string id = 1;
string name = 2;
double price = 3;
}
web/template/index.html:
HTML
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Cửa hàng trực tuyến</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1>Danh sách sản phẩm</h1>
<p>Chào mừng bạn đến với hệ thống backend viết bằng Go!</p>
</body>
</html>
cmd/server/main.go (Cấu hình route phục vụ cả giao ước OpenAPI, static assets và template):
Go
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// 1. Phục vụ tài liệu OpenAPI từ /api
apiDocFS := http.FileServer(http.Dir("./api/openapi"))
mux.Handle("/docs/", http.StripPrefix("/docs/", apiDocFS))
// 2. Phục vụ file tĩnh (CSS/JS) từ /web/static
staticFS := http.FileServer(http.Dir("./web/static"))
mux.Handle("/static/", http.StripPrefix("/static/", staticFS))
// 3. Phục vụ template HTML từ /web/template
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("./web/template/index.html")
if err != nil {
http.Error(w, "Lỗi server nội bộ", http.StatusInternalServerError)
return
}
_ = tmpl.Execute(w, nil)
})
log.Println("Server lắng nghe trên cổng :8080...")
_ = http.ListenAndServe(":8080", mux)
}
Bài 06: Vận hành và tự động hóa: Tổ chức thư mục /scripts, /deployments, /build
1. Mục tiêu bài học
-
Tách biệt logic mã nguồn ứng dụng khỏi quy trình đóng gói, tự động hóa và triển khai hạ tầng.
-
Thiết kế kịch bản CI/CD, Makefile, shell scripts chuẩn mực trong
/scripts. -
Đóng gói Docker đa giai đoạn (Multi-stage build) trong
/buildvà thiết lập hạ tầng Kubernetes/Docker Compose trong/deployments.
2. Nội dung chi tiết
-
Phần 1: Tự động hóa tác vụ với
/scripts-
Trách nhiệm: Chứa kịch bản build, test linting, kiểm tra bảo mật, database migration và code generation.
-
Phối hợp
Makefileở thư mục gốc và các file script thực thi phức tạp: Viết logic ngắn gọn trong Makefile, chuyển logic phân nhánh shell phức tạp vào/scripts/*.sh. -
Đảm bảo tính di động: Sử dụng shell POSIX chuẩn (
#!/usr/bin/env bash), quản lý quyền cấp phép thực thichmod +x.
-
-
Phần 2: Đóng gói và phát hành với
/build-
Mục đích: Lưu trữ cấu hình đóng gói phần mềm (Packaging & Continuous Integration).
-
Vị trí đặt file Docker: Tránh đặt rải rác ngoài thư mục root nếu có nhiều binary; đưa các cấu hình container và system package (systemd units, RPM/DEB specs) vào
/build/package/.
-
-
Phần 3: Quản lý cấu hình hạ tầng triển khai với
/deployments-
Phân loại hạ tầng: Docker-Compose môi trường local dev, Helm charts, Kubernetes manifests, cấu hình Terraform.
-
Tách bạch môi trường rõ ràng:
/deployments/k8s/dev/,/deployments/k8s/prod/.
-
3. Cấu trúc thư mục minh họa
Plaintext
microservice-hub/
├── build/
│ └── package/
│ ├── api.Dockerfile # Dockerfile cho Binary API
│ └── worker.Dockerfile # Dockerfile cho Binary Worker
├── deployments/
│ ├── docker-compose.dev.yaml # Chạy local testing (DB, Redis, App)
│ └── k8s/
│ ├── deployment.yaml
│ └── service.yaml
├── scripts/
│ ├── generate-proto.sh # Script tự động generate code từ proto
│ └── lint.sh # Script chạy golangci-lint
├── Makefile
├── cmd/
│ └── api/
│ └── main.go
├── go.mod
└── go.sum
4. Ví dụ code minh họa
scripts/generate-proto.sh (Kịch bản sinh mã tự động):
Bash
#!/usr/bin/env bash
set -euo pipefail
PROTO_DIR="api/proto/v1"
OUT_DIR="internal/gen/product/v1"
echo "==> Đang dọn dẹp thư mục code sinh cũ..."
mkdir -p "${OUT_DIR}"
echo "==> Bắt đầu biên dịch Protobuf qua protoc..."
protoc --proto_path="${PROTO_DIR}" \
--go_out="${OUT_DIR}" --go_opt=paths=source_relative \
--go-grpc_out="${OUT_DIR}" --go-grpc_opt=paths=source_relative \
"${PROTO_DIR}"/*.proto
echo "==> Hoàn tất sinh mã gRPC!"
build/package/api.Dockerfile (Multi-stage build tối ưu kích thước image và bảo mật):
Dockerfile
# Stage 1: Build binary bằng Go runtime đầy đủ
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code và compile binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/bin/server ./cmd/api/main.go
# Stage 2: Runtime image tối giản từ Scratch hoặc Distroless
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /
COPY --from=builder /app/bin/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
Makefile (Giao diện dòng lệnh trung tâm cho developer):
Makefile
.PHONY: build test lint proto run-dev
APP_NAME ?= api
lint:
@echo "Đang kiểm tra mã nguồn..."
@bash ./scripts/lint.sh
proto:
@echo "Đang sinh mã từ proto files..."
@bash ./scripts/generate-proto.sh
build:
@echo "Đang build binary cho $(APP_NAME)..."
@docker build -f build/package/$(APP_NAME).Dockerfile -t $(APP_NAME):latest .
run-dev:
@echo "Khởi động môi trường local development..."
@docker-compose -f deployments/docker-compose.dev.yaml up -d
All rights reserved