Mental Model ของ Go
ปรับ gear จาก TypeScript มา Go
หัวข้อที่มีชื่อว่า “ปรับ gear จาก TypeScript มา Go”ก่อนจะเขียน Go แม้แต่บรรทัดเดียว การ “ลืม” นิสัยแบบ Node/TypeScript บางอย่างจะช่วยได้มาก Go ไม่ใช่ “TypeScript ที่ไม่มี browser” — แต่เป็นชุด trade-off คนละแบบที่เลือกมาอย่างตั้งใจ บทนี้จะพาคุณผ่านการปรับ mindset 5 อย่างที่สำคัญที่สุด
1. Compiled vs Interpreted (ไม่มี node ที่ runtime)
หัวข้อที่มีชื่อว่า “1. Compiled vs Interpreted (ไม่มี node ที่ runtime)”TypeScript คือ source → JS → runtime (Node.js) คุณแทบจะรัน TypeScript ผ่าน runtime process เสมอ ส่วน Go คอมไพล์เป็น native binary เมื่อ build เสร็จ ./myapp รันตรงบน OS โดยไม่ต้องพึ่ง runtime
// TypeScript workflow// tsc src/index.ts --outDir dist// node dist/index.js// (Node.js ต้องติดตั้งบน server)// Go workflow// go build -o myapp .// ./myapp// (ไม่ต้องการอะไรอื่นบน server)เรื่องนี้สำคัญมากกับ Docker image (Go image เป็น FROM scratch ได้), Lambda cold start และการแจกจ่าย CLI — เพราะคุณส่งแค่ไฟล์เดียว
2. Static Binary vs node_modules
หัวข้อที่มีชื่อว่า “2. Static Binary vs node_modules”โปรเจกต์ TypeScript มี package.json และโฟลเดอร์ node_modules ที่อาจหนักหลายร้อย megabyte ส่วน Go ใช้ระบบ module (go.mod) โดยดาวน์โหลด dependency ตอน build time แล้วคอมไพล์รวม เข้าไปใน binary เลย จึงไม่มีอะไรเทียบเท่า node_modules บน server
// package.json{ "dependencies": { "express": "^4.18.2", "zod": "^3.22.0" }}// node_modules/ → ส่งแยกหรือติดตั้งใหม่// go.modmodule github.com/yourname/myapp
go 1.22
require ( github.com/gin-gonic/gin v1.9.1)// dependency คอมไพล์รวมเข้า binary — ไม่ต้องส่งแยก3. Goroutine vs Event Loop
หัวข้อที่มีชื่อว่า “3. Goroutine vs Event Loop”Node.js ทำงานแบบ single-threaded ความสามารถด้าน concurrency มาจาก event loop: async operation ถูกส่งต่อไปให้ libuv แล้ว callback (หรือ Promise) จะรันเมื่อทำงานเสร็จ คุณจึงไม่มีทางรัน JavaScript callback สองตัวพร้อมกัน
ส่วน Go ใช้ goroutine — concurrent function แบบ lightweight โดย Go runtime จะ multiplex goroutine หลายพันตัวลงบน OS thread จริง (GOMAXPROCS) goroutine สองตัวจึงรันแบบ parallel ได้จริงบนเครื่อง multi-core
// TypeScript: async/await, single threadasync function doWork() { const a = await stepOne(); // event loop yields const b = await stepTwo(); // event loop yields return a + b;}// Go: goroutine, parallelism จริงๆfunc doWork(ch chan int) { a := stepOne() // รันบน goroutine b := stepTwo() // รันบน goroutine ch <- a + b // ส่งผลลัพธ์ผ่าน channel}// เรียกใช้ด้วย: go doWork(ch)เราจะลงลึกเรื่อง goroutine และ channel ในบทถัดไป สำหรับตอนนี้ขอแค่จำ mental model ไว้ว่า: concurrency ของ Go เป็น parallel โดยค่าเริ่มต้น ไม่ใช่แค่ async
4. Error แบบ Explicit vs Exception
หัวข้อที่มีชื่อว่า “4. Error แบบ Explicit vs Exception”นี่คือนิสัยที่เลิกยากที่สุด ใน TypeScript/JavaScript ทุกอย่าง throw ได้หมด คุณป้องกันด้วย try/catch แต่ดูจาก signature ของ function ไม่ออกเลยว่าจะ throw หรือเปล่า ส่วนใน Go error คือ return value — เป็นส่วนหนึ่งของ contract ของ function
// TypeScript: function ใดๆ ก็ throw ได้function parseConfig(raw: string): Config { // อาจ throw SyntaxError — ไม่อยู่ใน signature! return JSON.parse(raw);}
try { const cfg = parseConfig(raw);} catch (e) { // จัดการ}// Go: error อยู่ใน return type — ไม่สามารถ ignore ได้func parseConfig(raw string) (Config, error) { var cfg Config if err := json.Unmarshal([]byte(raw), &cfg); err != nil { return Config{}, fmt.Errorf("parseConfig: %w", err) } return cfg, nil}
cfg, err := parseConfig(raw)if err != nil { // ต้องจัดการ — compiler เตือนถ้าไม่ใช้ err}5. ความเรียบง่าย — วิธีเดียวที่ idiomatic
หัวข้อที่มีชื่อว่า “5. ความเรียบง่าย — วิธีเดียวที่ idiomatic”TypeScript มีหลายวิธีทำสิ่งเดียวกัน: class, prototype, function, decorator และ module format หลายแบบ ชุมชนจึงถกเถียงเรื่อง pattern กันไม่จบ ส่วนวัฒนธรรมของ Go คิดตรงข้าม: มักมีวิธี idiomatic อยู่วิธีเดียว บังคับด้วย gofmt (automatic formatting), go vet (static analysis) และ convention ของชุมชน ถ้านักพัฒนา Go สองคนเขียน function เดียวกันแยกกัน โค้ดที่ได้จะหน้าตาเกือบเหมือนกัน
// TypeScript: หลาย style ที่ validclass UserService { constructor(private db: DB) {} async getUser(id: string) { ... }}// หรือ plain functionconst getUser = async (db: DB, id: string) => { ... }// หรือ factory function, namespace, ...// Go: style idiomatic ที่ชัดเจนแบบเดียวtype UserService struct { db *DB}
func NewUserService(db *DB) *UserService { return &UserService{db: db}}
func (s *UserService) GetUser(ctx context.Context, id string) (User, error) { // ... return User{}, nil}Playground: error เป็นค่า
หัวข้อที่มีชื่อว่า “Playground: error เป็นค่า”package main
import ( "errors" "fmt")
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}
func main() { result, err := divide(10, 2) if err != nil { fmt.Println("Error:", err) return } fmt.Printf("10 / 2 = %.1f\n", result)
_, err = divide(5, 0) if err != nil { fmt.Println("Caught error:", err) }}Loading Go runtime (first run only, ~8 MB)…