บทที่ 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 ของภาษา

ลำดับตัดสินของคอร์สคือ:

  1. ตรวจ language, memory model และ package contract ก่อน
  2. ใช้ official documentation ยืนยัน behavior และ Go version
  3. เปรียบเทียบเหตุผลจาก Google และ Uber
  4. เลือก Course default ที่ง่ายและปลอดภัยสำหรับ service ทั่วไป
  5. ระบุ Exception และ evidence ที่อนุญาตให้เปลี่ยน default

ทุกครั้งที่เห็นคำว่า “ควร” ในบทนี้ ให้อ่านต่อท้ายในใจว่า “ภายใต้ contract นี้” วิธีอ่าน source แบบนี้ต่อยอดจาก Go Conventions and Engineering Judgment

บทนี้เป็น decision map ของ Part ไม่ใช่บทสรุปแทนรายละเอียด จากนี้คอร์สนำหัวข้อใน source documents มา อธิบายต่อเป็น 4 บทที่อ่านได้ในตัว จัดตามปัญหาที่ใช้ review แทนการแยกตามชื่อบริษัท:

  1. Naming, Documentation and Imports — package names, initialisms, receivers, comments, examples, result names, aliases และ file navigation
  2. Values, Initialization and Control Flow — declarations, zero values, nil, composite literals, copying, pointers, early return และ defer
  3. APIs, Interfaces and Program Structure — packages, consumer interfaces, embedding, constructors, options, globals, init และ process edge
  4. 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 ที่ผูกกับองค์กรเดียว

DecisionCourse defaultException ที่ยอมรับได้
package namelowercase, สั้น และบอก domain; หลีกเลี่ยง util, common, helpergenerated package อาจบังคับชื่อและใช้ import alias
initialismใช้ ID, HTTP, URL หรือรูป lowercase ให้สม่ำเสมอชื่อแบรนด์ที่มี casing เฉพาะต้องเลือกตาม public vocabulary
receiverตัวย่อ 1–2 ตัวจาก type และใช้ชื่อเดียวกันทุก methodomit ชื่อเมื่อ receiver ไม่ถูกใช้
getterใช้ Name() ไม่ใช่ GetName()Get เป็นคำใน protocol จริง เช่น HTTP GET หรือ operation ที่มี I/O
import aliasไม่ alias ตามความชอบ; ใช้เมื่อชนกันหรือชื่อ generated ไม่เหมาะalias ต้องเหมือนกันใน package ใกล้เคียง
line lengthไม่มี hard limit; หักตาม semantic boundaryliteral, 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

DecisionCourse defaultเหตุผล
*string, *[]T, *map[K]Vไม่ใช้เพียงเพื่อประหยัด copyตัว value มีขนาดคงที่หรือเป็น descriptor อยู่แล้ว
nil sliceใช้ได้ภายในเมื่อ contract ไม่แยก nil/emptylen, 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 behaviorfield 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
  • ตรวจ go directive, 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 ใดมาเป็นกฎทั้งชุด