วิธีอ่านโค้ด Go ที่คุณไม่ได้เขียนเอง
ทุกโมดูลก่อนหน้านี้สอนให้คุณเขียน Go โมดูลนี้สอนอีกครึ่งหนึ่งของงานที่คอร์สสัญญาไว้ตั้งแต่ต้น นั่นคือการอ่านโค้ด Go ที่คนอื่น — หรือ AI — เขียนไว้ แล้วตัดสินได้ว่าถูกหรือผิด AI assistant จะส่งโค้ดที่ compile ผ่าน รันได้ และหน้าตาเหมือนตัวอย่าง idiomatic ที่คุณเรียนมาทุกประการ แต่การ compile ผ่านกับรันได้ ไม่ใช่สิ่งเดียวกับ “ถูกต้อง” สี่หัวข้อด้านล่างคือลำดับที่ reviewer Go ที่มีประสบการณ์ใช้อ่านโค้ดจริง และทุกบทถัดไปในโมดูลนี้จะเจาะลึกแต่ละส่วน
อ่าน type และ signature ก่อนเสมอ
หัวข้อที่มีชื่อว่า “อ่าน type และ signature ก่อนเสมอ”ก่อนอ่าน 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 เคารพทั้งสามข้อนี้หรือเปล่า
ตาม error path ให้ครบ
หัวข้อที่มีชื่อว่า “ตาม error path ให้ครบ”ธรรมเนียม 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 ละเมิดกฎเหล่านั้นแทน
ตาม goroutine lifetime
หัวข้อที่มีชื่อว่า “ตาม goroutine lifetime”ทุกครั้งที่มี 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 ละเมิดแต่ละกฎอย่างไร
ตาม resource ownership
หัวข้อที่มีชื่อว่า “ตาม resource ownership”อะไรก็ตามที่มี 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)} Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ลำดับการอ่าน 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