Skip to content

📦 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 maintainableproduction-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

  1. Pin versions: Luôn commit go.sum cùng go.mod
  2. Update regularly: Security patches quan trọng
  3. Review indirect deps: Chúng vẫn là attack surface
  4. 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 LetterVisibilityAccessible 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 allowed

Go 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

FolderPurposeImport Rule
/cmdExecutablesmain package only
/internalPrivate codeCompiler blocks external imports
/pkgPublic librariesAnyone can import
/apiAPI specsNo 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!

ScenarioUse Panic?Why
File not foundCaller can handle
Invalid inputReturn validation error
Database downRetry or circuit break
Nil pointer bugProgrammer error
Impossible stateAssert invariants
Init failureApp 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

ConceptKey Point
go.modSingle source of truth for dependencies
VisibilityUppercase = Public, lowercase = Private
Circular DepsForbidden — design layered architecture
/internalCompiler-enforced private code
Error ValuesReturn errors, don't panic
Error WrappingUse %w, check with Is/As
PanicUnrecoverable errors only
Early ReturnGuard 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

➡️ Chuẩn bị cho Module 3: Concurrency & System Scaling

🔥 Coming Next: The Power of Go

Module 3 sẽ cover những gì làm Go trở nên đặc biệt:

  • Goroutines — Lightweight concurrency (2KB per goroutine)
  • Channels — CSP model for safe communication
  • Context — Cancellation, timeouts, request-scoped values
  • sync Primitives — Mutex, WaitGroup, atomic operations
  • Concurrency Patterns — Worker pools, fan-out/fan-in, pipelines

"Do not communicate by sharing memory; share memory by communicating." — Go Proverb