0

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 /pkg thà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 /pkg trong hệ sinh thái Go

    • Tại sao /pkg cho phép các dự án bên ngoài go get và 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: /internal vs /pkg

    • Bảng đối chiếu trách nhiệm:

      • Chứa business logic (User, Order, Payment) →\rightarrow 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 →\rightarrow Có thể đưa vào /pkg.

    • Quy tắc vàng: “Bắt đầu mọi thứ trong /internal. Chỉ chuyển sang /pkg khi 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 util hoặc helpers.

    • 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ằng go: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ừ /assets và 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.

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí