25 คำถามสัมภาษณ์ Go ยอดนิยม: คู่มือฉบับสมบูรณ์สำหรับนักพัฒนา

พิชิตการสัมภาษณ์ Go ด้วย 25 คำถามที่ถูกถามบ่อย goroutine, channel, interface และรูปแบบการทำงานพร้อมกันพร้อมตัวอย่างโค้ด

คำถามสัมภาษณ์ Go - คู่มือเตรียมตัวฉบับสมบูรณ์

การสัมภาษณ์เชิงเทคนิคของ Go ประเมินความเข้าใจในแนวคิดหลักของภาษา ทั้งเรื่องการทำงานพร้อมกัน การจัดการหน่วยความจำ และรูปแบบเชิงสำนวน คู่มือนี้รวบรวม 25 คำถามที่พบบ่อยที่สุดพร้อมคำตอบโดยละเอียดและตัวอย่างโค้ด

คำแนะนำในการสัมภาษณ์

Go ให้ความสำคัญกับความเรียบง่ายและการอ่านง่าย ผู้สัมภาษณ์มักชื่นชอบคำตอบกระชับที่แสดงความเข้าใจเชิงลึกมากกว่าวิธีแก้ที่ซับซ้อนเกินจำเป็น

พื้นฐานของภาษา Go

1. var ต่างจาก := อย่างไร

การประกาศ var ระบุชนิดข้อมูลได้ชัดเจนและใช้ได้ที่ระดับ package ตัวดำเนินการ := อนุมานชนิดอัตโนมัติแต่ใช้ได้เฉพาะภายในฟังก์ชัน

declaration.gogo
package main

// Package level - var required
var globalConfig = "production"

func main() {
    // var with explicit type
    var count int = 10

    // var with type inference
    var name = "Alice"

    // Short declaration - functions only
    age := 25

    // Multiple declarations
    var (
        host = "localhost"
        port = 8080
    )
}

ภายในฟังก์ชันมักนิยมใช้รูปแบบสั้น := เพราะกระชับ ส่วน var ยังจำเป็นเมื่อประกาศตัวแปรระดับ package

2. ระบบชนิดข้อมูลของ Go ทำงานอย่างไร

Go ใช้การกำหนดชนิดแบบสแตติกพร้อมการอนุมานชนิด ภาษาให้ความแตกต่างระหว่างชนิดแบบค่า (คัดลอกเมื่อกำหนดค่า) กับชนิดแบบอ้างอิง (ใช้โครงสร้างพื้นฐานร่วมกัน)

types.gogo
package main

import "fmt"

func main() {
    // Value types - full copy
    a := [3]int{1, 2, 3}
    b := a          // Copies the array
    b[0] = 100      // Doesn't modify a
    fmt.Println(a)  // [1 2 3]

    // Reference types - share data
    slice1 := []int{1, 2, 3}
    slice2 := slice1    // Same underlying array
    slice2[0] = 100     // Also modifies slice1
    fmt.Println(slice1) // [100 2 3]

    // Maps are also references
    m1 := map[string]int{"a": 1}
    m2 := m1
    m2["a"] = 100
    fmt.Println(m1["a"]) // 100
}

อาเรย์เป็นชนิดแบบค่า ขณะที่ slice, map และ channel เป็นชนิดแบบอ้างอิง

3. อาเรย์กับ slice แตกต่างกันอย่างไร

อาเรย์มีขนาดคงที่ที่กำหนดในเวลาคอมไพล์ ส่วน slice เป็นมุมมองแบบไดนามิกบนอาเรย์ที่อยู่เบื้องหลังและประกอบด้วยสามส่วน ได้แก่ พอยน์เตอร์ ความยาว และความจุ

arrays_slices.gogo
package main

import "fmt"

func main() {
    // Array - fixed size, value type
    arr := [5]int{1, 2, 3, 4, 5}

    // Slice - view over the array
    slice := arr[1:4]  // [2 3 4]
    fmt.Printf("len=%d, cap=%d\n", len(slice), cap(slice))
    // len=3, cap=4

    // Modifications affect original array
    slice[0] = 20
    fmt.Println(arr) // [1 20 3 4 5]

    // Direct creation with make
    dynamic := make([]int, 3, 10)
    // len=3, cap=10

    // Append may reallocate
    dynamic = append(dynamic, 1, 2, 3, 4, 5)
}

ใน Go slice เป็นชนิดที่นิยมใช้ที่สุดสำหรับคอลเลกชันแบบไดนามิก

4. คำสั่ง defer ทำงานอย่างไร

defer กำหนดตารางให้เรียกใช้ฟังก์ชันเมื่อสิ้นสุดฟังก์ชันที่ครอบอยู่ การเรียกที่ถูกเลื่อนจะถูกซ้อนไว้และทำงานตามลำดับ LIFO (เข้าสุดท้าย ออกก่อน)

defer.gogo
package main

import (
    "fmt"
    "os"
)

func main() {
    // LIFO order
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    // Prints: 3, 2, 1
}

// Typical use case: resource cleanup
func readFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close() // Always executes

    // Read file...
    return os.ReadFile(path)
}

// Caution: arguments are evaluated immediately
func deferArgs() {
    x := 10
    defer fmt.Println(x) // Captures 10
    x = 20
    // Prints: 10
}

defer รับประกันการทำงานแม้ขณะเกิด panic จึงเหมาะกับการคืนทรัพยากร

5. interface ใน Go คืออะไร

interface กำหนดชุดของเมธอด ชนิดใดที่สร้างเมธอดเหล่านั้นจะเข้ากับ interface โดยอัตโนมัติโดยไม่ต้องประกาศชัดเจน

interfaces.gogo
package main

import "fmt"

// Interface definition
type Writer interface {
    Write([]byte) (int, error)
}

// Type that implicitly implements Writer
type FileLogger struct {
    path string
}

func (f *FileLogger) Write(data []byte) (int, error) {
    // Write to file
    fmt.Println("Writing to", f.path)
    return len(data), nil
}

// Empty interface - accepts any type
func printAny(v interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Type assertion
func process(w Writer) {
    // Type check
    if fl, ok := w.(*FileLogger); ok {
        fmt.Println("FileLogger with path:", fl.path)
    }
}

การสร้าง interface แบบนัยช่วยให้ package ต่าง ๆ ถูกแยกออกจากกันได้อย่างชัดเจน

การทำงานพร้อมกันและ goroutine

6. goroutine คืออะไร และต่างจาก thread อย่างไร

goroutine คือ thread น้ำหนักเบาที่บริหารโดย runtime ของ Go ใช้สแตกเพียงไม่กี่ KB (เทียบกับหลาย MB ของ thread ระบบ) และตัวจัดตารางเวลาของ Go จะสลับการทำงานของ goroutine นับพันไปยัง thread ระบบเพียงไม่กี่ตัว

goroutines.gogo
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    // Launch 1000 goroutines
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Goroutine %d finished\n", id)
        }(i) // Pass i by value
    }

    wg.Wait()
    fmt.Println("All goroutines completed")
}
ข้อผิดพลาดที่พบบ่อย

ตัวแปรของลูปควรส่งให้ goroutine แบบส่งค่าเสมอ มิฉะนั้น goroutine ทุกตัวอาจจับค่าสุดท้ายเหมือนกันทั้งหมด

7. channel ทำงานอย่างไร

channel ทำให้ goroutine สื่อสารและซิงโครไนซ์กันได้ มีทั้งแบบมีบัฟเฟอร์ (มีความจุ) และแบบไม่มีบัฟเฟอร์ (ซิงโครนัส)

channels.gogo
package main

import "fmt"

func main() {
    // Unbuffered channel - blocks until received
    ch := make(chan int)

    go func() {
        ch <- 42 // Blocks until read
    }()

    value := <-ch // Receives value
    fmt.Println(value)

    // Buffered channel - doesn't block until full
    buffered := make(chan string, 2)
    buffered <- "first"
    buffered <- "second"
    // buffered <- "third" // Would block

    fmt.Println(<-buffered) // "first"
    fmt.Println(<-buffered) // "second"
}

channel แบบไม่มีบัฟเฟอร์รับประกันการซิงโครไนซ์ ส่วน channel ที่มีบัฟเฟอร์ช่วยปลดล็อกเรื่องเวลาให้กับผู้ส่งและผู้รับ

8. ใช้ select กับหลาย channel อย่างไร

select รอการดำเนินการบน channel หลายตัวพร้อมกัน การดำเนินการที่พร้อมก่อนจะถูกทำงาน หากมีหลายตัวพร้อมพร้อมกันจะถูกสุ่มเลือก

select.gogo
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    // Wait with timeout
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("Timeout")
        }
    }

    // Non-blocking select with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("No message available")
    }
}

select คือเครื่องมือพื้นฐานสำหรับจัดการการทำงานพร้อมกันใน Go อย่างสง่างาม

9. ป้องกัน race condition อย่างไร

race condition เกิดเมื่อ goroutine หลายตัวเข้าถึงข้อมูลร่วมกันโดยไม่มีการซิงโครไนซ์ Go จัดเตรียมกลไกป้องกันไว้หลายแบบ

race_conditions.gogo
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// Solution 1: Mutex
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// Solution 2: RWMutex for read-heavy workloads
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // Multiple readers allowed
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // Single writer
    defer c.mu.Unlock()
    c.data[key] = value
}

// Solution 3: atomic for simple counters
var atomicCounter int64

func incrementAtomic() {
    atomic.AddInt64(&atomicCounter, 1)
}

func main() {
    // Detection: go run -race main.go
    counter := SafeCounter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Count:", counter.count)
}

แฟล็กของคอมไพเลอร์ -race ช่วยตรวจจับ race condition ระหว่างการทำงานจริง

10. อธิบายรูปแบบ worker pool

รูปแบบ worker pool จำกัดการทำงานพร้อมกันโดยสร้าง goroutine จำนวนคงที่เพื่อประมวลผลงานจากคิว

worker_pool.gogo
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(100 * time.Millisecond) // Simulate work
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup

    // Start workers
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Wait and close results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    for result := range results {
        fmt.Println("Result:", result)
    }
}

รูปแบบนี้ช่วยลดภาระหน่วยความจำและ CPU ที่อาจเกิดจากการสร้าง goroutine จำนวนมากเกินไป

พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การจัดการข้อผิดพลาดและ panic/recover

11. จัดการ error ใน Go อย่างไร

Go ใช้ค่ากลับแบบชัดเจนสำหรับ error โดยไม่มี exception ตามธรรมเนียม error เป็นพารามิเตอร์ลำดับสุดท้ายที่ถูกส่งกลับ

errors.gogo
package main

import (
    "errors"
    "fmt"
)

// Sentinel errors for comparison
var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("access unauthorized")
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "must be positive",
        }
    }
    return nil
}

func main() {
    // Basic check
    if err := validateAge(-5); err != nil {
        // Type assertion for custom error
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Field: %s\n", valErr.Field)
        }
    }

    // Sentinel error comparison
    err := findUser("unknown")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }
}

func findUser(id string) error {
    // Error wrapping with context
    return fmt.Errorf("findUser %s: %w", id, ErrNotFound)
}

การห่อด้วย %w ผูก error เข้าด้วยกันโดยที่ยังตรวจสอบ error ดั้งเดิมได้

12. ใช้ panic และ recover เมื่อใด

panic หยุดการทำงานปกติและคลายสแตก ส่วน recover จับ panic ใน defer และทำให้โปรแกรมทำงานต่อได้

panic_recover.gogo
package main

import "fmt"

func safeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()

    riskyOperation()
    return nil
}

func riskyOperation() {
    // Simulates an operation that can panic
    panic("something went wrong")
}

// Legitimate use case: initialization validation
func MustCompileRegex(pattern string) *Regexp {
    r, err := regexp.Compile(pattern)
    if err != nil {
        panic(err) // Programming error
    }
    return r
}

func main() {
    err := safeOperation()
    if err != nil {
        fmt.Println("Recovered error:", err)
    }
    fmt.Println("Program continues")
}
กฎทอง

panic ควรใช้เฉพาะกับข้อผิดพลาดเชิงโปรแกรม (invariant ที่ละเมิด) สำหรับ error ที่คาดเดาได้ (ไฟล์หาย ปัญหาเครือข่าย) ควรคืน error เสมอ

struct เมธอด และ embedding

13. receiver แบบค่าและแบบพอยน์เตอร์ต่างกันอย่างไร

receiver แบบค่ารับสำเนาของ struct ส่วน receiver แบบพอยน์เตอร์รับการอ้างอิงและสามารถแก้ไขข้อมูลต้นฉบับได้

receivers.gogo
package main

import "fmt"

type Counter struct {
    value int
}

// Value receiver - works on copy
func (c Counter) GetValue() int {
    return c.value
}

// Pointer receiver - modifies original
func (c *Counter) Increment() {
    c.value++
}

// Pointer receiver for large structs (avoids copy)
type LargeStruct struct {
    data [1000]int
}

func (l *LargeStruct) Process() {
    // Avoids copying 8000 bytes
}

func main() {
    c := Counter{value: 0}
    c.Increment() // Go automatically converts
    fmt.Println(c.GetValue()) // 1

    // Careful with interfaces
    var _ fmt.Stringer = &c // OK if method on *Counter
}

หลักการ: หากเมธอดใดใช้ receiver แบบพอยน์เตอร์ ทุกเมธอดในชนิดเดียวกันควรใช้ receiver แบบพอยน์เตอร์เพื่อความสม่ำเสมอ

14. embedding ใน Go ทำงานอย่างไร

embedding คือการนำชนิดหนึ่งมาฝังในอีกชนิดหนึ่งเพื่อรับเมธอดและฟิลด์มาด้วย ไม่ใช่การสืบทอดแบบดั้งเดิมแต่เป็นการประกอบ (composition)

embedding.gogo
package main

import "fmt"

type Logger struct {
    prefix string
}

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Embedding Logger
type Service struct {
    *Logger // Pointer embedding
    name    string
}

func NewService(name string) *Service {
    return &Service{
        Logger: &Logger{prefix: name},
        name:   name,
    }
}

func main() {
    svc := NewService("API")

    // Promoted method - direct access
    svc.Log("Starting")

    // Explicit access also works
    svc.Logger.Log("Explicit")

    // Promoted field
    fmt.Println(svc.prefix) // "API"
}

embedding มอบความยืดหยุ่นในการประกอบโครงสร้างโดยไม่ติดข้อจำกัดของการสืบทอด

15. ใช้รูปแบบ singleton ใน Go อย่างไร

แพ็กเกจ sync มี sync.Once สำหรับรับประกันให้การเริ่มต้นทำงานเพียงครั้งเดียวแม้มี goroutine หลายตัวเรียกพร้อมกัน

singleton.gogo
package main

import (
    "fmt"
    "sync"
)

type Database struct {
    connectionString string
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        fmt.Println("Single initialization")
        instance = &Database{
            connectionString: "postgres://...",
        }
    })
    return instance
}

func main() {
    // Concurrent calls - single initialization
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            db := GetDatabase()
            fmt.Printf("Instance: %p\n", db)
        }()
    }
    wg.Wait()
}

sync.Once ปลอดภัยต่อการทำงานพร้อมกันและสง่างามกว่าการใช้ mutex ร่วมกับ double-check locking

context และการยกเลิกงาน

16. แพ็กเกจ context มีไว้เพื่ออะไร

แพ็กเกจ context จัดการ deadline สัญญาณยกเลิก และค่าผูกกับคำขอตลอดต้นไม้การเรียกใช้

context.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Context with timeout
    ctx, cancel := context.WithTimeout(
        context.Background(),
        2*time.Second,
    )
    defer cancel() // Always call cancel

    result := make(chan string, 1)

    go func() {
        // Simulate long operation
        time.Sleep(3 * time.Second)
        result <- "completed"
    }()

    select {
    case res := <-result:
        fmt.Println(res)
    case <-ctx.Done():
        fmt.Println("Timeout:", ctx.Err())
    }
}

// Propagation through functions
func fetchData(ctx context.Context, url string) ([]byte, error) {
    // Early check
    if ctx.Err() != nil {
        return nil, ctx.Err()
    }

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    // HTTP client respects context
    resp, err := http.DefaultClient.Do(req)
    // ...
}

ฟังก์ชันที่อาจใช้เวลานานควรรับ context.Context เป็นพารามิเตอร์ตัวแรกเสมอ

17. จัดการ graceful shutdown ของโปรแกรมอย่างไร

สัญญาณระบบเช่น SIGINT และ SIGTERM สามารถดักจับเพื่อปิดโปรแกรมอย่างสะอาด

graceful_shutdown.gogo
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Context cancelled on signal
    ctx, stop := signal.NotifyContext(
        context.Background(),
        syscall.SIGINT,
        syscall.SIGTERM,
    )
    defer stop()

    // Start server
    server := startServer()

    // Wait for signal
    <-ctx.Done()
    fmt.Println("\nShutting down...")

    // Timeout for graceful shutdown
    shutdownCtx, cancel := context.WithTimeout(
        context.Background(),
        5*time.Second,
    )
    defer cancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("Shutdown error:", err)
    }

    fmt.Println("Shutdown complete")
}

รูปแบบนี้ช่วยให้การเชื่อมต่อที่ทำงานอยู่ปิดอย่างถูกต้องก่อนที่โปรแกรมจะหยุด

การทดสอบและ benchmark

18. เขียนเทสต์ใน Go อย่างไร

แพ็กเกจ testing ที่ติดมาให้ฟังก์ชันการทำงานพื้นฐาน เทสต์อยู่ในไฟล์ *_test.go

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

// Table-driven tests
func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"mixed", -1, 5, 4},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

table-driven test คือรูปแบบเชิงสำนวนของ Go สำหรับทดสอบหลายกรณีพร้อมกัน

19. เขียน benchmark อย่างไร

benchmark ใช้ testing.B และรันด้วย go test -bench

benchmark_test.gogo
package main

import (
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

// Typical results:
// BenchmarkStringConcat-8      50000    28000 ns/op
// BenchmarkStringBuilder-8   1000000     1200 ns/op

benchmark ช่วยเปิดเผยความต่างของประสิทธิภาพระหว่างการนำไปใช้แต่ละแบบ

Generics (Go 1.18+)

20. ใช้ generics ใน Go อย่างไร

Go 1.18 เพิ่มพารามิเตอร์ของชนิดข้อมูล จึงเขียนโค้ดแบบทั่วไปได้โดยยังคงความปลอดภัยของชนิด

generics.gogo
package main

import "fmt"

// Generic function
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Custom type constraint
type Number interface {
    int | int64 | float64
}

func Sum[T Number](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

// Generic type
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func main() {
    // Usage
    doubled := Map([]int{1, 2, 3}, func(n int) int {
        return n * 2
    })
    fmt.Println(doubled) // [2 4 6]

    fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15

    stack := &Stack[string]{}
    stack.Push("hello")
    stack.Push("world")
    val, _ := stack.Pop()
    fmt.Println(val) // "world"
}

generics ลดความจำเป็นในการเขียนโค้ดซ้ำหรือใช้ interface{}

โมดูลและ dependency

21. ระบบโมดูลของ Go ทำงานอย่างไร

โมดูล Go จัดการ dependency ด้วยการกำกับเวอร์ชันแบบ semantic ไฟล์ go.mod กำหนดโมดูลและ dependency ของมัน

go.mod examplego
module github.com/user/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
)

// Essential commands:
// go mod init github.com/user/project
// go mod tidy        - clean dependencies
// go get package@v1.2.3 - add/update
// go mod vendor      - copy locally
bash
# Updating dependencies
go get -u ./...           # All dependencies
go get -u=patch ./...     # Patches only

ไฟล์ go.sum เก็บค่าเช็คซัมแบบเข้ารหัสเพื่อยืนยันความถูกต้องของ dependency

22. โครงสร้างโปรเจกต์ Go ควรเป็นอย่างไร

โครงสร้างมาตรฐานยึดตามธรรมเนียมของชุมชนโดยไม่บังคับกฎเข้มงวด

text
myproject/
├── cmd/
│   └── api/
│       └── main.go      # Entry point
├── internal/            # Private to module
│   ├── handler/
│   ├── service/
│   └── repository/
├── pkg/                 # Reusable external code
├── go.mod
├── go.sum
└── README.md

โฟลเดอร์ internal มีคุณสมบัติพิเศษ คือเนื้อหาภายในไม่สามารถถูกนำเข้าโดยโมดูลอื่น

คำถามขั้นสูง

23. garbage collector ของ Go ทำงานอย่างไร

Go ใช้ garbage collector แบบ tricolor mark-and-sweep ที่ทำงานพร้อมกันและออกแบบมาเพื่อรองรับ latency ต่ำ

gc_optimization.gogo
package main

import "runtime"

func main() {
    // GC configuration
    // GOGC=100 (default) - triggers GC when heap doubles

    // Force GC
    runtime.GC()

    // Memory statistics
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)

    println("Alloc:", stats.Alloc)
    println("NumGC:", stats.NumGC)
    println("PauseTotalNs:", stats.PauseTotalNs)
}

// Optimization techniques
// 1. Reuse allocations with sync.Pool
// 2. Pre-allocate slices with make([]T, 0, cap)
// 3. Avoid repeated string/[]byte conversions
// 4. Use pointers for large structs

ตัวแปรสภาพแวดล้อม GODEBUG=gctrace=1 แสดง trace ของ GC

24. อธิบายตัวจัดตารางเวลา (scheduler) ของ Go

ตัวจัดตารางเวลาของ Go ใช้โมเดล M:N ที่จับคู่ goroutine จำนวน N ตัวเข้ากับ thread ระบบจำนวน M ตัว มีสามองค์ประกอบ ได้แก่ G (goroutine) M (thread) และ P (โปรเซสเซอร์เชิงตรรกะ)

scheduler.gogo
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Number of logical processors (P)
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

    // Number of active goroutines
    fmt.Println("NumGoroutine:", runtime.NumGoroutine())

    // Yield processor to other goroutines
    runtime.Gosched()

    // M:P:G model
    // - G: goroutine (lightweight stack ~2KB)
    // - M: OS thread (machine)
    // - P: logical processor (execution context)
    //
    // Each P has a local queue of Gs
    // Work stealing when queue is empty
}

ตั้งแต่ Go 1.14 ตัวจัดตารางเวลาเป็นแบบ preemptive จึงไม่ปล่อยให้ goroutine ตัวใดยึดครอง P ทั้งหมด

25. ปรับประสิทธิภาพใน Go อย่างไร

การปรับประสิทธิภาพเริ่มต้นจากการทำ profiling เพื่อหาคอขวด

profiling.gogo
package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    // Code to profile...

    // Memory profiling
    mf, _ := os.Create("mem.prof")
    defer mf.Close()
    pprof.WriteHeapProfile(mf)
}

// Analysis: go tool pprof cpu.prof

// Common optimization techniques:
// 1. Avoid allocations in hot loops
// 2. Use sync.Pool for reusable objects
// 3. Prefer []byte over string for mutations
// 4. Use bufio for I/O
// 5. Batch database operations
กฎของการปรับแต่ง

ควรวัดผลก่อนแล้วจึงปรับแต่ง การ profiling มักเปิดเผยคอขวดที่ไม่คาดคิด

บทสรุป

คำถามทั้ง 25 ข้อนี้ครอบคลุมแนวคิดพื้นฐานที่ใช้ประเมินในการสัมภาษณ์ Go:

รายการเตรียมตัว:

  • ✅ ความเชี่ยวชาญใน goroutine และ channel
  • ✅ เข้าใจ interface แบบนัย
  • ✅ จัดการ error ตามแนวทางของ Go
  • ✅ ใช้ context อย่างถูกต้อง
  • ✅ รูปแบบการทำงานพร้อมกัน (mutex, worker pool)
  • ✅ การทดสอบและ benchmark
  • ✅ ความรู้เรื่อง generics ตั้งแต่ Go 1.18+

กุญแจสู่ความสำเร็จในการสัมภาษณ์ Go คือ การแสดงความเข้าใจในการแลกเปลี่ยนระหว่างความเรียบง่ายกับประสิทธิภาพ และรู้ว่าเมื่อใดควรเลือกใช้รูปแบบการทำงานพร้อมกันแบบใด

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#go
#golang
#interview
#concurrency
#goroutines

แชร์

บทความที่เกี่ยวข้อง