บทที่ 4 · Part 2 — Opinionated Go Style Guide

Naming, Documentation and Imports

ตั้งชื่อ package และ symbol เขียน API docs และจัด imports จากมุมของผู้ใช้ package

โค้ด Go 2 ชุดอาจผ่าน gofmt เหมือนกันทุกบรรทัด แต่ให้ประสบการณ์กับผู้ใช้ต่างกันมาก ถ้า package ชื่อ common มี type ชื่อ CommonClient และ method ชื่อ GetData คนเรียกต้องเปิด implementation เพื่อเดาว่า ข้อมูลอะไร มาจากไหน และ call นี้ block หรือ fail ได้หรือไม่ ปัญหานี้ formatter แก้ไม่ได้ เพราะเป็นเรื่องของ vocabulary และ public contract

บทนี้นำรายละเอียดด้าน naming, documentation และ imports จาก Google Go Style Decisions, Google Go Best Practices และ Uber Go Style Guide มาเรียบเรียงเป็น house style ของคอร์ส เนื้อหาจงใจอยู่ในคอร์สเพื่อให้ นำไปใช้ review ได้ทันที ส่วน link ต้นทางมีไว้ตรวจบริบทและติดตามการเปลี่ยนแปลง ไม่ใช่การบ้านที่ต้องอ่านก่อน

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

  • ตั้งชื่อ package, exported symbol, local variable และ receiver จากมุมของคนอ่าน
  • เขียน doc comment ที่อธิบาย contract, failure และ ownership แทนการทวน signature
  • ใช้ runnable example เป็นทั้งเอกสารและ test
  • จัด import alias, blank import และ file layout โดยไม่สร้าง convention ที่ไม่มีเหตุผล

Package Name คือส่วนแรกของ API

ผู้ใช้เห็น package name ทุกครั้งที่เรียก exported symbol ชื่อจึงต้องอ่านร่วมกับ symbol ไม่ใช่ตัดสินแยกกัน Course default คือชื่อ lowercase สั้น ออกเสียงได้ และบอก domain ที่ package รับผิดชอบ หลีกเลี่ยงชื่อ util, common, helper, model หรือ misc เพราะชื่อเหล่านี้ไม่ได้สร้าง boundary และมักค่อย ๆ กลายเป็น ที่รวม dependency ซึ่งไม่เกี่ยวกัน

Call siteปัญหาชื่อที่ชัดขึ้น
widget.NewWidget()package และ type พูดซ้ำwidget.New()
userutil.NormalizeUserName()boundary กว้างและ stutterusername.Normalize()
database.LoadFromDatabase()บอก implementation ซ้ำstore.Load()
common.GetData()ไม่บอก domain หรือ costledger.FetchEntries()

ชื่อ package ไม่ต้องสรุปโครงสร้างทั้งระบบ ขอให้บอก capability ที่ cohesive ก็พอ package httpclient อาจยัง กว้างเกินถ้ารวม client ของทุก upstream แต่ riskapi หรือ exchange ช่วยให้ call site บอกบริบทเองได้ ถ้าต้องใช้คำหลายคำให้เขียนติดกันเป็น lowercase และพิจารณาว่าชื่อยาวนั้นกำลังบอกว่า package รับผิดชอบมาก เกินไปหรือไม่

ชื่อ folder, filename และ identifier เป็นคนละเรื่อง filename ใช้ underscore เช่น http_client_test.go ได้ เพราะไม่ปรากฏใน expression แต่ identifier ปกติใช้ MixedCaps ข้อยกเว้นที่มีเหตุผลคือชื่อ test อย่าง TestParse_invalidChecksum หรือ package black-box ที่ลงท้าย _test

Symbol ต้องอ่านดีเมื่อมี Package นำหน้า

Go ไม่ใช้ snake_case กับ identifier และเก็บ casing ของ initialism ให้สม่ำเสมอ: ID, URL, HTTP, userID, parseURL ไม่ใช่ Id, Url หรือ HttpClient สำหรับคำที่มี spelling ของแบรนด์ เช่น gRPC ให้ทีมเลือก vocabulary เดียวและบันทึกไว้ อย่าสร้างหลายรูปแบบใน public API เดียวกัน

ไม่เติม type ลงในชื่อถ้าคนอ่านเห็นจาก declaration และการใช้งานอยู่แล้ว:

users := make([]User, 0, len(rows)) // ไม่ใช่ userSlice
count := len(users)                 // ไม่ใช่ userCountInt
limitText := r.FormValue("limit")
limit, err := strconv.Atoi(limitText)

กรณีสุดท้ายเติม Text เพราะมีค่าความหมายเดียวกัน 2 representation ใน scope เดียว นี่ต่างจากการเติม String ทุกตัวแปรตาม type ชื่อที่ดีบอกบทบาทของค่า ไม่ใช่สิ่งที่ compiler รู้อยู่แล้ว

สำหรับ getter ให้ใช้ Name() แทน GetName() เมื่อเป็น cheap accessor ถ้า operation มี I/O หรือ computation ที่ควรสะดุดตา ใช้ verb เช่น Fetch, Load, Lookup หรือ Compute ชื่อจึงบอก cost model ได้ด้วย Get ยังเหมาะเมื่อเป็นคำของ protocol จริง เช่น HTTP GET

receiver ใช้อักษรย่อ 1–2 ตัวจาก type และใช้ชื่อเดิมทุก method:

func (c *Client) Fetch(ctx context.Context, id string) (Entry, error)
func (c *Client) Close() error

หลีกเลี่ยง this, self และชื่อ receiver ยาว เพราะ receiver ปรากฏถี่และ type อยู่ใกล้ method อยู่แล้ว ถ้า receiver ไม่ถูกใช้ให้ละชื่อ ไม่ใช้ _ เพียงเพื่อให้ signature ดูครบ

constant ใช้ MixedCaps เหมือน identifier อื่นและตั้งตามบทบาท ไม่ใช่ค่าของมัน MaxBatchSize มีเหตุผล แต่ Twelve = 12 เพียงตั้งชื่อซ้ำ literal Exported sentinel error ใช้ ErrNotFound; error type ที่ caller ต้องอ่าน ข้อมูลใช้ชื่อ ValidationError อย่าสร้าง exported error ทุกข้อความ เพราะแต่ละ symbol กลายเป็น compatibility surface ที่ต้องรักษา

หลีกเลี่ยง local/package name ที่ shadow predeclared identifiers อย่าง error, string, len, new หรือ make เมื่อทำให้ expression ถัดไปสับสน Scope สั้นซึ่งไม่ใช้ builtin นั้นอาจไม่ผิด แต่ Course default คือเลือก domain word เช่น length หรือ itemError เพื่อให้ refactor ภายหลังไม่สะดุด Tool ช่วยแจ้งได้ แต่ reviewer ต้อง ตัดสินจาก scope ไม่ตั้ง blacklist แบบไร้บริบท

ความยาวของชื่อแปรตาม Scope

คำแนะนำ “ห้ามใช้ตัวแปร 1 ตัวอักษร” และ “ชื่อสั้นกว่าเสมอ” ผิดทั้งคู่ Course default คือ scope ยิ่งกว้าง หรือมีค่าคล้ายกันหลายตัว ชื่อยิ่งต้องเจาะจง ใน loop สั้น i, n, r, w อ่านง่ายเพราะ convention และ ตำแหน่งช่วยบอกความหมาย แต่ c ที่อยู่ข้าม 30 บรรทัดควรเปลี่ยนเป็น retryCount หรือ customer

for _, entry := range entries {
    if entry.Status == Pending {
        pending = append(pending, entry)
    }
}

อย่าตัดตัวอักษรเพื่อประหยัดการพิมพ์ เช่น cfg และ ctx เป็นคำย่อที่ Go community เข้าใจตรงกัน แต่ stlmntRslt บังคับให้ผู้อ่านถอดรหัส ความกระชับที่ดีลด repetition; ไม่ได้ลด vowel

ชื่อ boolean ควรอ่านเป็นข้อเท็จจริงหรือ capability เช่น ready, hasToken, canRetry และควรระวัง negative name อย่าง disableCache เพราะ condition if !disableCache อ่าน 2 ชั้น แต่ configuration flag จากภายนอก อาจต้องรักษาชื่อเดิมเพื่อ compatibility ได้

Doc Comment ต้องบอกสิ่งที่ Type บอกไม่ได้

exported declaration ต้องมี doc comment ที่เป็นประโยคสมบูรณ์และเริ่มด้วยชื่อ symbol หรือ article ที่นำไปสู่ ชื่อ symbol กฎนี้ช่วยทั้ง go doc, pkg.go.dev, IDE และ reviewer แต่ comment ที่เพียงแปล signature ไม่มีคุณค่า:

// Fetch fetches an entry.
func (c *Client) Fetch(ctx context.Context, id string) (Entry, error)

เอกสารที่มีประโยชน์ควรตอบเรื่องที่ caller ต้องรู้เพื่อใช้ API อย่างถูกต้อง เช่น normalization, ownership, concurrency, blocking, error categories และ lifecycle:

// Fetch returns the latest committed entry for id.
// It returns ErrNotFound when the ledger has no matching entry.
// The returned Tags map is independent and may be modified by the caller.
func (c *Client) Fetch(ctx context.Context, id string) (Entry, error)

ไม่ต้องเขียนทุกหัวข้อให้ยาวเสมอ ถ้า behavior เป็นไปตาม convention และ type ชัดอยู่แล้ว ประโยคเดียวก็พอ แต่ถ้า function เก็บ slice ของ caller, callback อาจรันพร้อมกัน, result ต้อง Close, หรือ error ถูก wrap ให้ ตรวจด้วย errors.Is สิ่งนั้นคือส่วนหนึ่งของ API และต้องอยู่ในเอกสาร

package ควรมี package comment 1 แห่ง อาจอยู่ในไฟล์หลักหรือ doc.go ถ้าเนื้อหายาว สำหรับ command ให้ อธิบายว่า binary ทำอะไรและมี side effect ใด ไม่ต้องบรรยาย directory layout ภายในที่ผู้ใช้ไม่เห็น

ก่อน merge public API ให้ preview ด้วย go doc หรือ local pkgsite ไม่อ่านเฉพาะ raw source เพราะ headings, lists, links และ code blocks ถูก render ตาม syntax ของ go/doc/comment Caveat สำคัญควรอยู่ใกล้ declaration ที่เกี่ยวข้อง ไม่ฝังไว้ท้าย package overview จน IDE user มองไม่เห็น

Runnable Example คือ Documentation ที่ Compiler ช่วยดูแล

example ใน _test.go ปรากฏบน pkg.go.dev และถ้ามี // Output: จะถูกรันโดย go test จึงเหมาะกับ happy path ที่ผู้ใช้ต้องเข้าใจมากกว่าตัวอย่างยาวใน README:

func ExampleNormalize() {
    name := username.Normalize("  Ada.Lovelace ")
    fmt.Println(name)
    // Output: ada.lovelace
}

เลือก example ที่แสดง shape ของ API, default และ error handling สำคัญ อย่าใส่ token, network หรือเวลาจริง จน test ไม่ deterministic ถ้าตัวอย่างต้องใช้ setup มาก นั่นอาจเป็นสัญญาณว่า API surface ซับซ้อนเกิน

named result parameters ใช้เมื่อช่วยแยกผลลัพธ์ type เดียวกัน หรือบอก action ที่ caller ต้องทำ เช่น (ctx context.Context, cancel context.CancelFunc) อย่าตั้งชื่อทุก result เพื่อใช้ naked return ใน function ยาว Course default คือ return explicit เมื่อ control flow เริ่มต้องเลื่อนหน้าจอ

Imports และ Alias ต้องลดความประหลาดใจ

ให้ gofmt หรือ goimports ดูแล grouping และ ordering Import alias ใช้เมื่อชื่อชนกัน, generated package มี ชื่อไม่เหมาะ หรือ alias ที่เป็น vocabulary ช่วยแยก API 2 ระบบจริง ๆ ถ้า alias package เดิมหลายไฟล์ให้ใช้ชื่อ เดียวกัน ไม่เปลี่ยนตามรสนิยมของผู้เขียน

import (
    "context"

    riskpb "example.com/contracts/risk/v1"
    "example.com/service/internal/ledger"
)

หลีกเลี่ยง dot import เพราะทำให้หาต้นทางของ symbol ยาก Blank import มี side effect ผ่าน init จึงใช้ได้เฉพาะ ที่ architecture ตั้งใจ เช่น register SQL driver ใน composition root หรือ integration test พร้อม comment บอก เหตุผล ห้ามซ่อนไว้ใน domain package

คอร์สไม่กำหนด hard limit 99 ตัวอักษร บรรทัดควรหักตาม semantic group และอ่าน diff ได้ ถ้า signature ยาว เพราะมี positional parameters จำนวนมาก ให้ทบทวน API ก่อนบังคับ line wrap ส่วน URL, generated code และ raw literal อาจยาวได้เมื่อการหักทำให้ข้อมูลผิด

Declaration Order เป็น Navigation Aid

Uber เสนอ order ระดับไฟล์เพื่อให้ค้นหาได้คาดเดาได้ คอร์สรับเป็น team convention แบบยืดหยุ่น: package comment, imports, constants/variables, types, constructors, exported methods, unexported helpers แต่ cohesion สำคัญกว่า การย้าย helper ไปไกลจาก code ที่ใช้ หากไฟล์ต้องอาศัย rigid order เพื่อให้พออ่านได้ อาจถึงเวลาตัด package หรือแยก concern

reviewer ไม่ควรเสียเวลาบังคับ blank line ที่ formatter จัดการได้ ให้เน้นชื่อที่ call site, comment ที่ขาด contract และ import ที่สร้าง implicit behavior งาน mechanical ส่งให้ tool งาน semantic เก็บไว้ให้คน

Review Lab: อ่านจาก Call Site ก่อน

ลอง review API ต่อไปนี้โดยยังไม่เปิด implementation:

package common

// Client is a client.
type Client struct{}

func (this *Client) GetData(ctx context.Context, idString string) ([]byte, error)

สิ่งที่ควรถามไม่ใช่เพียง “ผิด style กี่ข้อ” แต่คือข้อมูลชนิดใด, remote หรือ local, bytes มี encoding อะไร, caller แก้ result ได้หรือไม่ และ error ใดที่ใช้ตัดสินใจ ถ้า domain คือ ledger อาจออกแบบเป็น ledger.Client.FetchEntry(ctx, id) (Entry, error) พร้อม doc เรื่อง consistency และ ErrNotFound ชื่อและ เอกสารทำให้ contract แคบลงก่อนแตะ implementation

Checklist สำหรับ Naming และ Docs

  • อ่าน package.Symbol ที่ call site แล้วไม่ stutter และไม่ต้องเดา domain
  • ชื่อบอก role/cost ไม่ทวน type หรือ surrounding context
  • exported API บอก ownership, failure, blocking และ concurrency เมื่อสิ่งเหล่านี้ไม่ obvious
  • example รันได้และไม่พึ่ง environment จริง
  • alias กับ blank import มีเหตุผลที่มองเห็นจากไฟล์
  • formatter/tool รับงาน layout ส่วน reviewer ตัดสิน semantics

เนื้อหาบทนี้เรียบเรียงใหม่โดยอิง Google Go Style Decisions, Google Go Best Practices, Uber Go Style Guide และ Go Doc Comments