Skip to main content

Go Fundamentals

Type System & Zero Values​

Go เป็น statically typed และทุก type มี zero value (ค่าเริ่มต้นเมื่อไม่ได้กำหนดค่า) เช่น 0 สำหรับ numeric, "" สำหรับ string, nil สำหรับ pointer/slice/map/channel/interface/func

var i int // 0
var s string // ""
var p *int // nil
var sl []int // nil (แต่ len(sl) == 0 ใช้ได้ปลอดภัย)
var m map[string]int // nil (อ่านได้ แต่เขียนจะ panic!)
ข้อผิดพลาดที่พบบ่อย

nil map อ่านค่าได้ (m["x"] จะได้ zero value) แต่ m["x"] = 1 จะ panic ต้อง make(map[string]int) ก่อนเขียนเสมอ

🧠 จำง่ายๆ

"ภาชนะว่าง อ่านได้ เขียนไม่ได้" — nil slice/map เหมือนแก้วเปล่าที่มองเห็น (อ่าน/len ได้) แต่ เทน้ำใส่ (เขียน) ไม่ได้จนกว่าจะมีแก้วจริง (make) ส่วน nil channel ถ้า send/receive จะ block ตลอดกาล ไม่ panic — คนละพฤติกรรมกับ map จำแยกไว้: "map เขียนพัง, channel เขียนค้าง"

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Zero value ของ struct คือ zero value ของทุก field รวมกัน ซึ่งทำให้บาง struct "ใช้งานได้ทันที โดยไม่ต้อง constructor" ถ้าออกแบบดีๆ เช่น sync.Mutex{} หรือ bytes.Buffer{} ใช้ได้เลยโดยไม่ต้อง New...() — นี่คือหลักการที่เรียกว่า "useful zero value" ที่ Go team ออกแบบ API ไว้ตั้งใจ เวลาออกแบบ struct ของตัวเอง ลองถามตัวเองว่า "zero value ของ struct นี้ใช้งานได้เลยไหม" จะช่วยให้ API เรียบง่ายขึ้นมาก

Structs & Composition​

Go ไม่มี inheritance แต่ใช้ composition ผ่าน embedding แทน

type Animal struct {
Name string
}

func (a Animal) Speak() string {
return a.Name + " makes a sound"
}

type Dog struct {
Animal // embedded struct — ได้ method Speak() มาด้วย
Breed string
}

d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Speak()) // "Rex makes a sound"
🧠 จำง่ายๆ

"Embedding = ยืมของมาใช้ ไม่ใช่เป็นลูกหลาน" — Dog ไม่ได้ "เป็น" Animal แบบ inheritance (is-a ที่ runtime รู้จัก) มันแค่ "มี" Animal อยู่ข้างในแล้ว Go เอื้อให้เรียก field/method ผ่าน ไปถึงได้แบบไม่ต้องพิมพ์ชื่อกลาง (d.Speak() แทนที่จะต้องเขียน d.Animal.Speak()) — เรียกว่า "promotion" ไม่ใช่ inheritance จริง

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ถ้า Dog ประกาศ method ชื่อ Speak() ของตัวเองด้วย จะ shadow (บัง) method ที่ promote มา จาก Animal โดยอัตโนมัติ ไม่มี error ไม่มี warning — และถ้า struct embed สอง type ที่มี method/field ชื่อซ้ำกัน (ambiguous) จะ compile error ก็ต่อเมื่อถูกเรียกใช้งานจริงเท่านั้น ไม่ error ตอน struct ถูกประกาศ เป็นจุดที่ debug งงมากเวลาเจอครั้งแรก เพราะ error message จะชี้ ไปที่บรรทัดที่เรียกใช้ ไม่ใช่บรรทัดที่ประกาศ struct

Interfaces — Implicit Satisfaction​

Interface ใน Go ถูก satisfy แบบ implicit (ไม่ต้องเขียน implements) — ถ้า type มี method ตรงตาม signature ก็ถือว่า implement interface นั้นโดยอัตโนมัติ นี่คือหัวใจของการออกแบบโค้ด Go ที่ยืดหยุ่นและ testable

type Writer interface {
Write(p []byte) (n int, err error)
}

// os.File, bytes.Buffer, strings.Builder ฯลฯ ล้วน satisfy Writer โดยไม่ต้องประกาศอะไรเพิ่ม

หลักออกแบบที่สำคัญ: "Accept interfaces, return structs" — รับ parameter เป็น interface (เพื่อความยืดหยุ่นและ testability) แต่ return เป็น concrete type (ให้ caller เข้าถึง method เฉพาะของ type นั้นได้เต็มที่)

🧠 จำง่ายๆ

"รับกว้าง คืนแคบ" (accept wide, return narrow) — นึกภาพกรวย: ปากกว้างรับ interface อะไรก็ได้ ที่ match เข้ามา แต่ปลายแคบคืนของจริงที่ชัดเจนออกไป ถ้าจำ "accept interfaces, return structs" ไม่ได้ ให้จำแค่คำนี้คำเดียว

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

nil interface ≠ nil pointer ที่ถูกเก็บใน interface — นี่คือกับดักที่แม้แต่ Go dev ที่เขียน มาหลายปียังพลาด:

type MyError struct{}
func (e *MyError) Error() string { return "boom" }

func doSomething() error {
var err *MyError // nil
return err // return nil pointer ที่ถูกห่อใน interface error
}

err := doSomething()
fmt.Println(err == nil) // false! เพราะ interface เก็บทั้ง (type, value) และ type ไม่ใช่ nil

Interface ภายในคือ pair (type, value) — err == nil จะ true ก็ต่อเมื่อทั้ง type และ value เป็น nil พร้อมกันเท่านั้น ถ้า concrete type ถูกเก็บไว้ (แม้ value จะเป็น nil pointer) interface นั้นจะไม่เท่ากับ nil วิธีป้องกัน: ฟังก์ชันที่ return error ควร return nil ตรงๆ ไม่ใช่ return ตัวแปร pointer ที่อาจเป็น nil

Pointers vs Values​

func increment(n int) { n++ } // แก้ copy ไม่มีผลกับตัวต้นฉบับ
func incrementP(n *int) { *n++ } // แก้ค่าจริงผ่าน pointer

เมื่อไหร่ควรใช้ pointer receiver:

  • ต้องการแก้ไข state ของ struct นั้นจริงๆ
  • struct มีขนาดใหญ่ (หลีกเลี่ยงการ copy ที่สิ้นเปลือง)
  • struct มี mutex หรือ field ที่ copy ไม่ได้ (เช่น sync.Mutex)

กฎง่ายๆ: ถ้า method หนึ่งของ type ใช้ pointer receiver ให้ทุก method ของ type นั้นใช้ pointer receiver ให้สอดคล้องกัน

🧠 จำง่ายๆ

"ตัวใหญ่ชี้ ตัวเล็กส่ง" — struct ใหญ่/มี state ที่ต้องแก้ ให้ "ชี้" ด้วย pointer, struct เล็กๆ ไม่กี่ field (เช่น time.Duration, จุดพิกัด {X, Y}) ส่งเป็น "value" ตรงๆ ได้เลย เร็วกว่าด้วย ซ้ำเพราะไม่ต้อง allocate บน heap

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Go compiler ตัดสินใจเองว่าตัวแปรจะอยู่ stack หรือ heap ผ่าน escape analysis — ไม่ใช่กฎตายตัว ว่า "pointer = heap เสมอ" ถ้า compiler พิสูจน์ได้ว่าตัวแปรที่ถูก pointer ชี้ไม่ "หนี" ออกจาก scope ของฟังก์ชัน (ไม่ถูก return, ไม่ถูกเก็บไว้ที่อื่น) มันจะอยู่บน stack แม้จะส่งผ่าน pointer ก็ตาม เช็คได้จริงด้วย go build -gcflags="-m" — เป็นจุดที่แยกคนเข้าใจ Go ตื้นๆ กับคนเข้าใจลึกได้ ชัดเจนมากในสัมภาษณ์

Error Handling Idiom​

Go ไม่มี exception — error คือ ค่า return ปกติ ที่ต้องเช็กทุกครั้ง

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

result, err := divide(10, 0)
if err != nil {
return fmt.Errorf("divide failed: %w", err) // %w wrap error เก็บ chain ไว้
}

ใช้ errors.Is / errors.As เพื่อตรวจสอบ error แบบ chain ได้:

if errors.Is(err, sql.ErrNoRows) { /* handle not found */ }

var valErr *ValidationError
if errors.As(err, &valErr) { /* handle specific type */ }
🧠 จำง่ายๆ

"Is แปลว่า เท่ากับ, As แปลว่า แปลงร่าง" — errors.Is ใช้เช็คว่า error "คือ" sentinel ตัวไหนในสาย chain (เหมือน ==) ส่วน errors.As ใช้ "แปลงร่าง" error กลับเป็น concrete type เพื่อดึง field พิเศษออกมาใช้ (เหมือน type assertion ที่ไล่ลงไปทั้ง chain)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

%w ใน fmt.Errorf ทำงานได้เพราะ error ที่ wrap แล้ว implement interface ที่ไม่มีใครเห็นชัดๆ คือ interface { Unwrap() error } — errors.Is/errors.As เดินตาม chain นี้ไปเรื่อยๆ ด้วยการ เรียก Unwrap() ซ้ำๆ จนเจอหรือหมด chain ถ้าใช้ %v แทน %w จะไม่มี Unwrap() ถูกสร้างให้ — error จะ "หลุด chain" ทันที คนที่ไม่รู้เรื่องนี้มักจะงงว่าทำไม errors.Is ถึง return false ทั้งที่ "เห็น" ข้อความ error เดิมอยู่ในสตริง (เพราะมันดู string ไม่ได้ มันดู chain ของ Unwrap())

Slices: len, cap, และ Shared Backing Array​

Slice คือ "view" บน array ภายใน (ptr, len, cap) — การ append อาจสร้าง array ใหม่ หรือใช้ของเดิมก็ได้ ขึ้นกับว่า cap พอหรือไม่ นี่คือบั๊กคลาสสิกที่ควรรู้:

a := make([]int, 3, 5)
b := a[:2]
b = append(b, 99) // cap พอ → เขียนทับ a[2] โดยไม่รู้ตัว!

ถ้าต้องการ slice ใหม่จริงๆ ที่ไม่ share memory กับต้นฉบับ ใช้ copy() หรือ append([]T{}, src...)

🧠 จำง่ายๆ

"Slice คือรีโมท ไม่ใช่ทีวี" — slice เป็นแค่ตัวชี้ไปยัง array จริงที่อยู่ข้างหลัง (ptr+len+cap) สอง slice อาจ "ชี้" ไปที่ทีวี (array) เครื่องเดียวกัน เปลี่ยนช่องจากรีโมทไหนก็กระทบจอเดียวกันหมด — ถ้าไม่อยากแชร์จอ ต้องซื้อทีวีเครื่องใหม่ (copy/append([]T{}, ...))

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

a[low:high:max] คือ full slice expression ที่คนส่วนใหญ่ไม่เคยใช้ — พารามิเตอร์ตัวที่ 3 กำหนด cap ของ slice ใหม่ให้แคบลงได้ (max - low) ทำให้ append ครั้งถัดไป บังคับให้สร้าง array ใหม่แทนที่จะเขียนทับของเดิม เป็นเทคนิคที่ใช้ป้องกันบั๊ก "เขียนทับโดยไม่รู้ตัว" ข้างบน ได้โดยไม่ต้อง copy ทั้งก้อน:

b := a[:2:2] // cap ของ b ถูกบังคับให้เป็น 2 พอดี
b = append(b, 99) // cap ไม่พอ → สร้าง array ใหม่ทันที ไม่กระทบ a
English Summary

Go has no inheritance — composition via struct embedding is the idiom. Interfaces are satisfied implicitly ("duck typing" at compile time): "accept interfaces, return structs." Every type has a zero value; a nil map panics on write but not on read. Errors are ordinary return values, not exceptions — wrap with %w and inspect with errors.Is/errors.As. Slices share a backing array, so append on a sub-slice can silently overwrite the original if capacity allows it — a classic interview gotcha. Two deep-dive interview differentiators: the nil-interface-vs-nil- pointer trap (an interface is a (type, value) pair, so a nil pointer wrapped in an interface is not == nil), and the three-index full slice expression a[low:high:max] for capping capacity to force a fresh backing array on the next append.