ข้ามไปยังเนื้อหา

วิธีอ่านโค้ด Go ที่คุณไม่ได้เขียนเอง

ทุกโมดูลก่อนหน้านี้สอนให้คุณเขียน Go โมดูลนี้สอนอีกครึ่งหนึ่งของงานที่คอร์สสัญญาไว้ตั้งแต่ต้น นั่นคือการอ่านโค้ด Go ที่คนอื่น — หรือ AI — เขียนไว้ แล้วตัดสินได้ว่าถูกหรือผิด AI assistant จะส่งโค้ดที่ compile ผ่าน รันได้ และหน้าตาเหมือนตัวอย่าง idiomatic ที่คุณเรียนมาทุกประการ แต่การ compile ผ่านกับรันได้ ไม่ใช่สิ่งเดียวกับ “ถูกต้อง” สี่หัวข้อด้านล่างคือลำดับที่ reviewer Go ที่มีประสบการณ์ใช้อ่านโค้ดจริง และทุกบทถัดไปในโมดูลนี้จะเจาะลึกแต่ละส่วน

ก่อนอ่าน body ของ function แม้แต่บรรทัดเดียว ให้อ่าน signature ก่อน ใน Go signature ไม่ใช่แค่การตกแต่ง — มันคือ contract ที่ compiler บังคับใช้จริง และมันตอบคำถามส่วนใหญ่ที่คุณต้องไล่ตาม body ไปหาคำตอบได้อยู่แล้ว:

  • parameter ตัวแรกเป็น context.Context ไหม? ถ้าใช่ แปลว่า call นี้ cancel ได้ และอาจกำลังรันพร้อมกับสิ่งที่ตั้ง timeout ให้มันได้
  • receiver เป็น pointer (*T) หรือ value (T)? pointer receiver แก้ไขค่าที่คุณส่งเข้าไปได้ ส่วน value receiver ทำไม่ได้ — มันทำงานบน copy
  • มี error เป็นค่า return ตัวสุดท้ายไหม? ถ้ามี แปลว่า function นี้มีจุดที่ fail ได้ และธรรมเนียมของ Go จะวาง signal ความล้มเหลวไว้ในที่ที่คุณ destructure ทิ้งไปเฉยๆ ไม่ได้
  • parameter และ return type เป็น interface หรือ concrete type? function ที่รับ io.Reader สัญญาว่าทำงานกับอะไรก็ได้ที่ satisfy มัน ส่วน function ที่รับ *os.File ได้จำกัดประโยชน์ของตัวเองแล้ว ไม่ว่าจะตั้งใจหรือไม่ก็ตาม

ทั้งหมดนี้ไม่ต้องรันโค้ดเลย reviewer ที่อ่าน func (a *Account) Withdraw(ctx context.Context, amount int) error ก่อนดู body จะรู้ทันทีว่า: นี่แก้ไข a, cancel ได้, และ fail ได้ — สิ่งที่ต้องเช็กต่อไปคือทุก caller เคารพทั้งสามข้อนี้หรือเปล่า

ธรรมเนียม if err != nil ของ Go ทำให้ error path มักเป็นส่วนใหญ่ของบรรทัดใน function และก็เป็น path ที่โค้ดจาก AI มักผิดพลาดแบบแนบเนียนที่สุดด้วย — เพราะ happy path คือสิ่งที่ training data ให้รางวัลว่าดูถูกต้อง การอ่าน function ให้ดีหมายถึงการไล่ตามทุก return ที่คืน error ที่ไม่ใช่ nil ไม่ใช่แค่ตัวสุดท้าย:

  • error มาจากไหน — จาก stdlib call, จาก error ระดับล่างที่ wrap มา, หรือจากการเทียบกับ sentinel?
  • มันถูก return, log, หรือทั้งสองอย่าง? การจัดการสองรอบ (log และ return) มักแปลว่า caller จะ log ซ้ำอีกที
  • early return ทำให้ resource รั่วไหม ทั้งที่บรรทัดถัดไปควรจะปิดมัน?
  • zero value ถูก return คู่กับ error ไหม แล้วมี caller ตัวไหนใช้ zero value นั้นโดยไม่เช็ก error ก่อนหรือเปล่า?

บทที่อธิบายกลไกเบื้องหลังคำถามเหล่านี้คือ Error Values และ Wrapping Errors — โมดูลนี้สมมติว่าคุณรู้จัก %w, errors.Is, และ errors.As แล้ว และจะฝึกให้คุณจับจุดที่โค้ด AI ละเมิดกฎเหล่านั้นแทน

ทุกครั้งที่มี keyword go มันเริ่มสิ่งหนึ่งที่ lifetime ของมันต้องถูกติดตามแยกจาก function ที่เรียกมัน การอ่านโค้ด concurrent หมายถึงต้องตอบให้ได้ ทุก goroutine ว่า: อะไรทำให้มันหยุด และใครรอมันหยุด?

  • มันอ่านหรือเขียน channel ที่อาจไม่มีวันพร้อมไหม — นี่คือ leak ที่รอเกิด?
  • มันถือ context.Context ที่ถูก cancel ได้ไหม — และมันเช็ก ctx.Done() จริงหรือเปล่า?
  • มันถูกติดตามด้วย sync.WaitGroup ไหม และ Add ถูกเรียกก่อน goroutine เริ่ม จาก goroutine เดียวกับที่จะเรียก Wait หรือเปล่า?
  • ถ้ามัน panic มี recover ที่เข้าถึงได้จากภายใน goroutine เดียวกันไหม — recover ที่อื่นช่วยมันไม่ได้

Goroutines, Channels, และ The sync package ครอบคลุมเรื่อง semantics ไว้แล้ว ส่วน catalog ในโมดูลนี้จะแสดงให้เห็นชัดๆ ว่าโค้ด AI ละเมิดแต่ละกฎอย่างไร

อะไรก็ตามที่มี method Close, Unlock, หรือ Done มีเจ้าของ และ Go ไม่เคยบังคับตอน compile ว่าใครคือเจ้าของ — วินัยทั้งหมดอยู่ที่โค้ดที่คุณกำลังอ่าน สำหรับทุก resource ที่ถูก acquire ให้ถามว่า:

  • ใครเรียก release ที่คู่กัน — ตัว acquirer เอง หรือสิ่งที่ acquirer ส่ง resource ต่อให้?
  • release ถูกรับประกันในทุก path ไหม รวมถึง early return และ panic — ปกติผ่าน defer ที่วางทันทีหลัง acquire สำเร็จ?
  • resource ถูกใช้หลังจาก release แล้วได้ไหม — เช่น รับค่าจาก channel ที่ปิดไปแล้ว, อ่านไฟล์หลัง Close, แตะ mutex หลังจาก struct ที่ถือมันถูก copy?
package main
import (
"context"
"errors"
"fmt"
)
// The signature alone tells you three things before you read a line of the
// body: ctx first means the call is cancellable, the pointer receiver means
// Withdraw mutates the Account, and the error return means something here
// can fail — go find out what before you trust a caller that ignores it.
type Account struct {
Balance int
}
func (a *Account) Withdraw(ctx context.Context, amount int) error {
if err := ctx.Err(); err != nil {
return err // follow the error path: cancellation short-circuits the withdrawal
}
if amount > a.Balance {
return errors.New("insufficient funds")
}
a.Balance -= amount
return nil
}
func main() {
acc := &Account{Balance: 100}
ctx := context.Background()
if err := acc.Withdraw(ctx, 30); err != nil {
fmt.Println("withdraw failed:", err)
}
fmt.Println("balance after first withdraw:", acc.Balance)
if err := acc.Withdraw(ctx, 1000); err != nil {
fmt.Println("withdraw failed:", err) // easy branch to miss when skimming the happy path
}
fmt.Println("balance after second withdraw:", acc.Balance)
}
ลำดับการอ่าน function ที่คุณไม่ได้เขียนเอง:
1. Signature → ctx ไหม? pointer หรือ value receiver? มี error return ไหม? param เป็น interface หรือ concrete?
2. Error path → ทุก return ที่ไม่ใช่ nil ไม่ใช่แค่ตัวสุดท้าย
3. Goroutine → อะไรหยุดแต่ละ `go` statement และใครรอมัน
4. Ownership → ใครเรียก Close/Unlock/cancel และมันถูกรับประกันในทุก path ไหม
การกระโดดไปดูแค่ "compile ผ่านไหม รันได้ไหม" ไม่ตอบคำถามพวกนี้เลยสักข้อ
สิ่งที่ได้ประโยชน์ต้นทุน
อ่าน signature ก่อนจับการละเมิด contract ได้ก่อนไล่ branch แม้แต่อันเดียวต้องรู้ว่า context.Context, pointer receiver, และ error return สัญญาอะไรไว้จริงๆ
ไล่ error path ทั้งหมดเจอ error ที่ถูกกลืน/จัดการซ้ำ/รั่วที่โค้ด AI ซ่อนไว้ช้ากว่าการอ่านแค่ happy path
ติดตาม goroutine lifetime ชัดเจนเจอ leak และ panic ที่หายไปก่อนมันจะปลุกคุณตอนตีสามต้องจำ state เยอะขึ้นระหว่างอ่าน
เช็ก ownership ทุกครั้งที่ acquireเจอ resource leak และ bug แบบใช้หลัง closeน่าเบื่อกับโค้ดที่มี helper function เล็กๆ เยอะ
  • “compile ผ่านแล้ว output ก็ดูถูก แปลว่าถูกต้อง” ทุก pattern ใน catalog ของโมดูลนี้ compile ผ่านและให้ output ที่ดูสมเหตุสมผลบน happy path ของมัน — นั่นแหละคือสิ่งที่ทำให้มันอันตราย
  • “อ่าน happy path ก็เท่ากับอ่าน function แล้ว” ใน Go แบบ idiomatic บรรทัดจัดการ error มักมีมากกว่าบรรทัด happy path การข้ามมันไปคือการข้ามส่วนใหญ่ของ function
  • “ถ้าคนเขียนก็โอเค ต้องระวังแค่โค้ด AI” ทุก pattern ในนี้ก็เจอในโค้ดที่คนเขียนเองเหมือนกัน AI แค่ผลิตมันออกมาในปริมาณที่เยอะกว่าและดูมั่นใจกว่ามาก
  • “หน้าที่ reviewer คือหา bug ด้วยการรันโค้ด” bug ส่วนใหญ่ใน catalog ของโมดูลนี้ไม่ crash กับ input ที่คุณลองเฉยๆ มันจะ crash ทีหลัง ตอน load สูง หรือตอนถูก cancel
คุณเห็น func Save(ctx context.Context, r io.Reader) (*Record, error) ก่อนจะอ่าน body คุณรู้อะไรไปแล้วบ้าง?
ทำไม error path มักสำคัญกว่าที่ต้องอ่านให้ละเอียด เมื่อเทียบกับ happy path?
สำหรับ goroutine ที่เริ่มด้วย go worker(ctx) คำถามไหนสำคัญที่สุดตอน review มัน?
reviewer ควรเช็กอะไรต่อเกี่ยวกับทุก caller ของ open?
func open() (*os.File, error) {
    f, err := os.Open("data.txt")
    return f, err
}