중급 Go 준비

중급 Go 백엔드 면접 질문

실무에서의 트레이드오프, 동시성, 서비스 동작을 설명해야 하는 백엔드 개발자를 위한 선별된 중급 Go 면접 질문 15개

중급 Go AI 면접 시작하기신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

타입 시스템

1Go에서 포인터, 슬라이스, 맵, 채널, 함수, 인터페이스에 대해 nil이 각각 어떻게 다르게 동작하는지 설명해 주세요.

Go에서 nil은 포인터, 슬라이스, 맵, 채널, 함수, 인터페이스의 기본값(zero value)이지만, 이러한 nil 값에 수행할 수 있는 연산은 타입마다 다릅니다. nil 포인터는 nil과 비교할 수 있지만, 역참조(dereferencing)하면 패닉(panic)이 발생합니다. nil 슬라이스는 길이(length)와 용량(capacity)이 모두 0이며, `for range` 문을 순회하거나 `append` 함수로 요소를 추가할 수 있습니다. nil 맵은 값을 읽거나 순회할 수 있지만, 새 키와 값을 할당하려고 하면 패닉이 발생합니다. nil 채널에 데이터를 송신하거나 수신하려고 하면 무한히 블로킹되며, nil 채널을 닫으려고(`close`) 하면 패닉이 발생합니다. nil 함수를 호출하면 패닉이 발생합니다. 인터페이스는 동적 타입과 동적 값을 모두 가지고 있지 않을 때만 nil입니다. nil 포인터와 같이 타입이 있는 nil 값을 담고 있는 인터페이스는 인터페이스 자체가 nil이 아닙니다.

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func returnsTypedNil() error {
	var e *MyErr = nil
	return e
}

func main() {
	var p *int = nil
	var s []int = nil
	var m map[string]int = nil
	var err error = returnsTypedNil()

	fmt.Println(p == nil)
	fmt.Println(len(s), cap(s), s == nil)
	fmt.Println(m["missing"])
	fmt.Println(err == nil)
}
AI 코치와 함께 이 질문에 답해 보세요

2Go에서 비교 가능한 타입(comparable types)이란 무엇이며, 비교 가능성 규칙이 맵 키, 동등성 비교, 제네릭 제약 조건에 어떤 영향을 미치나요?

Go에서 비교 가능한 타입이란 값을 `==` 및 `!=`로 비교할 수 있는 타입을 말합니다. 기본 타입, 포인터, 채널, 인터페이스, 그리고 모든 필드나 요소가 비교 가능한 구조체 및 배열은 비교 가능합니다. 반면 슬라이스, 맵, 함수는 `nil`과의 비교를 제외하고는 비교할 수 없습니다. 맵의 키는 반드시 비교 가능해야 합니다. 동등성 비교는 각 타입의 비교 규칙을 따르며, 인터페이스 비교는 런타임의 동적 구체 값에 따라 달라집니다. 비교 대상 인터페이스가 비교 불가능한 동적 값을 담고 있다면 비교 시 패닉(panic)이 발생합니다. 제네릭에서는 미리 선언된 `comparable` 제약 조건을 통해 타입 매개변수를 `==`/`!=`로 비교하고 맵 키로 사용할 수 있도록 허용합니다.

package main

import "fmt"

type Point struct{ X, Y int }      // comparable
type Bag struct{ Items []string }  // not comparable because of slice field

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target {
            return true
        }
    }
    return false
}

func main() {
    p1, p2 := Point{1, 2}, Point{1, 2}
    fmt.Println(p1 == p2)

    m := map[Point]string{p1: "value"}
    fmt.Println(m[p2])

    fmt.Println(Contains([]string{"a", "b"}, "b"))

    var a any = []int{1}
    var b any = []int{1}
    _, _ = a, b
    // fmt.Println(a == b) // panic: comparing uncomparable type []int
}
AI 코치와 함께 이 질문에 답해 보세요

3Go에서는 바이트, 룬(rune), UTF-8 (8-bit Unicode Transformation Format) 인코딩 텍스트를 어떻게 표현하며, len(s)가 사용자가 눈으로 보는 문자 수와 다를 수 있는 이유는 무엇인가요?

Go에서 `byte`는 `uint8`의 별칭(alias)으로 단일 원시 바이트(raw byte)를 나타내며, `rune`은 `int32`의 별칭으로 유니코드 코드 포인트(Unicode code point)를 나타냅니다. `string`은 읽기 전용 바이트 시퀀스로, 대개 UTF-8로 인코딩된 텍스트이지만 임의의 바이트도 포함할 수 있습니다. `len(s)`는 룬이나 눈에 보이는 문자 수가 아닌 바이트 수를 반환합니다. 문자열을 인덱싱하면 바이트가 반환되고, `for range` 루프로 문자열을 순회하면 UTF-8을 디코딩하여 바이트 인덱스와 룬을 함께 제공합니다. `len(s)`가 눈에 보이는 문자 수와 다를 수 있는 이유는 UTF-8에서 코드 포인트당 여러 바이트를 사용할 수 있는 데다, 결합 문자(combining mark)나 이모지 시퀀스처럼 사용자가 인지하는 하나의 문자가 여러 코드 포인트로 구성될 수 있기 때문입니다.

package main

import "fmt"

func main() {
    s := "é🙂"

    fmt.Println(len(s))         // bytes: é is 2 bytes, 🙂 is 4 bytes
    fmt.Println(len([]rune(s))) // Unicode code points

    fmt.Printf("first byte: %x\n", s[0])

    for i, r := range s {
        fmt.Printf("byte index %d: %q U+%04X\n", i, r, r)
    }
}
AI 코치와 함께 이 질문에 답해 보세요

4타입 별칭(type alias)과 정의된 타입(defined type)은 어떻게 다르며, 각각 언제 사용해야 하나요?

`type UserID int64`와 같은 정의된 타입은 `int64`를 기저 타입(underlying type)으로 갖는 완전히 새로운 독립적인 타입을 만듭니다. 명시적 형변환 없이는 `int64`에 자유롭게 할당할 수 없으며, 고유한 메서드를 가질 수 있습니다. 반면 `type UserID = int64`와 같은 타입 별칭은 동일한 타입에 대한 또 다른 이름일 뿐이므로, 타입 동일성과 할당 가능성이 그대로 유지됩니다. 도메인 모델링, 타입 안전성 강화, 전용 메서드 정의가 필요할 때는 정의된 타입을 사용하고, 새로운 타입을 도입하지 않고 리팩터링, 마이그레이션 또는 코드 호환성을 유지해야 할 때는 주로 타입 별칭을 사용합니다.

type UserID int64      // defined type
type AccountID = int64 // alias

func takeInt64(x int64) {}

var u UserID = 10
var a AccountID = 20

takeInt64(int64(u)) // explicit conversion required
takeInt64(a)        // ok: AccountID is exactly int64

func (u UserID) String() string {
    return fmt.Sprintf("user:%d", u)
}
AI 코치와 함께 이 질문에 답해 보세요

5Go는 부동소수점 특수값을 어떻게 처리하며, 백엔드 시스템에서 주의해야 할 동등성 비교 함정에는 무엇이 있나요?

Go의 `float32`와 `float64`는 양/음의 무한대(infinity) 및 NaN (Not a Number) 같은 특수값을 포함하여 IEEE-754 표준 동작을 따릅니다. `float64`의 경우 `math.Inf`, `math.IsInf`, `math.NaN`, `math.IsNaN` 등의 보조 함수가 제공됩니다. NaN은 자기 자신을 포함하여 어떤 값과도 같지 않으므로, `x`가 NaN일 때 `x == x`는 false가 됩니다. 또한 연산된 부동소수점 값의 정확한 일치 여부를 비교하는 것은 위험합니다. 반올림과 정밀도 한계로 인해 수학적으로 같은 값이라도 달라질 수 있기 때문입니다. 따라서 도메인에 맞는 허용 오차(tolerance)를 사용하거나, 화폐 금액과 같이 정확해야 하는 비즈니스 값에는 부동소수점 타입 사용을 피해야 합니다. 부동소수점 값을 맵의 키로 사용할 수는 있지만, NaN 키는 문제가 됩니다. 맵의 조회는 동등성 비교에 의존하지만 NaN은 자기 자신과도 같다고 평가되지 않기 때문입니다.

nan := math.NaN()
fmt.Println(nan == nan)
fmt.Println(math.IsNaN(nan))

m := map[float64]string{nan: "bad key"}
fmt.Println(m[nan]) // lookup fails because nan != nan
AI 코치와 함께 이 질문에 답해 보세요

6Go의 구조체 임베딩(struct embedding)과 승격된 필드 및 메서드의 동작 방식을 설명해 주세요.

Go에서 구조체 임베딩은 `type User struct { Person }`과 같이 명시적인 필드명 없이 타입만 선언하는 것을 의미합니다. 임베딩된 값은 여전히 실제 필드로 존재하므로 `u.Person`처럼 접근할 수 있지만, 외부로 노출(exported)되거나 접근 가능한 필드와 메서드가 승격(promotion)되므로 호출자는 임베딩된 필드를 거치는 축약 문법으로 `u.Name`이나 `u.Greet()`과 같은 선택자(selector)를 작성할 수 있습니다. 임베딩은 전통적인 상속이 아닌 컴포지션(합성)이므로 외부 타입이 임베딩된 타입의 서브타입이 자동으로 되는 것은 아닙니다. 승격된 선택자 간에 이름 충돌이 발생하면 Go는 임의로 추측하지 않으므로, 모호한 이름은 명시적으로 수식(qualification)해야 하거나 외부 값을 통해 직접 선택할 수 없게 됩니다.

package main

import "fmt"

type Person struct {
    Name string
}

func (p Person) Greet() string {
    return "hi " + p.Name
}

type User struct {
    Person // embedded field; field name is Person
    ID     int
}

func main() {
    u := User{Person: Person{Name: "Ana"}, ID: 10}

    fmt.Println(u.Name)        // promoted field: u.Person.Name
    fmt.Println(u.Greet())     // promoted method: u.Person.Greet()
    fmt.Println(u.Person.Name) // explicit qualification still works
}
AI 코치와 함께 이 질문에 답해 보세요

자료 구조

7슬라이스 재슬라이싱(reslicing)과 대입 연산으로 인해 여러 슬라이스가 동일한 내부 배열을 공유하게 되는 원리와, 이로 인해 발생할 수 있는 버그를 설명해 주세요.

슬라이스 값은 내부 배열을 가리키는 헤더 구조체입니다. 슬라이스를 대입하거나 함수에 인자로 전달할 때 요소 전체가 아닌 헤더만 복사됩니다. 재슬라이싱을 수행하면 동일한 내부 배열의 특정 범위를 가리키는 또 다른 헤더가 생성됩니다. 따라서 여러 슬라이스가 동일한 저장 공간을 참조(aliasing)할 수 있습니다. 즉, 한 슬라이스를 통해 요소를 변경하면 다른 슬라이스에도 그대로 반영되며, 여유 용량(capacity)이 남아 있는 상태에서 한 슬라이스에 `append`를 수행하면 다른 슬라이스에서 참조 중인 데이터를 덮어쓸 수 있습니다. 이로 인해 의도치 않은 데이터 변경, 결과 왜곡, 작은 서브 슬라이스로 인한 거대한 내부 배열의 메모리 누수 유지, 동시 접근 시 데이터 경합(data race) 등의 버그가 발생할 수 있습니다. 의도치 않은 공유를 방지하려면 `copy` 함수나 `append([]T(nil), s...)`를 사용해 방어적 복사를 수행하거나, `append` 전에 전체 슬라이스 표현식(full-slice expression)으로 용량을 제한해야 합니다.

package main

import "fmt"

func main() {
	base := []int{1, 2, 3, 4}
	a := base[:2] // len 2, cap 4
	b := base[2:] // len 2, cap 2

	a[0] = 99
	fmt.Println(base, a, b)

	a = append(a, 77) // reuses base's backing array, overwrites base[2]
	fmt.Println(base, a, b)

	c := append([]int(nil), base[:2]...) // defensive copy
	c[0] = 42
	fmt.Println(base, c)
}
AI 코치와 함께 이 질문에 답해 보세요

8append 시 슬라이스(slice) 확장의 개념적 동작 방식과 반복적인 재할당이 성능에 미치는 영향을 설명해 주세요.

append로 슬라이스에 요소를 추가할 때 용량(capacity)이 충분하다면 기존의 기반 배열(backing array)에 새 값을 씁니다. 용량이 부족하면 Go는 더 큰 기반 배열을 새로 할당하고 기존 요소를 복사한 뒤 새 요소를 쓰고 새 저장소를 가리키는 슬라이스 헤더를 반환합니다. 정확한 확장 정책은 런타임 구현에 따라 다르지만, 개념적으로는 반복적인 append의 분할 상환(amortized) 시간 복잡도가 효율적이 되도록 충분한 크기로 늘어납니다. 하지만 반복적인 재할당은 복사로 인한 CPU 비용 발생, 새로운 메모리 할당, GC(Garbage Collector) 압박 증가를 초래하며 기존 슬라이스 별칭(alias)과의 데이터 공유 관계를 끊어버릴 수 있습니다. 예상 크기를 미리 알고 있다면 재할당 횟수를 줄이기 위해 append로 구성할 때는 make([]T, 0, n)으로, 인덱스로 직접 채울 때는 make([]T, n)으로 사전 할당하는 것이 좋습니다.

package main

func collectNoPrealloc(input []int) []int {
	var out []int
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}

func collectPrealloc(input []int) []int {
	out := make([]int, 0, len(input))
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}
AI 코치와 함께 이 질문에 답해 보세요

패키지

9Go에서 초기화 시점 등록 패턴(init-time registration pattern)이란 무엇이며, 빈 식별자 임포트(blank import)의 부수 효과 및 전역 레지스트리와 관련하여 어떤 위험이 따르나요?

초기화 시점 등록 패턴은 패키지가 `init` 함수 내에서 공유 레지스트리에 자신의 구현체를 등록하는 패턴입니다. `_ "example.com/driver"`와 같은 빈 식별자 임포트(blank import)는 오직 부수 효과(side effect)만을 위해 패키지를 가져올 때 주로 사용되며, 공개된 이름을 직접 참조하지 않더라도 해당 패키지의 `init` 함수가 실행되도록 만듭니다. 이 패턴은 드라이버, 코덱, 플러그인, 메트릭, 직렬화기 등의 확장 지점에서 널리 사용됩니다. 그러나 숨겨진 의존성과 시작 시점의 부수 효과, 전역적인 가변 상태, 중복 등록이나 실행 순서에 따른 취약성, 테스트 격리의 어려움, 의존성 연결의 불투명성 등의 위험이 있습니다. 따라서 신중하게 사용하고 명확히 문서화해야 하며, 명시적 등록 방식을 적용하거나 멱등성 및 동시성이 보장되는 레지스트리, 혹은 테스트를 위해 주입이나 초기화가 가능한 레지스트리를 설계하여 문제를 완화해야 합니다.

// registry/registry.go
package registry

import "fmt"

type Factory func() string

var factories = map[string]Factory{}

func Register(name string, f Factory) {
    if _, exists := factories[name]; exists {
        panic("duplicate registration: " + name)
    }
    factories[name] = f
}

func New(name string) (string, error) {
    f, ok := factories[name]
    if !ok {
        return "", fmt.Errorf("unknown implementation %q", name)
    }
    return f(), nil
}

// jsondriver/jsondriver.go
package jsondriver

import "example.com/app/registry"

func init() {
    registry.Register("json", func() string { return "json impl" })
}

// main.go
package main

import (
    "fmt"

    "example.com/app/registry"
    _ "example.com/app/jsondriver" // imported only for init side effect
)

func main() {
    v, _ := registry.New("json")
    fmt.Println(v)
}
AI 코치와 함께 이 질문에 답해 보세요

오류 처리

10반환 에러를 수정할 수 있는 정리 작업을 구현할 때 defer, panic, 명명된 반환값은 어떻게 상호작용하나요?

지연된 함수(`defer`)는 반환값이 할당된 후, 함수가 호출자에게 반환되기 직전에 실행됩니다. 그렇기 때문에 지연된 클로저는 명명된 `err`와 같은 명명된 반환값을 읽거나 수정할 수 있습니다. 이는 `Close`, `Commit` 등 정리 작업에서 발생한 에러를 반환 에러에 추가할 때 흔히 사용되며, 이상적으로는 원래의 주요 에러를 덮어쓰지 않고 보존해야 합니다. 패닉 전파 과정에서도 지연된 함수는 정상적으로 실행됩니다. 지연된 함수 내에서 `recover`를 호출하여 명명된 반환값을 설정할 수도 있지만, 이는 의도된 패닉 경계로 제한해야 합니다. 또한 `err` 같은 명명된 반환 변수가 섀도잉되지 않도록 주의해야 합니다. 그렇지 않으면 `defer`가 의도한 것과 다른 변수를 참조하거나 수정할 수 있습니다.

func writeFile(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}

	defer func() {
		cerr := f.Close()
		if cerr == nil {
			return
		}
		if err != nil {
			err = errors.Join(err, fmt.Errorf("close %s: %w", path, cerr))
		} else {
			err = fmt.Errorf("close %s: %w", path, cerr)
		}
	}()

	_, err = f.Write(data)
	if err != nil {
		return fmt.Errorf("write %s: %w", path, err)
	}
	return nil
}
AI 코치와 함께 이 질문에 답해 보세요

11Go의 에러 래핑 체인에서 errors.Is, errors.As, %w는 어떻게 동작하나요?

`fmt.Errorf`에 `%w`를 사용하면 컨텍스트를 추가하면서 기저 에러(underlying error)를 감싸는 새로운 에러를 생성합니다. 래퍼(wrapper)는 `Unwrap`을 통해 내부 에러를 노출하며, 표준 라이브러리가 탐색할 수 있는 체인이나 트리 구조를 형성합니다. `errors.Is(err, target)`는 `err` 또는 감싸고 있는 에러 중 대상 에러와 일치하는 것이 있는지 확인합니다. `errors.As(err, &target)`는 `err` 또는 감싸고 있는 에러가 대상 타입에 할당 가능한지 확인하고, 일치하는 값을 제공된 포인터에 저장합니다.

package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("not found")

type QueryError struct {
	Query string
	Err   error
}

func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
func (e *QueryError) Unwrap() error { return e.Err }

func load() error {
	return fmt.Errorf("load user: %w", &QueryError{Query: "select user", Err: ErrNotFound})
}

func main() {
	err := load()
	fmt.Println(errors.Is(err, ErrNotFound))

	var qe *QueryError
	fmt.Println(errors.As(err, &qe), qe.Query)
}
AI 코치와 함께 이 질문에 답해 보세요

12센티널 에러(sentinel error)란 무엇이며, 사용자 정의 타입 에러나 더 풍부한 도메인 에러 모델과 비교했을 때 어떤 트레이드오프가 있나요?

센티널 에러는 호출자가 확인할 수 있는 특정 조건을 나타내기 위해 미리 정의해 둔 고유한 에러 값으로, 주로 `var ErrNotFound = errors.New("not found")`와 같은 패키지 수준 변수로 정의됩니다. 에러 래핑이 적용된 경우에는 대개 `errors.Is`를 사용하여 확인합니다. 센티널 에러는 구조가 단순하고 광범위하며 안정적인 에러 범주를 표현할 때 유용하지만, 외부에 공개(export)된 센티널은 API의 일부가 되어 호출자가 특정 값에 결합될 수 있습니다. 사용자 정의 타입 에러는 구조화된 필드를 가질 수 있으며 `errors.As`를 통해 확인할 수 있습니다. 더 풍부한 도메인 에러 모델은 실패 유형이나 코드로 오류를 분류하고 안전한 메시지나 메타데이터를 포함할 수 있어, 호출자에게 단일 고정 에러 값 이상의 안정적인 처리가 필요할 때 유용합니다.

package users

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("user not found")

type DomainError struct {
	Kind   string
	UserID int64
	Err    error
}

func (e *DomainError) Error() string {
	return fmt.Sprintf("%s user %d: %v", e.Kind, e.UserID, e.Err)
}
func (e *DomainError) Unwrap() error { return e.Err }

func Lookup(id int64) error {
	return &DomainError{Kind: "not_found", UserID: id, Err: ErrNotFound}
}
AI 코치와 함께 이 질문에 답해 보세요

13Go 백엔드에서 내부 오류를 클라이언트에게 유용한 응답으로 변환하면서, 운영자에게도 문제를 진단할 수 있는 시그널을 제공하려면 어떻게 해야 하나요?

Go 백엔드는 애플리케이션 또는 전송 계층의 경계에서 내부 오류를 안정적이고 클라이언트에게 안전한 카테고리 및 응답으로 변환해야 합니다. 가공되지 않은 내부 오류를 그대로 노출하지 않고 안전한 메시지와 기계 판독 가능한 에러 코드를 제공하며, 이를 적절한 HTTP 상태 코드나 이에 상응하는 전송 상태로 매핑해야 합니다. 반면 운영자는 구조화된 로그, 트레이스, 메트릭, 상관관계/요청 ID(correlation/request ID), 보존된 근본 원인을 통해 진단 정보를 확보할 수 있어야 합니다. 장애 누락과 중복 로깅으로 인한 노이즈를 방지하려면, 요청 컨텍스트를 보유한 최외곽 경계에서 한 번만 로깅을 수행하는 것이 가장 좋습니다.

type ErrorCode string

const (
	CodeNotFound ErrorCode = "not_found"
	CodeInvalid  ErrorCode = "invalid_argument"
	CodeInternal ErrorCode = "internal"
)

type AppError struct {
	Code    ErrorCode
	Message string // safe for clients
	Err     error  // internal cause
}

func (e *AppError) Error() string { return string(e.Code) + ": " + e.Err.Error() }
func (e *AppError) Unwrap() error { return e.Err }

func statusFor(code ErrorCode) int {
	switch code {
	case CodeNotFound:
		return 404
	case CodeInvalid:
		return 400
	default:
		return 500
	}
}

// Handler boundary idea:
// - call service
// - classify or convert error to AppError
// - log original error with request_id and safe request context
// - return {"code":"not_found","message":"user not found","request_id":"..."}
AI 코치와 함께 이 질문에 답해 보세요

14Go에서 사용자 정의 에러 타입을 올바르게 구현하는 방법은 무엇이며, errors.Join은 에러 검사와 정리 경로(cleanup path)에서의 에러 처리에 어떤 영향을 미칩니까?

Go의 사용자 정의 에러 타입은 `Error() string`을 구현하여 `error` 인터페이스를 만족합니다. 또한 구조화된 필드를 가질 수 있으며, `Unwrap() error`를 구현하여 근본 원인이 되는 에러를 노출할 수도 있습니다. 포인터 수신자(pointer receiver)와 값 수신자(value receiver)의 선택도 중요합니다. 포인터 수신자를 사용하면 `*T`만 `error`를 만족하는 반면, 값 수신자를 사용하면 보통 `T`와 `*T` 모두 만족하므로, 복사 동작과 호출자가 `errors.As`에 전달해야 하는 타입에 영향을 줍니다. `errors.Join`은 여러 에러를 하나의 에러로 결합하며, `errors.Is`와 `errors.As`는 결합된 하위 에러들을 검사할 수 있습니다. 이는 기본 작업 에러와 지연/정리(deferred/cleanup) 작업 에러 중 어느 쪽도 유실하지 않고 반환해야 할 때 유용합니다.

package main

import (
	"errors"
	"fmt"
)

var ErrWrite = errors.New("write failed")
var ErrClose = errors.New("close failed")

type OpError struct {
	Op  string
	Err error
}

func (e *OpError) Error() string { return e.Op + ": " + e.Err.Error() }
func (e *OpError) Unwrap() error { return e.Err }

func save() (err error) {
	defer func() {
		closeErr := ErrClose
		err = errors.Join(err, closeErr)
	}()
	return &OpError{Op: "write file", Err: ErrWrite}
}

func main() {
	err := save()
	fmt.Println(errors.Is(err, ErrWrite))
	fmt.Println(errors.Is(err, ErrClose))

	var op *OpError
	fmt.Println(errors.As(err, &op), op.Op)
}
AI 코치와 함께 이 질문에 답해 보세요

동시성

15채널에서 `select` 문은 어떻게 동작하며, 준비된 케이스의 선택 방식, `default` 케이스, 취소 가능한 연산은 각각 어떻게 처리되나요?

`select`는 여러 채널 연산을 대기하다가 송신 또는 수신을 진행할 수 있는 케이스 하나를 실행합니다. 준비된 채널 케이스가 없다면 `default` 케이스가 없는 한 블로킹됩니다. `default`는 진행할 수 있는 채널 연산이 없을 때만 즉시 실행되므로, 논블로킹(non-blocking) 방식의 송수신 시도에 유용합니다. 여러 케이스가 동시에 준비된 경우, Go는 소스 코드의 순서가 아니라 의사 난수(pseudo-random) 방식으로 하나를 선택합니다. 취소 가능한 채널 연산은 일반적으로 `ctx.Done()`에서 수신하는 케이스를 추가하여 컨텍스트가 취소되거나 타임아웃되었을 때 goroutine이 대기를 중단할 수 있도록 구현합니다.

func send(ctx context.Context, ch chan<- string, v string) error {
    select {
    case ch <- v:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}
AI 코치와 함께 이 질문에 답해 보세요