ในฝั่ง JVM เมื่อคุณเขียนระบบโอนเงิน คุณมักเขียนแบบนี้:
// Kotlin / Spring Boot style
fun transferMoney(from: String, to: String, amount: Long) {
if (balance < amount) {
throw InsufficientFundsException("Account $from has insufficient funds")
}
// Logic อื่นๆ
}
เมื่อเกิด throw Exception เบื้องหลัง JVM จะทำ Stack Unwinding: ไล่จับ Stack Trace ทั้งหมด, หยุดการทำงานของ Thread ชั่วคราว, จัดสรร Object ขนาดใหญ่ลง Heap, แล้วกระโดดข้าม Call Stack หลายชั้นขึ้นไปหาบล็อก catch ซึ่งทำให้เกิดปัญหาใหญ่ 2 ประการ:
ภาษา Go จึงเลือกตัดแนวคิด Exception ทิ้งไป และใช้ปรัชญา "Errors are Values": Error ก็เป็นเพียง "ข้อมูล (Data Value)" ก้อนหนึ่งที่ฟังก์ชันส่งคืนกลับมาคู่กับผลลัพธ์ผ่าน Multi-value return ให้นักพัฒนาตัดสินใจจัดการทันที ณ จุดเกิดเหตุ
Error() string เมธอดเดียวerrors.New) ที่ส่งสัญญาณสถานะเฉพาะตัว เช่น ErrInsufficientFunds เพื่อให้ฝั่งเรียกใช้ตรวจสอบด้วยเครื่องหมาย ==Error() string เพื่อบรรจุ Metadata ทางธุรกิจ เช่น AccountID, DeficitAmount, Timestampif err != nil แล้วจัดการ Error ออกจากฟังก์ชันทันที ทำให้โค้ดส่วน Business Logic ไม่ต้องเยื้องเข้าไปลึกในวงเล็บปีกกาเรียนรู้วิธีการเขียนฟังก์ชันคืนค่า Error และการสร้าง Sentinel / Custom Error แบบทีละขั้นตอน:
ใน Go เมื่อฟังก์ชันอาจทำงานล้มเหลว ลายเซ็นต์มาตรฐานคือต้องคืนค่า error เป็นพารามิเตอร์สุดท้ายเสมอ:
package banking
import (
"errors"
"fmt"
)
// ฟังก์ชันคืนค่าสองตัว: (ยอดเงินคงเหลือ int64, ข้อผิดพลาด error)
func Withdraw(currentBalance, amount int64) (int64, error) {
if amount <= 0 {
// สร้าง error ง่ายๆ ด้วย errors.New
return currentBalance, errors.New("withdrawal amount must be greater than zero")
}
if currentBalance < amount {
// จัด Format ข้อความ error ด้วย fmt.Errorf
return currentBalance, fmt.Errorf("insufficient funds: balance is %d, requested %d", currentBalance, amount)
}
newBalance := currentBalance - amount
return newBalance, nil // nil หมายถึงทำงานสำเร็จ ไม่มี Error!
}
func ProcessTransaction() {
balance := int64(1000)
// สไตล์ Idiomatic Go: รับค่าคู่ แล้วตรวจ if err != nil ทันที
newBalance, err := Withdraw(balance, 1500)
if err != nil {
// จัดการ Error ทันที (เช่น บันทึก Log, Rollback, แจ้งเตือน)
fmt.Println("Transaction Failed:", err)
return // Early return จบงาน
}
// Happy Path: โค้ดที่ทำงานสำเร็จจะอยู่ชิดซ้าย ไม่อยู่ใน else block!
fmt.Printf("Withdrawal Success! New Balance: %d\n", newBalance)
}
นิยมตั้งชื่อขึ้นต้นด้วย Err เป็นตัวแปร Global ระดับ Package เพื่อให้ Caller เช็คเงื่อนไขได้:
package banking
import "errors"
// ประกาศ Sentinel Errors ไว้ที่ระดับ Package
var (
ErrAccountNotFound = errors.New("banking: account not found")
ErrInsufficientFunds = errors.New("banking: insufficient balance")
ErrAccountLocked = errors.New("banking: account is locked")
)
func Debit(accountID string, amount int64) error {
if accountID != "ACC-01" {
return ErrAccountNotFound
}
// สมมติยอดเงินไม่พอ
return ErrInsufficientFunds
}
func CallDebit() {
err := Debit("ACC-01", 500)
// ตรวจสอบด้วยเครื่องหมาย == ตรงๆ
if err == ErrInsufficientFunds {
fmt.Println("Please top-up your account")
}
}
สร้าง Struct ธรรมดา และผูกเมธอด Error() string เข้าไป:
type FundError struct {
AccountID string
Balance int64
Attempted int64
}
// ผูกเมธอด Error() string เพื่อให้ Struct นี้กลายเป็น error interface โดยอัตโนมัติ!
func (e FundError) Error() string {
return fmt.Sprintf("account %s deficit: balance %d but attempted %d",
e.AccountID, e.Balance, e.Attempted)
}
func ExecuteTransfer(from string, balance, amt int64) error {
if balance < amt {
return FundError{
AccountID: from,
Balance: balance,
Attempted: amt,
}
}
return nil
}
ใน Go ตัวแปรชนิด error ไม่ใช่พอยน์เตอร์เดี่ยวๆ และไม่ใช่สตริง แต่คือ Interface Container ที่มีขนาด 16 Bytes บนระบบ 64-bit โครงสร้างใต้กระโปรงคือ Struct iface:
// โครงสร้างภายใน Go Runtime (src/runtime/runtime2.go)
type iface struct {
tab *itab // 8 Bytes: ชี้ไปยัง Type Metadata และ Function Pointer ของ Error()
data unsafe.Pointer // 8 Bytes: ชี้ไปยังข้อมูลจริง (เช่น ชี้ไปที่ String หรือ Custom Struct)
}
มาดูโค้ดจริงของ Standard Library ในไฟล์ src/errors/errors.go:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
err1 := errors.New("fail") กับ err2 := errors.New("fail")err1 == err2 ผลลัพธ์จะเป็น false เพราะมันเปรียบเทียบ Memory Address ของ data pointer ไม่ใช่การเทียบตัวหนังสือ! ทำให้แต่ละ Package สามารถสร้าง Sentinel Error ของตัวเองได้โดยไม่ชนกับคนอื่น
พิจารณาลำดับการทำงานเมื่อเกิดข้อผิดพลาดในการโอนเงิน:
นี่คือบั๊กที่วิศวกรที่ย้ายมาจาก Java/Kotlin ตกม้าตายบ่อยที่สุด:
type CustomError struct {
Code int
}
func (c *CustomError) Error() string {
return fmt.Sprintf("code: %d", c.Code)
}
// BUG FUNCTION: ประกาศ return type เป็น Custom Pointer แต่ฟังก์ชันภายนอกรับเป็น error interface!
func Process() error {
var err *CustomError = nil // ตัวแปรพอยน์เตอร์มีค่าเป็น nil
if someCondition {
err = &CustomError{Code: 404}
}
return err // ⚠️ หายนะเกิดขึ้นตรงนี้!
}
func main() {
err := Process()
if err != nil {
// บรรทัดนี้จะถูกรันเสมอ! แม้ว่า err จะเป็น nil pointer มาก็ตาม!
fmt.Println("Error occurred:", err)
}
}
iface ได้ไหมครับ? ตัวแปร error คือ Interface Container ที่เก็บ 2 ช่อง:tab: ชี้ไปยังประเภท *CustomError (มีค่า! ไม่ใช่ nil)data: มีค่า nilnil ก็ต่อเมื่อ ทั้ง Type (`tab`) และ Value (`data`) เป็น nil ทั้งคู่! ในเคสนี้ tab มีข้อมูลประเภทอยู่ ทำให้ err != nil เป็นจริงเสมอ!error และไม่มีความผิดพลาด ให้สั่ง `return nil` เปล่าๆ ออกมาเสมอ ห้ามคืนตัวแปร Custom Struct Pointer ที่เป็น nil!
Sentinel Error มีไว้สำหรับบอก **"สถานะที่คาดการณ์ได้ว่าจะเกิด"** เช่น ยอดเงินไม่พอ, บัญชีถูกล็อค, บัตรหมดอายุ ห้ามใช้ Sentinel Error กับข้อผิดพลาดเชิงลึกที่ต้องส่งต่อ Dynamic Metadata (เช่น เกิด Network Timeout กี่วินาที) เพราะถ้าใช้ Sentinel Error เราจะไม่สามารถแทรกข้อมูลเพิ่มเติมลงไปได้
ErrNegativeAmount = errors.New("withdrawal amount must be positive")ErrInsufficientFunds = errors.New("insufficient balance for withdrawal")ProcessWithdrawal(balance, amount int64) (int64, error):
amount <= 0 ให้คืนค่า balance, ErrNegativeAmountbalance < amount ให้คืนค่า balance, ErrInsufficientFundsbalance - amount, nilTxError มีฟิลด์: TxID string, AccountID string, Code int, และ Message stringError() string ให้กับ TxError โดยคืนค่าในรูปแบบ: "tx [TxID] account [AccountID] error (code Code): Message" (ใช้ fmt.Sprintf)ValidateTx(txID, accID string, amount int64) error:
amount <= 0 คืนค่า Struct TxError ที่มี Code = 400 และ Message = "invalid amount"nilvar appErr *AppError = nil แล้วส่งออกไปผ่าน error interface ทำให้ผลลัพธ์ err != nil เป็นจริงเสมอแม้ไม่มี errorExecutePayment(amount int64) error ให้คืนค่า nil เปล่าๆ ออกมาเมื่อไม่มีข้อผิดพลาด