Skip to main content

Go Interview Prep — คำถามที่พบบ่อย

รวมคำถามเชิง conceptual ที่มักถูกถามในรอบ technical interview พร้อมแนวคำตอบสั้นกระชับ (ฝึกพูดออกเสียงเป็นภาษาอังกฤษด้วยกล่อง English Summary ของแต่ละข้อ)

1. Slice vs Array ต่างกันอย่างไร?​

Array มีขนาดคงที่ตอน compile time เป็นส่วนหนึ่งของ type ([5]int กับ [10]int คือคนละ type) Slice คือ struct เล็กๆ (pointer + len + cap) ที่ "ชี้" ไปยัง array ภายใน ทำให้ resize ได้แบบ dynamic ผ่าน append — เกือบทุกที่ในโค้ด Go จริงใช้ slice ไม่ใช่ array

🧠 จำง่าย + 🎯 Beyond

จำว่า "Array = กล่องขนาดตายตัว, Slice = รีโมทที่ปรับช่องได้" — Beyond: array ที่ assign ให้ตัวแปรอื่นหรือส่งเข้าฟังก์ชันจะ copy ทั้งก้อนเสมอ (value type เต็มรูปแบบ) ต่างจาก slice ที่ copy แค่ header (ptr+len+cap) คนที่ส่ง array ขนาดใหญ่เข้าฟังก์ชันโดยไม่รู้ตัวว่ามัน copy ทั้งก้อน คือสาเหตุ performance bug ที่ profiler จับได้บ่อยแต่คนเขียนไม่ทันสังเกต

2. make กับ new ต่างกันอย่างไร?​

new(T) จองหน่วยความจำสำหรับ zero value ของ T แล้ว return *T ใช้ได้กับทุก type make(T, ...) ใช้ได้เฉพาะ slice, map, channel และ return ค่า ที่ initialize แล้ว พร้อมใช้งาน (ไม่ใช่ pointer) — เพราะ 3 type นี้ต้องมี internal state (เช่น hashmap bucket) ที่ zero value เปล่าๆ ใช้งานไม่ได้ทันที

🧠 จำง่าย + 🎯 Beyond

จำว่า "new ให้บ้านเปล่า (pointer), make ให้บ้านพร้อมอยู่ (ready-to-use value)" — Beyond: ในทางปฏิบัติแทบไม่มีใครใช้ new() เลยในโค้ด Go จริง (นิยม &T{} มากกว่าเพราะเซ็ตค่า field พร้อมกันได้ในที) new() มักเจอแค่ในโจทย์สัมภาษณ์หรือโค้ดเก่ามากๆ — รู้ไว้ตอบได้ แต่ไม่ต้องใช้จริง

3. อธิบาย Goroutine Leak พร้อมตัวอย่าง​

func leaky() {
ch := make(chan int) // unbuffered
go func() {
ch <- compute() // ไม่มีใครรับ → goroutine นี้ค้างตลอดไป
}()
// ฟังก์ชันออกไปโดยไม่รับค่าจาก ch
}

Goroutine ค้างรอส่งเข้า channel ที่ไม่มีใครอ่าน จะไม่ถูก garbage collect (เพราะ runtime ยังมี reference ถึงมันอยู่) วิธีแก้: ใช้ context ให้ goroutine เช็ค cancellation, หรือทำ channel เป็น buffered/non-blocking send

🧠 จำง่าย + 🎯 Beyond

จำว่า "Goroutine ที่ค้างรอ = ผีที่ GC มองไม่เห็นว่าควรลบ" (runtime ยังมี pointer ถึง stack ของมันอยู่ ไม่ว่าง) — Beyond: pprof มี endpoint /debug/pprof/goroutine ที่นับจำนวน goroutine ปัจจุบันได้แบบ real-time วิธี debug goroutine leak ใน production จริงคือ monitor ตัวเลขนี้เป็น metric แล้วดู trend ว่ามันไต่ขึ้นเรื่อยๆ ไม่มีวันลดหรือไม่ (ถ้าขึ้นแบบไม่มีเพดาน = leak ชัวร์) ไม่ใช่แค่เดาจากโค้ดอย่างเดียว

4. defer ทำงานตามลำดับไหน และมี pitfall อะไร​

defer ทำงานแบบ LIFO (last-in-first-out) และ argument ของ deferred call ถูก evaluate ทันทีตอนเจอ defer ไม่ใช่ตอนรัน

func example() {
for i := 0; i < 3; i++ {
defer fmt.Println(i) // พิมพ์ 2, 1, 0 (LIFO)
}
}

Pitfall ที่ถามบ่อย: defer ใน loop ที่เปิด resource (เช่น file.Close()) จะไม่ถูกเรียกจนกว่า ฟังก์ชันทั้งหมด จะ return ไม่ใช่ตอนจบ loop แต่ละรอบ — ถ้า loop เปิดไฟล์เป็นพันไฟล์จะ leak file descriptor ชั่วคราวจนฟังก์ชัน return

🧠 จำง่าย + 🎯 Beyond

จำว่า "defer = จานล้างซ้อนกันในอ่าง ล้างจากบนลงล่าง" (LIFO) — Beyond: argument evaluate ทันทีแต่ การเรียก method ผ่าน receiver ที่เป็น pointer ยัง "เห็น" ค่าล่าสุดได้ เพราะสิ่งที่ evaluate ทันทีคือ "ตัว pointer" ไม่ใช่ "ค่าที่ pointer ชี้ไป": defer obj.Method() ที่ obj เป็น pointer จะใช้ state ของ obj ณ ตอนที่ defer ทำงานจริง (ไม่ใช่ตอนเขียน defer) ต่างจาก defer fmt.Println(x) ที่ x เป็น value ธรรมดาซึ่ง snapshot ค่าไว้ทันที เป็นจุดที่ทำให้คน สับสนว่า "defer evaluate ทันที" นั้นจริงแค่ไหนกันแน่

5. Go จัดการ Memory/Garbage Collection อย่างไร​

Go ใช้ concurrent, tri-color mark-and-sweep GC ที่ออกแบบมาให้ pause time ต่ำ (sub-millisecond ในเวอร์ชันใหม่ๆ) ตัวแปร escape ไป heap เมื่อ compiler escape-analysis พบว่ามันถูกใช้งานเกิน scope ของฟังก์ชัน (เช่น return pointer ของ local variable) ใช้ go build -gcflags="-m" ดู escape analysis ได้

🧠 จำง่าย + 🎯 Beyond

จำสี 3 สี: ขาว (ยังไม่ตรวจ) → เทา (กำลังตรวจ) → ดำ (ตรวจแล้ว ไม่ลบ) — object ที่เหลือเป็น "ขาว" ตอนจบ mark phase คือขยะที่ถูกลบ — Beyond: ตัวแปร GOGC (default 100) ควบคุม "GC จะรอให้ heap โตขึ้นกี่ % ก่อนเริ่มรอบถัดไป" — GOGC=100 แปลว่ารอจน heap โตเป็น 2 เท่าของหลัง GC รอบ ก่อน ตั้งค่าสูงขึ้น (เช่น GOGC=200) แลก memory usage ที่สูงขึ้นกับความถี่ GC ที่ลดลง (CPU ลด) — เป็น trade-off ที่ engineer ระดับ production ต้อง tune เองตามลักษณะ workload ไม่ใช่ปล่อย default เสมอไป

6. ความแตกต่างระหว่าง Buffered vs Unbuffered Channel​

Unbuffered: ส่งจะ block จนกว่ามีคนรับ (synchronization point) — ใช้เมื่อต้องการ "handshake" Buffered: ส่งไม่ block จนกว่า buffer เต็ม — ใช้เมื่อต้องการ decouple ความเร็วผู้ส่ง/ผู้รับ (เหมือน bounded queue) แต่ buffer ที่ใหญ่เกินไปอาจซ่อนปัญหา backpressure ไว้

🧠 จำง่าย + 🎯 Beyond

จำว่า "Unbuffered บังคับเจอหน้า, Buffered แค่ฝากของไว้" — Beyond: channel ที่มี cap == 1 ใช้ pattern เป็น mutex ทางเลือก ได้จริง (ch <- struct{}{} แทน mu.Lock(), <-ch แทน mu.Unlock()) ข้อดีคือ "lock" แบบนี้ cancel ได้ผ่าน select + ctx.Done() ซึ่ง sync.Mutex ทำไม่ได้โดยตรง — เป็นเทคนิคที่ใช้น้อยแต่ interviewer ระดับ senior ชอบถามว่า "ใช้ channel แทน mutex ได้ไหม เมื่อไหร่ควรใช้"

7. Struct embedding vs interface embedding ต่างกันอย่างไร​

Struct embedding = composition ของ data + behavior (ได้ field และ method มาด้วย) Interface embedding = รวม method set ของหลาย interface เข้าด้วยกันเป็น interface ใหม่ เช่น io.ReadWriter = io.Reader + io.Writer — ไม่มี data เกี่ยวข้องเลย เป็นแค่ contract

🧠 จำง่าย + 🎯 Beyond

จำว่า "Struct embedding ยืมของ, Interface embedding รวมสัญญา" — Beyond: ถ้า struct embed interface (ไม่ใช่ struct) จะได้ pattern ที่ทรงพลังมาก: struct นั้น "satisfy" interface ตัวใหญ่ได้ทันทีโดยยังไม่ implement ครบทุก method (เพราะ method ที่ขาดถูก delegate ไปที่ embedded interface ซึ่งอาจเป็น nil ถ้าไม่ได้ set ค่า) — เทคนิคนี้ใช้ทำ partial mock หรือ decorator pattern ได้ง่ายมาก แต่ก็เสี่ยง nil pointer panic ถ้า embedded interface ไม่ได้ ถูก set ค่าไว้ก่อนเรียก method ที่ delegate ไป เป็นดาบสองคมที่ต้องระวัง

8. ทำไม Go ไม่มี Generics จนถึง 1.18?​

ทีม Go ให้ความสำคัญกับ compile speed, simplicity, และ explicit code มากกว่า — Generics (type parameters) เพิ่ม complexity ให้ compiler และอาจทำให้ error message อ่านยากขึ้น เมื่อ เปิดตัวใน 1.18 ก็ออกแบบให้ minimal (ไม่มี operator overloading, ไม่มี specialization ซับซ้อน)

func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
🧠 จำง่าย + 🎯 Beyond

จำว่า "Go รอ 10 ปีเพื่อทำ generics ให้ถูก ไม่ใช่ทำให้เร็ว" — Beyond: generic function ของ Go ใช้กลยุทธ์ compile แบบ GC shape stenciling (ไม่ใช่ full monomorphization แบบ C++ template หรือ full type erasure แบบ Java) คือ type ที่มี memory layout เหมือนกัน (เช่น *T ทุกตัว) จะแชร์ compiled code ชุดเดียวกัน ลด binary size เมื่อเทียบกับ C++ ที่ generate code แยกทุก concrete type แต่ก็แลกกับ runtime overhead เล็กน้อยเมื่อเทียบกับ C++ — เป็นจุดที่ แสดงว่าเข้าใจ "ทำไมถึงออกแบบแบบนี้" ลึกกว่าแค่ "มี generics ใน 1.18"

English Summary

Common Go interview themes: slices (dynamic view over an array, copied by header only) vs fixed-size arrays (copied in full); make (initializes slice/map/channel internal state) vs new (zero-valued pointer, rarely used in practice); goroutine leaks from unbuffered sends with no receiver (monitor via pprof's goroutine count in production); defer's LIFO order with arguments evaluated at defer-time — except pointer receivers, which still see later state changes; the concurrent tri-color mark-and-sweep GC, escape analysis, and the GOGC tuning knob; buffered vs unbuffered channels as a synchronization vs decoupling tool, including the cap-1-channel-as-cancellable-mutex trick; struct embedding (composition) vs interface embedding (contract composition, including the partial-mock-via-embedded-interface pattern); and why generics arrived late — Go prioritized compile speed and binary size via GC shape stenciling over C++-style full monomorphization.