切換語言最容易製造的問題,不是不知道語法,而是把上一個語言的慣用思維直接帶過去

語法可以查文件,心智模型的切換要靠意識到自己在做什麼。


Anti-pattern 1:用 Python 思維寫 Go(Exception → Panic)

Python 工程師寫 Go 最常見的錯誤:

// ❌ Python 思維的 Go:error 不想處理就 panic
func getUser(id int) User {
    user, err := db.QueryUser(id)
    if err != nil {
        panic(err)  // 「反正 recover 會接住」
    }
    return user
}

panic 在 Go 的語意是「程式遇到了不應該發生的情況」——nil pointer、index out of bounds、程式邏輯錯誤。「找不到 user」是預期的業務情境,不是程式錯誤,不應該 panic。

// ✅ Go 的慣用寫法
func getUser(id int) (User, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        return User{}, fmt.Errorf("get user %d: %w", id, err)
    }
    return user, nil
}

Anti-pattern 2:用 Java 思維寫 JavaScript(Class Everywhere)

Java 工程師寫 JavaScript 的典型:

// ❌ Java 思維:到處建 class
class UserService {
    constructor(db) {
        this.db = db;
    }
 
    async getUser(id) {
        return await this.db.query(`SELECT * FROM users WHERE id = ?`, [id]);
    }
}
 
class UserController {
    constructor(userService) {
        this.userService = userService;
    }
 
    async handleGet(req, res) {
        const user = await this.userService.getUser(req.params.id);
        res.json(user);
    }
}

在 JavaScript,function 是一等公民,大多數情況下不需要 class 的包裝:

// ✅ JavaScript 慣用:function 組合
const createUserRepository = (db) => ({
    getUser: (id) => db.query(`SELECT * FROM users WHERE id = ?`, [id]),
});
 
const createUserHandler = (userRepo) => async (req, res) => {
    const user = await userRepo.getUser(req.params.id);
    res.json(user);
};

Class 在 JavaScript 不是錯的,但如果你的 class 只有方法、沒有狀態、沒有繼承,它只是 function 的無謂包裝。


Anti-pattern 3:在 Go 裡用全局狀態做依賴注入

Python 和 Ruby 的傳統做法:全局 singleton:

# Python 常見模式
db = Database.get_instance()
cache = Redis.get_instance()
 
def get_user(user_id):
    cached = cache.get(f"user:{user_id}")
    if cached:
        return cached
    user = db.query(...)
    cache.set(...)
    return user

帶到 Go 裡,工程師可能寫出:

// ❌ Go 裡的全局 singleton 反模式
var globalDB *sql.DB
var globalRedis *redis.Client
 
func GetUser(id int) (User, error) {
    // 直接用全局變數
    cached, _ := globalRedis.Get(ctx, fmt.Sprintf("user:%d", id)).Result()
    // ...
}

問題:難以測試(你沒辦法換掉全局的 DB),初始化順序難以控制,在並行測試時全局狀態互相污染。

// ✅ Go 的慣用做法:依賴透過參數或 struct 傳入
type UserService struct {
    db    *sql.DB
    cache *redis.Client
}
 
func (s *UserService) GetUser(id int) (User, error) {
    // 使用 s.db 和 s.cache,不是全局變數
}

Anti-pattern 4:在 Python asyncio 裡呼叫阻塞函式

# ❌ 在 async 函式裡呼叫同步阻塞操作
async def handle_request(request):
    user_id = request.user_id
 
    # 這是同步阻塞的資料庫呼叫!
    # 會卡死整個 event loop,所有其他 request 都要等
    user = psycopg2.connect(...).cursor().execute("SELECT ...")
 
    return user

asyncio 的 event loop 裡,任何同步阻塞的操作都會凍結整個 event loop。

# ✅ 使用 async 的資料庫驅動,或 run_in_executor
import asyncpg  # async 的 PostgreSQL 驅動
 
async def handle_request(request):
    conn = await asyncpg.connect(...)
    user = await conn.fetchrow("SELECT ...", request.user_id)
    return user
 
# 或者,用 run_in_executor 把同步函式移到 thread pool
async def handle_request_with_sync_db(request):
    loop = asyncio.get_event_loop()
    user = await loop.run_in_executor(None, sync_get_user, request.user_id)
    return user

Anti-pattern 5:在 Go channel 裡漏掉 goroutine leak

// ❌ goroutine leak:沒有機制結束 goroutine
func processItems(items []Item) {
    results := make(chan Result)
 
    for _, item := range items {
        go func(item Item) {
            results <- process(item)
        }(item)
    }
 
    // 只取第一個結果就返回
    first := <-results
    doSomething(first)
    // 函式返回了,但其他 goroutine 還在等 results channel 被接收
    // channel 沒人讀了,goroutine 永遠卡住 → goroutine leak
}
// ✅ 用 context 和 done channel 確保 goroutine 能退出
func processItems(ctx context.Context, items []Item) Result {
    results := make(chan Result, len(items))  // buffered channel
 
    for _, item := range items {
        go func(item Item) {
            select {
            case <-ctx.Done():
                return  // context 取消,goroutine 退出
            case results <- process(item):
            }
        }(item)
    }
 
    select {
    case <-ctx.Done():
        return Result{}
    case first := <-results:
        return first
    }
}

Anti-pattern 6:在靜態語言裡濫用 any/interface{}

// ❌ 把 Go 的型別系統當 Python 用
type Config map[string]interface{}  // 失去所有型別資訊
 
func processConfig(cfg Config) {
    port := cfg["port"].(int)  // runtime panic,如果 port 是 string
    host := cfg["host"].(string)
}
// ✅ 讓型別系統幫你
type Config struct {
    Port int    `yaml:"port"`
    Host string `yaml:"host"`
}
 
func processConfig(cfg Config) {
    // Port 和 Host 的型別在編譯時就確定了
    fmt.Printf("connecting to %s:%d\n", cfg.Host, cfg.Port)
}

interface{}(或 Go 1.18 的 any)有它的用途,但在 Go 裡大量使用,等於是在靜態語言裡模擬動態語言,既得不到靜態型別的好處,又多了 type assertion 的 runtime panic 風險。


識別自己在使用哪種 anti-pattern

切換語言時,一個簡單的問題能幫助自我檢查:

「在這個語言的標準庫或主流框架,它自己是怎麼寫的?」

Go 的標準庫到處是 func foo() (T, error),不是 panic
Node.js 的標準庫到處是 function,不是 class。
Python 的 asyncio 生態系到處是 async def,不是同步的 psycopg2。

主流的程式碼風格,是這個語言的設計者認為「最自然」的用法。偏離它,可能有正當理由,但也可能只是你在用舊語言的思維操作新語言。

下一篇:跨語言相關工具