Bài 02: Chia sẻ thư viện an toàn với /pkg và ranh giới với /internal
1. Mục tiêu bài học
-
Phân biệt rạch ròi giữa code ứng dụng (Private -
/internal) và code thư viện chia sẻ (Public -/pkg). -
Hiểu cam kết tương thích ngược (Backward Compatibility) và Semantic Versioning khi public API qua
/pkg. -
Nhận diện các bẫy thiết kế: Biến
/pkgthành bãi rác chứa các hàm tiện ích vô thưởng vô phạt.
2. Nội dung chi tiết
-
Phần 1: Khái niệm và vị trí của
/pkgtrong hệ sinh thái Go-
Tại sao
/pkgcho phép các dự án bên ngoàigo getvà import trực tiếp. -
Tiêu chuẩn để một module được nằm trong
/pkg: Độc lập, không chứa business logic của domain, có API ổn định, có tài liệu và test coverage cao. -
Các dự án mẫu mực sử dụng
/pkg: Kubernetes (k8s.io/client-go), Prometheus.
-
-
Phần 2: Ranh giới quyết định:
/internalvs/pkg-
Bảng đối chiếu trách nhiệm:
-
Chứa business logic (User, Order, Payment) Bắt buộc nằm trong
/internal. -
Chứa wrapper SDK, thư viện mã hóa, logger tùy biến, client HTTP tái sử dụng Có thể đưa vào
/pkg.
-
-
Quy tắc vàng: “Bắt đầu mọi thứ trong
/internal. Chỉ chuyển sang/pkgkhi thực sự có nhu cầu chia sẻ cho bên ngoài hoặc dự án thứ hai.”
-
-
Phần 3: Tránh Anti-pattern
pkg/utils-
Tác hại của việc gom file linh tinh vào một package
utilhoặchelpers. -
Cách chia nhỏ package theo mục đích chuyên biệt:
pkg/hasher,pkg/httputil,pkg/validator.
-
3. Cấu trúc thư mục minh họa
Plaintext
payment-gateway/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ └── billing/ # Logic thanh toán nội bộ (Không ai ngoài repo import được)
│ └── service.go
├── pkg/
│ └── signature/ # Thư viện ký số HMAC SHA-256 (Bên ngoài có thể import)
│ ├── signer.go
│ └── signer_test.go
├── go.mod
└── go.sum
4. Ví dụ code minh họa
pkg/signature/signer.go (Public library hoàn toàn độc lập với domain):
Go
package signature
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
)
var ErrEmptyKey = errors.New("secret key cannot be empty")
type Signer struct {
secretKey []byte
}
func NewSigner(secretKey string) (*Signer, error) {
if secretKey == "" {
return nil, ErrEmptyKey
}
return &Signer{secretKey: []byte(secretKey)}, nil
}
func (s *Signer) Sign(payload []byte) string {
h := hmac.New(sha256.New, s.secretKey)
h.Write(payload)
return hex.EncodeToString(h.Sum(nil))
}
internal/billing/service.go (Dùng module từ /pkg phục vụ nghiệp vụ):
Go
package billing
import (
"fmt"
"payment-gateway/pkg/signature"
)
type Service struct {
signer *signature.Signer
}
func NewService(signer *signature.Signer) *Service {
return &Service{signer: signer}
}
func (s *Service) ProcessWebhook(rawPayload []byte) string {
// Ký payload đơn hàng để gửi cho đối tác bên ngoài xác thực
signatureStr := s.signer.Sign(rawPayload)
return fmt.Sprintf("X-Signature: %s", signatureStr)
}
Bài 04: Quản lý cấu hình, môi trường và tài nguyên với /configs và /assets
1. Mục tiêu bài học
-
Tổ chức các file cấu hình ứng dụng (
/configs) tuân thủ nguyên tắc Twelve-Factor App. -
Tách biệt giữa cấu hình tĩnh (file mẫu, local config) và nạp cấu hình động qua biến môi trường.
-
Xử lý tài nguyên tĩnh (
/assets) như HTML email template, migration SQL, ảnh mẫu bằnggo:embed.
2. Nội dung chi tiết
-
Phần 1: Kiến trúc thư mục
/configs-
File gì nên đưa vào
/configs? File template mẫu (config.yaml.example), cấu hình local development, schema định nghĩa cấu hình. -
Điều cấm kỵ: Tuyệt đối không commit file chứa credentials thật (
secret.yaml,.env.production) lên Git. -
Kỹ thuật ánh xạ cấu hình: Phối hợp nạp file YAML/JSON với ghi đè (override) từ Environment Variables (sử dụng thư viện phổ biến như Viper hoặc
caarlos0/env).
-
-
Phần 2: Quản lý tài nguyên với
/assets-
Vị trí lưu trữ: File SQL migrations, template email, localization files (i18n), static assets.
-
So sánh cơ chế đóng gói: Đọc file từ disk thông thường vs Đóng gói nhúng trực tiếp vào binary qua package tiêu chuẩn
embed(//go:embed).
-
-
Phần 3: Tối ưu phân phối binary độc lập
- Gom toàn bộ logic nạp template từ
/assetsvà biến cấu hình thành một file binary duy nhất không phụ thuộc đường dẫn relative path khi deploy container.
- Gom toàn bộ logic nạp template từ
3. Cấu trúc thư mục minh họa
Plaintext
notification-service/
├── assets/
│ └── templates/
│ └── welcome.html # File template email tĩnh
├── configs/
│ ├── config.yaml # File cấu hình phát triển cục bộ
│ └── config.yaml.example # Bản mẫu cấu hình commit lên Git
├── cmd/
│ └── sender/
│ └── main.go
├── internal/
│ ├── config/ # Logic parse & validate config
│ │ └── config.go
│ └── mailer/ # Logic render email sử dụng embed
│ └── mailer.go
├── go.mod
└── go.sum
4. Ví dụ code minh họa
configs/config.yaml:
YAML
server:
port: 8080
email:
from: "no-reply@company.com"
assets/templates/welcome.html:
HTML
<!DOCTYPE html>
<html>
<body>
<h1>Chào mừng, {{.Name}}!</h1>
<p>Cảm ơn bạn đã đăng ký tài khoản tại hệ thống của chúng tôi.</p>
</body>
</html>
internal/config/config.go (Struct hóa cấu hình kết hợp fallback):
Go
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Server struct {
Port string `yaml:"port"`
} `yaml:"server"`
Email struct {
From string `yaml:"from"`
} `yaml:"email"`
}
func LoadConfig(path string) (*Config, error) {
cfg := &Config{}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
if err := yaml.NewDecoder(file).Decode(cfg); err != nil {
return nil, err
}
// Ưu tiên ghi đè từ biến môi trường nếu có
if envPort := os.Getenv("APP_PORT"); envPort != "" {
cfg.Server.Port = envPort
}
return cfg, nil
}
internal/mailer/mailer.go (Sử dụng //go:embed nhúng template từ /assets):
Go
package mailer
import (
"bytes"
"embed"
"html/template"
)
//go:embed ../../assets/templates/*.html
var templateFS embed.FS
type Mailer struct {
tmpl *template.Template
}
func NewMailer() (*Mailer, error) {
tmpl, err := template.ParseFS(templateFS, "assets/templates/*.html")
if err != nil {
return nil, err
}
return &Mailer{tmpl: tmpl}, nil
}
func (m *Mailer) RenderWelcome(name string) (string, error) {
var buf bytes.Buffer
data := map[string]string{"Name": name}
if err := m.tmpl.ExecuteTemplate(&buf, "welcome.html", data); err != nil {
return "", err
}
return buf.String(), nil
}
All rights reserved