Giao diện
📦 Packages, Visibility & Error Engineering
Go's package system và error handling philosophy khác biệt hoàn toàn so với Java/Python. Nắm vững module này để viết code maintainable và production-ready.
🔧 Packages & Modules
go.mod: The Heart of Dependency Management
bash
# Khởi tạo module
$ go mod init github.com/hpn/myproject
# go.mod được tạo:go
// go.mod
module github.com/hpn/myproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/redis/go-redis/v9 v9.3.0
)
require (
// indirect dependencies (auto-managed)
github.com/bytedance/sonic v1.10.2 // indirect
// ...
)Key Commands
bash
# Thêm dependency
$ go get github.com/gin-gonic/gin@latest
# Update tất cả dependencies
$ go get -u ./...
# Cleanup unused dependencies
$ go mod tidy
# Download dependencies to local cache
$ go mod download
# Vendor dependencies (offline builds)
$ go mod vendor📌 HPN Standard: Dependency Management
- Pin versions: Luôn commit
go.sumcùnggo.mod - Update regularly: Security patches quan trọng
- Review indirect deps: Chúng vẫn là attack surface
- Use
go mod tidy: Sau mỗi lần thêm/xóa imports
🔐 Visibility Rules
The Capitalization Rule
🎓 Professor Tom's Rule: Case = Visibility
Go dùng first letter capitalization thay vì keywords như public/private:
| First Letter | Visibility | Accessible From |
|---|---|---|
| Uppercase (User, GetName) | Public (Exported) | Any package |
| lowercase (user, getName) | Private (Unexported) | Same package only |
go
// package user
type User struct {
ID int64 // Public - accessible from other packages
Name string // Public
password string // private - only accessible in package user
}
func NewUser(name string) *User { // Public
return &User{
Name: name,
password: generatePassword(), // Call private function
}
}
func generatePassword() string { // private
return "secret123"
}
// From another package:
// u := user.NewUser("Raizo") ✅
// u.Name ✅
// u.password ❌ Compile error!
// user.generatePassword() ❌ Compile error!Struct Fields & JSON
go
type APIResponse struct {
Status string `json:"status"` // Public + JSON
Message string `json:"message"`
data []byte `json:"data"` // ⚠️ Won't be serialized!
}
// JSON output: {"status":"ok","message":"success"}
// 'data' field ignored because it's unexported🔄 Circular Dependency
Why Go Forbids It
🔥 Raizo's Warning: Circular Import = Compile Error
package a imports package b
package b imports package a
→ import cycle not allowedGo từ chối compile — không có runtime workaround như Python/Node.
The Problem
go
// ❌ WRONG: Circular dependency
// package user
import "myapp/order"
type User struct {
Orders []order.Order // Depends on order
}
// package order
import "myapp/user"
type Order struct {
Owner user.User // Depends on user → CYCLE!
}The Solution: Design Better Layers
go
// ✅ CORRECT: Dependency flows one way
// 1. Create shared types package (no dependencies)
// package models
type User struct {
ID int64
Name string
}
type Order struct {
ID int64
OwnerID int64 // Reference by ID, not full struct
Total float64
}
// 2. Service packages depend on models
// package user
import "myapp/models"
type UserService struct{}
func (s *UserService) GetUser(id int64) (*models.User, error) { ... }
// package order
import "myapp/models"
type OrderService struct{}
func (s *OrderService) GetOrdersByUser(userID int64) ([]models.Order, error) { ... }Dependency Direction:
┌─────────────────────────────────────────────────────────────┐
│ DEPENDENCY FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ user │ │ order │ │ api │ ← Higher │
│ │ service │ │ service │ │ handler │ Level │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ ▼ │
│ ┌────────────┐ │
│ │ models │ ← Shared, no deps │
│ │ (types) │ │
│ └────────────┘ │
│ │
│ Rule: Dependencies flow DOWN, never UP or SIDEWAYS │
│ │
└─────────────────────────────────────────────────────────────┘📁 Standard Project Layout (HPN Style)
myproject/
├── cmd/ # ← Entry points (main packages)
│ ├── api/
│ │ └── main.go # go run ./cmd/api
│ ├── worker/
│ │ └── main.go # go run ./cmd/worker
│ └── cli/
│ └── main.go # go run ./cmd/cli
│
├── internal/ # ← Private code (compiler enforced!)
│ ├── auth/ # Cannot be imported by other modules
│ │ ├── jwt.go
│ │ └── middleware.go
│ ├── database/
│ │ └── postgres.go
│ └── domain/
│ ├── user.go
│ └── order.go
│
├── pkg/ # ← Public library code (can be imported)
│ ├── validator/
│ │ └── validator.go # github.com/hpn/myproject/pkg/validator
│ └── httpclient/
│ └── client.go
│
├── api/ # ← API definitions (OpenAPI, protobuf)
│ └── openapi.yaml
│
├── configs/ # ← Configuration files
│ └── config.yaml
│
├── scripts/ # ← Build/deploy scripts
│ └── build.sh
│
├── go.mod
├── go.sum
└── README.md📌 HPN Standard: Folder Rules
| Folder | Purpose | Import Rule |
|---|---|---|
/cmd | Executables | main package only |
/internal | Private code | Compiler blocks external imports |
/pkg | Public libraries | Anyone can import |
/api | API specs | No Go code, just schemas |
⚠️ Error Handling: The Go Way
Errors Are Values
go
// error là interface đơn giản
type error interface {
Error() string
}
// Tạo custom error
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}
// Usage
func ValidateEmail(email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{
Field: "email",
Message: "must contain @",
}
}
return nil
}Error Wrapping (Go 1.13+)
go
import (
"errors"
"fmt"
)
// Wrap error với context
func GetUser(id int64) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
// %w wraps error, preserving chain
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return user, nil
}
// Caller có thể check original error
func HandleRequest(userID int64) {
user, err := GetUser(userID)
if err != nil {
// Check if underlying error is specific type
if errors.Is(err, sql.ErrNoRows) {
// User not found - return 404
return
}
// Unwrap to specific type
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
log.Printf("Postgres error: %s", pgErr.Code)
}
// Log full chain
log.Printf("Error: %v", err)
// Output: "Error: get user 123: sql: no rows in result set"
}
}errors.Is vs errors.As
go
// errors.Is - Check for specific error VALUE
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) {
// err hoặc any wrapped error == ErrNotFound
}
// errors.As - Check for specific error TYPE
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
// err hoặc any wrapped error là *pgconn.PgError
fmt.Println(pgErr.Code)
}💥 Panic vs Recover
🔥 Raizo's Critical Rule: Panic ≠ Exception
Panic is for UNRECOVERABLE errors only!
| Scenario | Use Panic? | Why |
|---|---|---|
| File not found | ❌ | Caller can handle |
| Invalid input | ❌ | Return validation error |
| Database down | ❌ | Retry or circuit break |
| Nil pointer bug | ✅ | Programmer error |
| Impossible state | ✅ | Assert invariants |
| Init failure | ✅ | App can't start |
When to Panic
go
// ✅ OK: Programmer error (should never happen in production)
func MustParseConfig(path string) *Config {
cfg, err := ParseConfig(path)
if err != nil {
panic(fmt.Sprintf("failed to parse config: %v", err))
}
return cfg
}
// ✅ OK: Impossible state (invariant violation)
func (s *StateMachine) Transition(event Event) {
switch s.state {
case StateA:
// ...
case StateB:
// ...
default:
panic(fmt.Sprintf("impossible state: %v", s.state))
}
}When NOT to Panic
go
// ❌ WRONG: Don't panic for expected errors
func GetUser(id int64) *User {
user, err := db.QueryUser(id)
if err != nil {
panic(err) // ❌ BAD! Caller can handle this!
}
return user
}
// ✅ CORRECT: Return error
func GetUser(id int64) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return user, nil
}Recover (Rarely Needed)
go
// recover() chỉ dùng trong HTTP handlers hoặc goroutine pools
func SafeHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}🚀 Pattern: Fail Fast, Return Early
📌 HPN Standard: Early Returns
Xử lý error cases trước, happy path cuối cùng:
go
// ❌ BAD: Nested if-else
func ProcessOrder(order *Order) (*Result, error) {
if order != nil {
if order.Items != nil && len(order.Items) > 0 {
if order.UserID > 0 {
// Finally, the actual logic buried deep
return processValidOrder(order)
} else {
return nil, errors.New("invalid user")
}
} else {
return nil, errors.New("no items")
}
} else {
return nil, errors.New("nil order")
}
}
// ✅ GOOD: Fail fast, return early
func ProcessOrder(order *Order) (*Result, error) {
// Guard clauses first
if order == nil {
return nil, errors.New("nil order")
}
if len(order.Items) == 0 {
return nil, errors.New("no items")
}
if order.UserID <= 0 {
return nil, errors.New("invalid user")
}
// Happy path - clean and obvious
return processValidOrder(order)
}📊 Summary
| Concept | Key Point |
|---|---|
| go.mod | Single source of truth for dependencies |
| Visibility | Uppercase = Public, lowercase = Private |
| Circular Deps | Forbidden — design layered architecture |
/internal | Compiler-enforced private code |
| Error Values | Return errors, don't panic |
| Error Wrapping | Use %w, check with Is/As |
| Panic | Unrecoverable errors only |
| Early Return | Guard clauses first, happy path last |
🎯 Module 2 Complete!
Chúc mừng! Bạn đã hoàn thành Go Foundations Module. Bạn đã nắm vững:
- ✅ Variables, Types & Memory
- ✅ Control Structures
- ✅ Functions & Defer
- ✅ Structs & Memory Layout
- ✅ Interfaces & Dependency Injection
- ✅ Packages & Error Handling