บทที่ 2 · Part 1 — Foundations and Judgment

Readable Go

ตั้งชื่อ จัด control flow และออกแบบโค้ดจากมุมคนเรียก โดยให้เครื่องมือดูแล formatting

service มักไม่ได้พังเพราะ gofmt จัดช่องว่างผิด แต่มักพังเพราะชื่อทำให้คนอ่านเข้าใจ contract คนละแบบ, happy path ถูกซ่อนใน nesting หลายชั้น หรือ helper ที่ชื่อทั่วไปนำ side effect ไปซ่อน ไว้ไกลจาก call site Readability ใน Go จึงเป็นงานออกแบบ ไม่ใช่งานตกแต่งท้าย sprint

จบบทนี้คุณจะ

  • ตั้งชื่อจากมุมผู้เรียกและหลีกเลี่ยง package ที่ไม่มี purpose
  • จัด control flow ให้ failure path ชัดและ happy path อ่านเป็นลำดับ
  • รู้ว่าเรื่องใดให้ formatter/linter จัดการ และเรื่องใดต้องใช้ judgment

อ่านจาก Call Site ก่อน

ชื่อ package เป็นส่วนหนึ่งของทุก exported name ถ้า package ชื่อ settlement type ควรชื่อ Batch ไม่ใช่ SettlementBatch เพราะ client อ่านเป็น settlement.Batch อยู่แล้ว ในทำนองเดียวกัน http.HTTPServer และ user.UserService มักซ้ำคำโดยไม่เพิ่มข้อมูล

// อ่านแล้วเห็น domain และ action จาก call site
batch, err := settlement.CreateBatch(ctx, request)
if err != nil {
    return fmt.Errorf("creating settlement batch: %w", err)
}

ตั้งชื่อสั้นตาม scope: index ใน loop ใช้ i ได้ แต่ค่าที่เดินทางข้ามหลาย branch ควรเป็น batchID ไม่ใช่ id รักษา initialism ให้สม่ำเสมอ เช่น HTTPClient, userID, ServeHTTP และตั้งชื่อ interface จาก behavior เมื่อเป็นธรรมชาติ เช่น Reader, Clock, BatchStore

หลีกเลี่ยง package ชื่อ util, common, helper, types หรือ interfaces เพราะชื่อเหล่านี้ บอกตำแหน่งเก็บของ ไม่ได้บอก capability เมื่อ package เริ่มรวม validation, clock, JSON และ retry เข้าด้วยกัน dependency direction จะมองไม่เห็น ทางแก้คือย้ายโค้ดไปอยู่กับเจ้าของ behavior ไม่ใช่ตั้งชื่อกล่องใหม่ให้กว้างกว่าเดิม

ให้ Failure Path ออกมาก่อน

Go ใช้ explicit error flow ข้อดีจะหายไปทันทีเมื่อซ้อน if หลายชั้น ให้ handle input และ error ก่อน แล้วรักษา happy path ไว้ที่ indentation ต่ำที่สุด:

func (s *Service) Submit(ctx context.Context, req SubmitRequest) (Batch, error) {
    if err := req.Validate(); err != nil {
        return Batch{}, fmt.Errorf("validating request: %w", err)
    }

    batch, err := s.store.Create(ctx, req)
    if err != nil {
        return Batch{}, fmt.Errorf("creating batch: %w", err)
    }

    return batch, nil
}

ไม่ต้องมี else หลัง branch ที่ return, continue หรือ break และถ้า condition มี business meaning ให้แยกเป็นฟังก์ชันหรือ boolean ที่ตั้งชื่อได้ แต่อย่าแยกทุก expression เป็น helper จนคนอ่าน ต้องกระโดดไฟล์เพื่อรู้ว่า isAllowed() แค่เปรียบเทียบ 2 ค่า

function ที่ยาวไม่ผิดด้วยจำนวนบรรทัดโดยอัตโนมัติ สิ่งที่ควรถามคือมันมี abstraction level เดียวไหม และชื่อสามารถสรุป contract ได้หรือไม่ การบังคับ “ไม่เกิน 20 บรรทัด” มักสร้าง helper แบบ processData ที่ซ่อนลำดับมากกว่าช่วยอ่าน

Formatter ทำอะไร และไม่ทำอะไร

ใช้ gofmt เป็นข้อบังคับเพื่อตัดการถกเรื่อง whitespace และใช้ goimports เมื่อทีมต้องการจัด import พร้อมกัน CI ควรตรวจว่ารันแล้วไม่มี diff แต่ formatter ตัดสินชื่อ, API shape, ownership หรือความยาว เชิงความคิดไม่ได้

line length ก็ไม่ใช่ language rule คอร์สนี้หักบรรทัดที่ semantic boundary โดยเฉพาะ error message, function call และ composite literal ที่ยาว แต่ไม่บังคับเลข 99 หรือ 120 เป็นสากล ถ้า signature ต้องมี parameter 7 ตัว คำถามแรกควรเป็นว่า dependency และ input ถูกจัดกลุ่มถูกหรือยัง ไม่ใช่ว่า จะหักบรรทัดตรงไหน

comment ที่ดีอธิบาย เหตุผล, invariant หรือข้อจำกัดภายนอก ไม่เล่าซ้ำว่าโค้ดบรรทัดถัดไปทำอะไร สำหรับ exported API ให้เขียน doc comment จาก contract ที่ caller ต้องรู้ รวม concurrency safety, nil behavior, ownership และ error ที่ caller inspect ได้

// Snapshot returns a copy of the batch status at the time of the call.
// The returned map can be mutated by the caller.
func (s *Service) Snapshot() map[string]Status {
    return maps.Clone(s.statuses)
}

ฝึก Review แบบ 2 รอบ

รอบแรกให้ formatter และ static tools จัดเรื่อง mechanical รอบที่ 2 อ่าน public function จาก call site โดยไม่เปิด implementation แล้วตอบให้ได้ว่า input ใด required, caller ถือ ownership ของ output หรือไม่, function อาจ block ไหม และ error ใดตัดสินใจต่อได้ ถ้าตอบไม่ได้ ต่อให้ code สวยก็ยังอ่านไม่ชัด

Production Toolbox

Default คือ gofmt + goimports และ review ด้วยคนสำหรับ naming/control flow หลีกเลี่ยงการเพิ่ม formatter หลายตัวที่ให้ผลขัดกัน ใช้ linter เฉพาะ rule ที่ทีมพร้อมแก้และมี false positive ที่ยอมรับได้ รายละเอียดการวาง quality gate อยู่ใน Tool-Driven Review

Checklist ของ Readable Go

  • package name บอก purpose และไม่ซ้ำ exported name
  • ชื่อยาวขึ้นตาม scope และรักษา initialism สม่ำเสมอ
  • failure path มาก่อน happy path และไม่มี nesting ที่ไม่จำเป็น
  • comment อธิบาย why/contract ไม่ถอดความ code
  • exported API อ่านจาก call site แล้วตอบ ownership, blocking และ error behavior ได้
  • style rule ที่บังคับมีเหตุผลของทีม ไม่ถูกอ้างว่าเป็นกฎของภาษา

อ่านเพิ่ม: Package names, Go Doc Comments, Go Code Review Comments และ Uber Go Style Guide