คุยกันเรื่อง Writing Better Go: Lessons from 10 Code Reviews
Source: Dev.to
Handle Errors
- อย่าเมิน error ไปแบบเงียบ ๆ (เช่นใช้
_รับค่า error) - อย่าคิดว่า error นี้ “รับได้” เช่น
if err != nil { return nil } - เมื่อเจอ error ให้ตรวจสอบและจัดการทันที เช่น log แล้วถือว่า error นั้นจัดการแล้ว ไม่ต้องส่งต่อให้คนอื่น
- อย่าโวยซ้ำ — ถ้า log แล้วยัง
returnให้คนอื่นไป log อีกครั้ง จะทำให้ log ซ้ำซ้อน
ตัวอย่าง (bad vs. good)
// Bad
if err != nil {
slog.Error(err)
return err
}
// Good
if err != nil {
slog.Error(err)
return nil // หรือจัดการ error ที่นี่แล้วไม่ต้องส่งต่อ
}
ลดภาระให้ฝั่งที่เรียกใช้
return result, nil– คืนผลลัพธ์และไม่มี error ทำให้ผู้เรียกใช้เข้าใจง่ายreturn nil, err– มี error ให้จัดการในขณะที่ไม่มีผลลัพธ์return nil, nil– แย่ เพราะผู้เรียกต้องตีความว่าผลลัพธ์หรือ error ใด ๆ ควรหลีกเลี่ยงreturn result, err– แย่ เพราะผู้เรียกต้องตรวจสอบทั้งสองค่าเพื่อทราบว่าใช้งานได้หรือไม่
การใช้ Interface
- อย่าเริ่มด้วยการสร้าง interface ก่อน (มักมาจากภาษาอื่นเช่น Java) หรือเพื่อให้ mock ใน test เท่านั้น
- ใช้ accept interfaces, return concrete types – สื่อสารผ่าน interface แต่คืนค่าเป็น concrete type
- เริ่มจากการใช้ concrete type จนถึงจุดที่จำเป็นต้องใช้ interface จริง ๆ
- Litmus Test: ถ้าทดสอบได้โดยไม่ใช้ interface ก็ไม่จำเป็นต้องสร้าง
- อย่าสร้าง interface เพียงเพื่อให้เทสได้ – พยายามเขียน test ที่ไม่ต้องพึ่ง interface
Mutexes Before Channels
- การใช้ channel ทำให้โค้ดซับซ้อนและเสี่ยงต่อ panic (เช่น close channel ที่เปิดอยู่, ส่งไปยัง channel ที่ปิดแล้ว) หรือ deadlock
- เริ่มด้วยวิธีง่าย ๆ ก่อน: ใช้
sync.Mutex,sync.WaitGroupจัดการ shared state - เพิ่ม goroutine หรือ channel เฉพาะเมื่อ profiling แสดงว่ามีคอขวดจริง ๆ
- ใช้ channel เฉพาะเมื่อจัดการที่ซับซ้อนจริง ๆ ไม่ใช่สำหรับงานง่าย ๆ
การประกาศ (Declare) ใกล้จุดใช้
- ประกาศ constants, variables, functions, types ใกล้กับที่ใช้จริงในไฟล์เดียวกัน
- Export (ชื่อขึ้นต้นด้วย Capital Letter) เฉพาะเมื่อจำเป็นต้องใช้จากภายนอก package
- ภายในฟังก์ชัน ให้ประกาศตัวแปรใกล้กับจุดที่ใช้ที่สุด
- จำกัดขอบเขตให้แคบที่สุด เช่น ทำ 1 statement ใน
if
อย่าให้เกิด Runtime Panics
- ตรวจสอบ input จากภายนอกก่อนนำไปใช้
- อย่าใช้
if x == nilหากคุณควบคุมการไหลของข้อมูลแล้ว – ให้เชื่อการจัดการ error ที่จุดนั้น - เมื่อ dereference pointer (เช่น
*p = 10) ต้องตรวจสอบnilก่อนเสมอ - วิธีที่ปลอดภัยที่สุดคือ หลีกเลี่ยงการใช้ pointer หากออกแบบได้
การจัดรูปแบบ (Spacing)
- อย่าห่อโลจิกหลายชั้น (nested
if,for) - ใช้ Return Early และ Flatter Structure: จัดการ error ก่อน แล้วค่อยทำงานต่อ หรือกำจัดเงื่อนไขที่ไม่ต้องทำต่อทันที
การตั้งชื่อไฟล์และแพ็กเกจ
- หลีกเลี่ยงชื่อคลุมเครือ เช่น
util.go,misc.go,constants.go,interfaces/interfaces.go - ให้ชื่อสื่อความหมายของสิ่งที่ทำ ไม่ใช่ตำแหน่งในโครงสร้าง
- จัดไฟล์ให้อยู่ใกล้กับโค้ดที่เกี่ยวข้อง ไม่แยกกันไปคนละที่
การจัดกลุ่มและลำดับการประกาศ
- จัดกลุ่มตามความหมาย ไม่ใช่ตาม type (เช่น ไม่จัดกลุ่ม controller แบบแยกจากกัน)
- เรียงลำดับ declarations ตามความสำคัญ:
- Exported API‑facing functions/structs (ต้องการให้ผู้ใช้เห็นก่อน)
- Helper functions ที่อธิบายหรือสนับสนุนส่วนบน
การตั้งชื่อตัวแปร
- อย่าเติม type ลงท้ายชื่อ (เช่น
userMap,idStr,injectFn) – ชื่อตัวแปรควรบอกว่าเก็บอะไร - ความยาวของชื่อควรสอดคล้องกับ scope: ตัวแปร scoped สั้นอาจใช้ชื่อสั้น ๆ, แต่ global ควรใช้ชื่อที่อธิบายได้ชัดเจน
Documentation
- Document ควรตอบ “ทำไม” ไม่ใช่แค่ “ทำอะไร”
- ให้เหตุผลว่าฟีเจอร์หรือโค้ดมีไว้ทำไม และทำไมต้องใช้วิธีนั้น
- คอมเมนต์ควรสื่อถึงจุดประสงค์หรือประโยชน์ของโค้ด ไม่ใช่แค่อธิบายการทำงานซ้ำ ๆ
- Documentation คือเจตนา ไม่ใช่วิธีการ – ช่วยให้ผู้อ่านเข้าใจแรงจูงใจของการออกแบบ
สรุปจาก “Writing Better Go: Lessons from 10 Code Reviews” ของ Konrad Reiche (โดย Asleep‑Actuary‑4428)