บทที่ 3 · Part 2 — Opinionated Go Style Guide
Style Decisions in Practice
กลั่น Google และ Uber Go style guides เป็น course defaults พร้อมข้อยกเว้นและโจทย์ code review
เมื่อเข้าทีม Go ใหม่ เรามักเจอ comment สั้น ๆ เช่น “อย่าใช้ Get”, “copy slice ก่อนเก็บ” หรือ
“ห้ามสร้าง goroutine แล้วปล่อยไว้” บาง comment เป็น convention เพื่อให้ codebase สม่ำเสมอ บางข้อ
ป้องกัน bug จริง และบางข้อเคยเหมาะกับ Go รุ่นเก่าแต่ไม่ควรใช้เป็น default ในปี 2026 ถ้าผู้อ่านต้อง
เปิด Google และ Uber guides สลับกันทุกข้อ เขายังต้องทำงานยากที่สุดเองคือเลือกว่าระบบนี้ควรเชื่ออะไร
บทนี้กลั่นคำแนะนำที่ใช้บ่อยจาก Google Go Style และ Uber Go Style Guide ให้เป็น course defaults พร้อมเหตุผล ข้อยกเว้น และลิงก์ไปบทที่อธิบาย semantics แบบเต็ม จุดประสงค์ไม่ใช่จำกฎให้มากที่สุด แต่คือ review code แล้วบอกได้ว่าข้อเสนอหนึ่งช่วย clarity, contract, ownership หรือ lifecycle อย่างไร
จบบทนี้คุณจะ
- ใช้ Google และ Uber guides โดยไม่สับสนว่าเป็นกฎของภาษา
- มี default สำหรับ naming, declarations, nil, copying, errors, interfaces, global state และ tests
- รู้ว่าคำแนะนำใดต้อง modernize สำหรับ Go 1.25/1.26
- review diff โดยเขียน Course default และ Exception ได้ ไม่ใช่เพียงตอบว่า good หรือ bad
Style Guide มี Authority แค่ไหน
Google แยกเอกสารเป็น Core Guide, Style Decisions และ Best Practices โดยให้สถานะไม่เท่ากัน และ ทั้งหมดออกแบบเพื่อ readability ใน codebase ของ Google ส่วน Uber ระบุว่า guide ของตนรวบรวม patterns และ conventions ที่ใช้ใน Go code ของ Uber ทั้ง 2จึงเป็นหลักฐานจาก production ที่มีคุณค่า แต่ไม่ได้ เปลี่ยน behavior ของภาษา
ลำดับตัดสินของคอร์สคือ:
- ตรวจ language, memory model และ package contract ก่อน
- ใช้ official documentation ยืนยัน behavior และ Go version
- เปรียบเทียบเหตุผลจาก Google และ Uber
- เลือก Course default ที่ง่ายและปลอดภัยสำหรับ service ทั่วไป
- ระบุ Exception และ evidence ที่อนุญาตให้เปลี่ยน default
ทุกครั้งที่เห็นคำว่า “ควร” ในบทนี้ ให้อ่านต่อท้ายในใจว่า “ภายใต้ contract นี้” วิธีอ่าน source แบบนี้ต่อยอดจาก Go Conventions and Engineering Judgment
บทนี้เป็น decision map ของ Part ไม่ใช่บทสรุปแทนรายละเอียด จากนี้คอร์สนำหัวข้อใน source documents มา อธิบายต่อเป็น 4 บทที่อ่านได้ในตัว จัดตามปัญหาที่ใช้ review แทนการแยกตามชื่อบริษัท:
- Naming, Documentation and Imports — package names, initialisms, receivers, comments, examples, result names, aliases และ file navigation
- Values, Initialization and Control Flow — declarations, zero values, nil, composite literals, copying, pointers, early return และ defer
- APIs, Interfaces and Program Structure — packages,
consumer interfaces, embedding, constructors, options, globals,
initและ process edge - Errors, Concurrency and Testing Style — wrapping, translation, context, goroutine lifecycle, channels, failure messages, table tests และ fixtures
หัวข้อที่มีบท semantics แยกอยู่แล้วจะถูกสอน 2 ชั้น: Part นี้กำหนด house style ที่ใช้ review ได้ ส่วน Part หลังอธิบายเหตุผลระดับ runtime, trade-off และเครื่องมือ เช่น pointer/escape, error taxonomy หรือ race testing
Naming, Imports และ Code Shape
Google ให้ความสำคัญกับ clarity ที่ call site ส่วน Uber เพิ่ม convention ที่ช่วยให้ review สม่ำเสมอ Course default จึงรับส่วนที่ทั้ง 2อธิบายเหตุผลได้ และไม่รับตัวเลขหรือ prefix ที่ผูกกับองค์กรเดียว
| Decision | Course default | Exception ที่ยอมรับได้ |
|---|---|---|
| package name | lowercase, สั้น และบอก domain; หลีกเลี่ยง util, common, helper | generated package อาจบังคับชื่อและใช้ import alias |
| initialism | ใช้ ID, HTTP, URL หรือรูป lowercase ให้สม่ำเสมอ | ชื่อแบรนด์ที่มี casing เฉพาะต้องเลือกตาม public vocabulary |
| receiver | ตัวย่อ 1–2 ตัวจาก type และใช้ชื่อเดียวกันทุก method | omit ชื่อเมื่อ receiver ไม่ถูกใช้ |
| getter | ใช้ Name() ไม่ใช่ GetName() | Get เป็นคำใน protocol จริง เช่น HTTP GET หรือ operation ที่มี I/O |
| import alias | ไม่ alias ตามความชอบ; ใช้เมื่อชนกันหรือชื่อ generated ไม่เหมาะ | alias ต้องเหมือนกันใน package ใกล้เคียง |
| line length | ไม่มี hard limit; หักตาม semantic boundary | literal, URL หรือ generated code อาจยาวโดยไม่ควรบิด API |
| declaration | := เมื่อสร้าง non-zero value, var เมื่อ zero value สื่อ intent | เลือก = เมื่อ := จะ shadow ตัวแปรที่ต้องใช้ต่อ |
// ชื่อ package ทำให้ call site ไม่ stutter
client := settlement.NewClient(httpClient)
// error path ออกก่อน และชื่อบอกสิ่งที่ตัวแปรหมายถึง
batch, err := client.FetchBatch(ctx, batchID)
if err != nil {
return fmt.Errorf("fetching batch %q: %w", batchID, err)
}
return use(batch)
คอร์สไม่รับ soft limit 99 characters ของ Uber เป็นกฎ เพราะบรรทัดยาวอาจเป็นสัญญาณว่า API รับ argument มากเกินหรือชื่อซ้ำ context การ refactor contract มีค่ากว่าการหักบรรทัดเพื่อให้ผ่านตัวเลข อ่านต่อใน Readable Go
Values, Nil และ Copying
ส่วนนี้เป็นจุดที่ style เชื่อมกับ correctness โดยตรง Go ส่งทุก argument แบบ value แต่ slice และ map ยังอ้าง storage เดิมหลัง copy ดังนั้น “รับเป็น value” ไม่ได้แปลว่า callee มีข้อมูล independent
| Decision | Course default | เหตุผล |
|---|---|---|
*string, *[]T, *map[K]V | ไม่ใช้เพียงเพื่อประหยัด copy | ตัว value มีขนาดคงที่หรือเป็น descriptor อยู่แล้ว |
| nil slice | ใช้ได้ภายในเมื่อ contract ไม่แยก nil/empty | len, range และ append ทำงานได้ตามปกติ |
| JSON array | ให้ schema ตัดสิน null เทียบกับ [] | serialization เป็น boundary contract ไม่ใช่ style ส่วนตัว |
| input slice/map ที่ callee จะเก็บ | clone หรือประกาศ transfer ownership | ป้องกัน caller mutate state ภายหลัง |
| output ที่เปิด internal state | คืน defensive copy หรือ read-only behavior | field unexported อย่างเดียวไม่ป้องกัน alias |
| type ที่มี mutex/once | ใช้ pointer และห้าม copy หลังเริ่มใช้ | เป็น correctness rule ไม่ต้องรอ benchmark |
Uber อธิบาย boundary copying ได้จับต้องได้มาก ส่วน Google เตือนว่าอย่าส่ง pointer เพื่อประหยัดเพียง ไม่กี่ bytes คอร์สจึงเลือก semantics ก่อน performance และให้วัด large struct ใน hot path อ่านต่อใน Values, Pointers and Ownership และ Modern Collections
Errors, Panic และ Process Exit
error ใน Go เป็น value ที่ caller ต้องตัดสินใจ ไม่ใช่ข้อความสำหรับ log อย่างเดียว Course default คือ สร้าง error ให้ช่วยการตัดสินของ caller และ handle 1 ครั้ง: layer หนึ่งอาจ wrap/translate แล้วส่งต่อ หรือบันทึกและจบการจัดการ แต่ไม่ log ทุกชั้นจนเหตุการณ์เดียวกลายเป็นหลายบรรทัด
- error string เริ่ม lowercase และไม่มี punctuation ที่ไม่จำเป็น เพราะอาจถูกประกอบกับข้อความอื่น
- ใช้
%wเมื่อยอมให้ caller ตรวจ underlying error ด้วยerrors.Is/errors.As; นี่เป็น API commitment - ใช้
%vหรือสร้าง error ใหม่เมื่อ boundary ไม่ควรเปิด implementation detail - library คืน
errorสำหรับ failure ที่คาดหมายได้; ไม่panicเพราะ network, input หรือ config ผิด Must*เหมาะกับค่าคงที่หรือ startup invariant ที่ programmer ควบคุม ไม่ใช่ request data- มีเพียง
mainที่ตัดสิน exit process และต้อง exit ครั้งเดียวหลัง cleanup ที่จำเป็น
Google ลงรายละเอียดเรื่อง error structure และ placement ของ %w ส่วน Uber เน้น wrapping กับ handle
once คอร์สรวม 2 มุมเป็น contract เดียว อ่านต่อใน
Errors as Public Contracts
Construction, Global State และ Interfaces
mutable global ทำให้ caller 2 ชุดและ test 2 ตัวรบกวนกันได้ init() ที่เปิด network, สร้าง goroutine
หรืออ่าน config ซ่อน failure ไว้ก่อน main จะจัดการได้ Course default คือสร้าง dependency ที่
composition root แล้ว inject instance ที่มี lifecycle ชัด
func run(ctx context.Context, cfg Config) error {
db, err := OpenDB(ctx, cfg.Database)
if err != nil {
return fmt.Errorf("opening database: %w", err)
}
defer db.Close()
service := NewService(db)
return service.Run(ctx)
}
สำหรับ options ให้ใช้ ordinary parameters เมื่อมีค่าจำเป็นไม่กี่ตัว ใช้ options struct เมื่อ caller มัก ตั้งหลายค่าและต้องการเห็นทั้งหมดที่ call site ส่วน functional options เหมาะกับ public constructor ที่ default เยอะและต้อง evolve โดยไม่เพิ่ม positional parameters ห้ามใช้กฎว่า “เกิน 3 parameter ต้องเป็น functional options” เพราะจำนวนอย่างเดียวไม่บอก contract
interface ก็เช่นกัน: เริ่ม concrete, ให้ consumer สร้าง interface เท่าที่ใช้ และ return concrete type เป็น
default อย่าส่ง *interface; interface value เก็บ dynamic type/value อยู่แล้ว หลีกเลี่ยง embedding type
ภายนอกใน public struct เพราะ promoted methods อาจเปิด API ที่เราไม่ได้ตั้งใจจะ support อ่านต่อใน
Construction and Compatibility,
Packages as APIs และ
Interfaces, Generics and Reflection
Goroutines, Context และ Tests
Uber ใช้ประโยคที่จำง่ายว่าอย่า fire-and-forget ส่วน Google เน้น synchronous function เป็น default และ ต้องอธิบาย lifetime ของ asynchronous work Course default คือ code ที่เริ่ม goroutine ต้องตอบได้ว่าใคร stop, ใคร wait, error ไปไหน และ overload ถูกจำกัดอย่างไร อย่ารับ “channel size one or none” เป็น universal rule เพราะ capacity ต้องมาจาก invariant, memory budget และ behavior เมื่อ queue เต็ม
context.Context เป็น parameter แรกของ operation ที่ข้าม boundary; caller เป็นผู้กำหนด deadline;
ไม่เก็บ context ใน struct และไม่ใช้เป็น parameter bag เมื่อสร้าง derived context ต้องเรียก cancel ตาม
contract เสมอ รายละเอียดอยู่ใน Context, Resources and Cancellation
และ Goroutine Ownership
สำหรับ test ให้ table-driven test เฉพาะ cases ที่ใช้ setup และ assertion shape เดียวกัน ถ้า test body
แตก branch ตาม case หรือแต่ละ case ต้องสร้างโลกคนละแบบ ให้แยก test เพื่อให้ failure อ่านได้ ใช้
t.Helper() ใน helper, t.Cleanup() สำหรับ resource และอย่าเรียก t.Fatal จาก goroutine อื่น
assertion library ใช้ได้เมื่อ failure output ชัดขึ้น ไม่ใช่เพียงย้าย control flow ไปซ่อนใน helper
อ่านต่อใน Unit Testing in Go
Modernization Gate สำหรับปี 2026
guide ที่ดีอาจมีบางบรรทัดล้าสมัย คอร์สจึงตรวจคำแนะนำกับ go directive และ current package docs เสมอ:
- ใช้ typed
sync/atomicจาก standard library ไม่ใช้go.uber.org/atomicเป็น default - module ที่ประกาศ Go 1.22+ ไม่ต้องเขียน
x := xทุก loop เพื่อแก้ closure capture แบบเก่า - benchmark ใหม่ใช้
b.Loop()บน baseline ของคอร์สและเปรียบเทียบหลาย samples - ไม่ copy lint config หรือชื่อ tool เก่าจาก guide โดยไม่ตรวจสถานะและ version
- ตัวเลข benchmark ใน guide เป็น hypothesis ไม่ใช่หลักฐานของ workload เรา
คำแนะนำอย่าง prefix unexported global ด้วย _, ให้ enum ทุกตัวเริ่มที่ 1 หรือกำหนด channel capacity
เป็น 1 หรือ 0 ถูกเก็บไว้เป็น review prompts ไม่ใช่ Course defaults สิ่งที่ต้องตอบคือ zero value,
ownership และ backpressure contract ไม่ใช่ว่าบริษัทใดใช้เลขอะไร
Review Lab: จาก Rule ไปสู่ Reason
ลอง review code นี้โดยห้ามตอบเพียงว่า “Google/Uber ไม่แนะนำ” ให้เขียน comment ในรูป
Risk → Course default → Exception:
var DefaultStore Store
type Handler struct {
*Service
IDs []string
}
func (this *Handler) GetBatches(
ctx *context.Context,
ids *[]string,
) (Results []Batch) {
go this.refresh()
this.IDs = *ids
Results, _ = this.Service.Load(*ctx, *ids)
return Results
}
จุดที่ควรพบอย่างน้อยคือ mutable global, public embedding, receiver name, Get ที่อาจซ่อน I/O,
pointer-to-context, pointer-to-slice, retaining caller-owned slice, fire-and-forget goroutine, ignored error,
named return ที่ไม่ช่วย documentation และ exported result name ที่ไม่จำเป็น
คำตอบที่ดีต้องเสนอ contract ใหม่ด้วย เช่นให้ Handler มี named service field, รับ context.Context
และ []string เป็น value, clone เมื่อจะ retain, คืน ([]Batch, error), ให้ caller เป็นเจ้าของ refresh
lifecycle หรือให้ method รับ context และคืน error/handle ที่ wait ได้ จุดสำคัญคือแต่ละ edit ลด risk อะไร
Course Style Card
เมื่อทีมถกกัน ให้เขียน 5 ช่อง: Source, Why, Course default, Exception, Version checked
ถ้าเติมช่อง Why หรือ Exception ไม่ได้ อย่าเพิ่งเพิ่มกฎลง style guide ของทีม
Checklist ก่อนปิด Style Review
- comment แยก language guarantee ออกจาก organizational convention แล้วหรือยัง
- default ช่วย clarity, contract, ownership หรือ lifecycle อย่างไร
- มีข้อยกเว้นที่ชัด ไม่ใช้คำว่า always กับเรื่องที่ขึ้นกับ context
- ตรวจ
godirective, package docs และวันที่ของ guide แล้วหรือยัง - semantics ลึกมี chapter owner เดียวและลิงก์ไปบทนั้นหรือยัง
- performance claim มี benchmark/profile ของ workload ปัจจุบันหรือยัง
- rule ที่ enforce ด้วย tool ถูกย้ายออกจากการเถียงเชิงรสนิยมหรือยัง
บทนี้เรียบเรียงคำอธิบายและตัวอย่างใหม่โดยเปรียบเทียบ Google Go Style Decisions, Google Go Best Practices และ Uber Go Style Guide ไม่ได้คัดลอก guide ใดมาเป็นกฎทั้งชุด