ข้ามไปยังเนื้อหา

Mental Model ของ Go

ก่อนจะเขียน Go แม้แต่บรรทัดเดียว การ “ลืม” นิสัยแบบ Node/TypeScript บางอย่างจะช่วยได้มาก Go ไม่ใช่ “TypeScript ที่ไม่มี browser” — แต่เป็นชุด trade-off คนละแบบที่เลือกมาอย่างตั้งใจ บทนี้จะพาคุณผ่านการปรับ mindset 5 อย่างที่สำคัญที่สุด

TypeScript คือ source → JS → runtime (Node.js) คุณแทบจะรัน TypeScript ผ่าน runtime process เสมอ ส่วน Go คอมไพล์เป็น native binary เมื่อ build เสร็จ ./myapp รันตรงบน OS โดยไม่ต้องพึ่ง runtime

TypeScript
// TypeScript workflow
// tsc src/index.ts --outDir dist
// node dist/index.js
// (Node.js ต้องติดตั้งบน server)
Go
// Go workflow
// go build -o myapp .
// ./myapp
// (ไม่ต้องการอะไรอื่นบน server)

เรื่องนี้สำคัญมากกับ Docker image (Go image เป็น FROM scratch ได้), Lambda cold start และการแจกจ่าย CLI — เพราะคุณส่งแค่ไฟล์เดียว

โปรเจกต์ TypeScript มี package.json และโฟลเดอร์ node_modules ที่อาจหนักหลายร้อย megabyte ส่วน Go ใช้ระบบ module (go.mod) โดยดาวน์โหลด dependency ตอน build time แล้วคอมไพล์รวม เข้าไปใน binary เลย จึงไม่มีอะไรเทียบเท่า node_modules บน server

TypeScript
// package.json
{
"dependencies": {
"express": "^4.18.2",
"zod": "^3.22.0"
}
}
// node_modules/ → ส่งแยกหรือติดตั้งใหม่
Go
// go.mod
module github.com/yourname/myapp
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
)
// dependency คอมไพล์รวมเข้า binary — ไม่ต้องส่งแยก

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
// TypeScript: async/await, single thread
async function doWork() {
const a = await stepOne(); // event loop yields
const b = await stepTwo(); // event loop yields
return a + b;
}
Go
// 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

นี่คือนิสัยที่เลิกยากที่สุด ใน TypeScript/JavaScript ทุกอย่าง throw ได้หมด คุณป้องกันด้วย try/catch แต่ดูจาก signature ของ function ไม่ออกเลยว่าจะ throw หรือเปล่า ส่วนใน Go error คือ return value — เป็นส่วนหนึ่งของ contract ของ function

TypeScript
// TypeScript: function ใดๆ ก็ throw ได้
function parseConfig(raw: string): Config {
// อาจ throw SyntaxError — ไม่อยู่ใน signature!
return JSON.parse(raw);
}
try {
const cfg = parseConfig(raw);
} catch (e) {
// จัดการ
}
Go
// 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
}

TypeScript มีหลายวิธีทำสิ่งเดียวกัน: class, prototype, function, decorator และ module format หลายแบบ ชุมชนจึงถกเถียงเรื่อง pattern กันไม่จบ ส่วนวัฒนธรรมของ Go คิดตรงข้าม: มักมีวิธี idiomatic อยู่วิธีเดียว บังคับด้วย gofmt (automatic formatting), go vet (static analysis) และ convention ของชุมชน ถ้านักพัฒนา Go สองคนเขียน function เดียวกันแยกกัน โค้ดที่ได้จะหน้าตาเกือบเหมือนกัน

TypeScript
// TypeScript: หลาย style ที่ valid
class UserService {
constructor(private db: DB) {}
async getUser(id: string) { ... }
}
// หรือ plain function
const getUser = async (db: DB, id: string) => { ... }
// หรือ factory function, namespace, ...
Go
// 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
}
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)
}
}
Go ใช้อะไรแทน Node.js event loop สำหรับ concurrency?
ใน Go error ถูกสื่อสารจาก function อย่างไร?
Go dependency เกิดอะไรขึ้นตอน build time?
gofmt คืออะไร?