Go 면접 핵심 25문항: 개발자를 위한 완전 가이드

Go 면접에 가장 많이 등장하는 25개 질문으로 합격을 노리세요. 고루틴, 채널, 인터페이스, 동시성 패턴을 코드 예제로 정리했습니다.

Go 면접 질문 - 완전 준비 가이드

Go 기술 면접에서는 언어의 핵심 개념인 동시성, 메모리 관리, 관용적 패턴에 대한 이해가 평가됩니다. 이 가이드는 가장 자주 등장하는 25개 질문을 상세한 답변과 코드 예제와 함께 정리합니다.

면접 팁

Go는 단순함과 가독성을 중시합니다. 면접관은 지나치게 복잡한 풀이보다 깊은 이해를 보여주는 간결한 답변을 선호합니다.

Go 언어의 기초

1. var:=의 차이는 무엇입니까

var 선언은 타입을 명시할 수 있고 패키지 수준에서도 사용할 수 있습니다. := 연산자는 타입을 자동으로 추론하지만 함수 내부에서만 사용할 수 있습니다.

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가 필요합니다.

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. Go에서 인터페이스는 무엇입니까

인터페이스는 일련의 메서드를 정의합니다. 해당 메서드를 구현하는 모든 타입은 명시적인 선언 없이 인터페이스를 암묵적으로 만족합니다.

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)
    }
}

암묵적 구현 덕분에 패키지 사이의 결합도를 낮게 유지할 수 있습니다.

동시성과 고루틴

6. 고루틴이란 무엇이며 스레드와 어떻게 다릅니까

고루틴은 Go 런타임이 관리하는 가벼운 스레드입니다. 스택은 수 KB 수준(시스템 스레드의 수 MB와 대비)이며, Go 스케줄러는 수천 개의 고루틴을 소수의 시스템 스레드에 다중화합니다.

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")
}
흔한 함정

반복문 변수는 항상 값으로 고루틴에 전달해야 합니다. 그렇지 않으면 모든 고루틴이 동일한 마지막 값을 캡처할 수 있습니다.

7. 채널의 동작 방식을 설명해 주세요

채널은 고루틴 사이의 통신과 동기화를 가능하게 합니다. 버퍼가 있는(용량을 가지는) 채널과 버퍼가 없는(동기) 채널이 있습니다.

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"
}

버퍼가 없는 채널은 동기화를 보장하고, 버퍼가 있는 채널은 송신자와 수신자의 시간적 결합을 느슨하게 만듭니다.

8. 여러 채널과 함께 select를 어떻게 사용합니까

select는 여러 채널 작업을 동시에 기다립니다. 가장 먼저 준비된 작업이 실행되며, 동시에 준비된 경우에는 무작위로 선택됩니다.

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은 여러 고루틴이 동기화 없이 공유 데이터에 접근할 때 발생합니다. 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 패턴은 고정된 수의 고루틴이 큐에서 작업을 가져와 처리하도록 하여 동시성을 제한합니다.

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 부담을 줄여 줍니다.

Go 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

오류 처리와 panic/recover

11. Go에서 오류를 어떻게 처리합니까

Go는 예외 대신 명시적인 반환값으로 오류를 표현합니다. 관례상 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로 감싸면 오류를 연결하면서도 원본 오류를 검사할 수 있는 능력을 유지할 수 있습니다.

12. panic과 recover는 언제 사용해야 합니까

panic은 정상 흐름을 중단하고 스택을 풀어냅니다. recoverdefer 안에서 panic을 잡아 실행을 계속할 수 있게 합니다.

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은 프로그래밍 오류(불변식 위반)에만 사용해야 합니다. 예상되는 오류(파일 누락, 네트워크 문제)에는 항상 error를 반환하는 편이 안전합니다.

구조체, 메서드, 임베딩

13. 값 receiver와 포인터 receiver의 차이는 무엇입니까

값 receiver는 구조체의 사본을 받고, 포인터 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. Go에서 임베딩은 어떻게 동작합니까

임베딩은 한 타입을 다른 타입에 포함시켜 메서드와 필드를 물려받게 합니다. 이는 고전적 상속이 아니라 컴포지션입니다.

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"
}

임베딩은 상속의 경직성을 피하면서 유연한 조합을 가능하게 합니다.

15. Go에서 싱글턴 패턴을 어떻게 구현합니까

sync 패키지의 sync.Once는 동시 고루틴 환경에서도 초기화가 단 한 번만 실행되도록 보장합니다.

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는 스레드 안전하며 더블 체크 잠금을 사용하는 뮤텍스보다 우아합니다.

context와 취소

16. context 패키지는 어디에 사용됩니까

context 패키지는 데드라인, 취소 시그널, 요청 단위 값을 호출 트리 전체에 걸쳐 관리합니다.

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")
}

이 패턴은 종료 전에 활성 연결이 정상적으로 마무리되도록 보장합니다.

테스트와 벤치마크

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)
            }
        })
    }
}

테이블 기반 테스트는 여러 케이스를 검증하는 Go의 관용적 패턴입니다.

19. 벤치마크는 어떻게 작성합니까

벤치마크는 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

벤치마크는 구현 간 성능 차이를 명확히 드러냅니다.

제네릭(Go 1.18 이상)

20. 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"
}

제네릭은 코드 중복이나 interface{} 사용 필요성을 없애 줍니다.

모듈과 의존성

21. Go 모듈 시스템은 어떻게 동작합니까

Go 모듈은 시맨틱 버전 관리로 의존성을 다룹니다. go.mod 파일이 모듈과 의존성을 정의합니다.

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 파일에는 의존성의 무결성을 보장하는 암호학적 체크섬이 들어 있습니다.

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 폴더는 특별합니다. 그 내용은 다른 모듈이 import할 수 없습니다.

심화 질문

23. Go의 가비지 컬렉터는 어떻게 동작합니까

Go는 동시성을 활용한 트라이컬러 mark-and-sweep 가비지 컬렉터를 사용하며, 낮은 지연 시간에 최적화되어 있습니다.

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은 GC 추적 정보를 출력합니다.

24. Go 스케줄러를 설명해 주세요

Go 스케줄러는 N개의 고루틴을 M개의 시스템 스레드에 매핑하는 M:N 모델을 사용하며, G(고루틴), M(스레드), 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부터 선점형이어서 단일 고루틴이 P를 독점하지 않습니다.

25. Go에서 성능을 어떻게 최적화합니까

최적화는 병목 구간을 찾기 위한 프로파일링부터 시작합니다.

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
최적화 원칙

최적화 전에 측정이 우선입니다. 프로파일링은 실제 병목에 대해 종종 예상 밖의 진실을 알려 줍니다.

결론

이 25개의 질문은 Go 면접에서 평가되는 기본 개념을 모두 다룹니다.

준비 체크리스트:

  • ✅ 고루틴과 채널의 숙달
  • ✅ 암묵적 인터페이스에 대한 이해
  • ✅ Go다운 오류 처리
  • ✅ context의 적절한 사용
  • ✅ 동시성 패턴(뮤텍스, worker pool)
  • ✅ 테스트와 벤치마크
  • ✅ Go 1.18 이상의 제네릭에 대한 지식

Go 면접에서 성공의 열쇠는 단순함과 성능 사이의 트레이드오프를 이해하고, 어떤 동시성 패턴을 언제 적용해야 하는지 분명히 아는 것입니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#go
#golang
#interview
#concurrency
#goroutines

공유

관련 기사