> 백엔드 개발 > Golang > Go Generics: 심층 분석

Go Generics: 심층 분석

Mary-Kate Olsen
풀어 주다: 2025-01-01 01:51:09
원래의
978명이 탐색했습니다.

Go Generics: A Deep Dive

1. 제네릭 없이 사용하기

제네릭이 도입되기 전에는 다양한 데이터 유형을 지원하는 제네릭 함수를 구현하는 여러 가지 접근 방식이 있었습니다.

접근법 1: 데이터 유형별 함수 구현
이 접근 방식은 코드가 극도로 중복되고 유지 관리 비용이 높아집니다. 수정하려면 모든 기능에서 동일한 작업을 수행해야 합니다. 게다가 Go 언어는 동일한 이름의 함수 오버로딩을 지원하지 않기 때문에 이러한 함수를 외부 모듈 호출에 노출시키는 것도 불편합니다.

접근법 2: 범위가 가장 큰 데이터 유형 사용
코드 중복을 피하기 위해 또 다른 방법은 가장 큰 범위의 데이터 유형을 사용하는 것입니다(예: 접근법 2). 일반적인 예는 두 숫자 중 더 큰 숫자를 반환하는 math.Max입니다. 다양한 데이터 유형의 데이터를 비교할 수 있도록 math.Max는 Go에서 숫자 유형 중 범위가 가장 넓은 데이터 유형인 float64를 입력 및 출력 매개변수로 사용하여 정밀도 손실을 방지합니다. 이렇게 하면 코드 중복 문제가 어느 정도 해결되지만 모든 유형의 데이터는 먼저 float64 유형으로 변환되어야 합니다. 예를 들어 int와 int를 비교할 때 여전히 타입 캐스팅이 필요하므로 성능이 저하될 뿐만 아니라 부자연스러워 보입니다.

접근 방법 3: 인터페이스{} 유형 사용
인터페이스{} 유형을 사용하면 위의 문제가 효과적으로 해결됩니다. 그러나 인터페이스{} 유형은 런타임에 유형 어설션이나 유형 판단이 필요하기 때문에 특정 런타임 오버헤드를 도입하며, 이로 인해 성능이 저하될 수 있습니다. 또한 인터페이스{} 유형을 사용할 때 컴파일러는 정적 유형 검사를 수행할 수 없으므로 일부 유형 오류는 런타임에만 발견될 수 있습니다.

2. 제네릭의 장점

Go 1.18에는 제네릭 지원이 도입되었는데, 이는 Go 언어 오픈 소스 이후로 큰 변화입니다.
제네릭은 프로그래밍 언어의 기능입니다. 이를 통해 프로그래머는 프로그래밍에서 실제 유형 대신 일반 유형을 사용할 수 있습니다. 그런 다음 실제 호출 중에 명시적 전달 또는 자동 추론을 통해 제네릭 유형을 대체하여 코드 재사용 목적을 달성합니다. 제네릭을 사용하는 과정에서 연산할 데이터 타입을 파라미터로 지정한다. 이러한 매개변수 유형을 각각 클래스, 인터페이스, 메소드에서 제네릭 클래스, 제네릭 인터페이스, 제네릭 메소드라고 합니다.
제네릭의 주요 장점은 코드 재사용성과 유형 안전성을 향상시키는 것입니다. 기존 형식 매개변수와 비교하여 제네릭은 범용 코드 작성을 더욱 간결하고 유연하게 만들어 다양한 유형의 데이터를 처리할 수 있는 기능을 제공하고 Go 언어의 표현력과 재사용성을 더욱 향상시킵니다. 동시에 특정 유형의 제네릭은 컴파일 타임에 결정되므로 유형 검사를 제공하여 유형 변환 오류를 방지할 수 있습니다.

3. 제네릭과 인터페이스의 차이점{}

Go 언어에서 인터페이스{}와 제네릭은 모두 여러 데이터 유형을 처리하기 위한 도구입니다. 차이점을 논의하기 위해 먼저 인터페이스{}와 제네릭의 구현 원칙을 살펴보겠습니다.

3.1 인터페이스{} 구현 원칙

인터페이스{}는 인터페이스 유형에 메서드가 없는 빈 인터페이스입니다. 모든 유형은 인터페이스{}를 구현하므로 모든 유형을 수용할 수 있는 함수, 메서드 또는 데이터 구조를 생성하는 데 사용할 수 있습니다. 런타임 시 인터페이스{}의 기본 구조는 eface로 표시됩니다. 그 구조는 아래에 표시되어 있으며 주로 _type과 data라는 두 개의 필드를 포함합니다.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

_type은 실제 값의 크기, 종류, 해시 함수, 문자열 표현과 같은 정보가 포함된 _type 구조에 대한 포인터입니다. data는 실제 데이터에 대한 포인터입니다. 실제 데이터의 크기가 포인터의 크기보다 작거나 같으면 데이터는 데이터 필드에 직접 저장됩니다. 그렇지 않으면 데이터 필드에 실제 데이터에 대한 포인터가 저장됩니다.
특정 유형의 객체가 인터페이스 유형의 변수에 할당되면 Go 언어는 암시적으로 eface의 박싱 작업을 수행하여 _type 필드를 값 유형으로 설정하고 데이터 필드를 값 데이터로 설정합니다. . 예를 들어, var i 인터페이스{} = 123 문이 실행되면 Go는 eface 구조를 생성합니다. 여기서 _type 필드는 int 유형을 나타내고 데이터 필드는 값 123을 나타냅니다.
인터페이스{}에서 저장된 값을 검색할 때 언박싱 프로세스, 즉 유형 어설션 또는 유형 판단이 발생합니다. 이 프로세스에서는 예상 유형을 명시적으로 지정해야 합니다. 인터페이스{}에 저장된 값의 유형이 예상 유형과 일치하면 유형 어설션이 성공하고 값을 검색할 수 있습니다. 그렇지 않으면 유형 어설션이 실패하며 이 상황에 대한 추가 처리가 필요합니다.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
로그인 후 복사
로그인 후 복사
로그인 후 복사

인터페이스{}가 런타임 시 박싱 및 언박싱 작업을 통해 여러 데이터 유형에 대한 작업을 지원하는 것을 볼 수 있습니다.

3.2 제네릭 구현 원리

Go 핵심 팀은 Go 제네릭의 구현 계획을 평가할 때 매우 신중했습니다. 총 3가지 구현 계획이 제출되었습니다.

  • 스텐실 계획
  • 사전 체계
  • GC 모양 스텐실 구성표

Stenciling 방식은 C, Rust 등의 언어에서 제네릭 구현을 위해 채택한 구현 방식이기도 합니다. 구현 원칙은 컴파일 기간 동안 일반 함수가 호출될 때 특정 유형 매개변수 또는 제약 조건의 유형 요소에 따라 각 유형 인수에 대해 일반 함수의 별도 구현이 생성되어 유형 안전성과 최적의 성능을 보장한다는 것입니다. . 그러나 이 방법을 사용하면 컴파일러 속도가 느려집니다. 호출되는 데이터 유형이 많을 때 일반 함수는 각 데이터 유형에 대해 독립적인 함수를 생성해야 하므로 컴파일된 파일이 매우 커질 수 있습니다. 동시에 CPU 캐시 미스, 명령 분기 예측 등의 문제로 인해 생성된 코드가 효율적으로 실행되지 않을 수 있습니다.

사전 구성표는 일반 함수에 대해 하나의 함수 논리만 생성하지만 dict 매개변수를 함수의 첫 번째 매개변수로 추가합니다. dict 매개변수는 제네릭 함수 호출 시 타입 인수의 타입 관련 정보를 저장하고, 함수 호출 시 AX 레지스터(AMD)를 이용하여 사전 정보를 전달한다. 이 방식의 장점은 컴파일 단계 오버헤드를 줄이고 바이너리 파일의 크기를 늘리지 않는다는 것입니다. 하지만 런타임 오버헤드가 증가하고, 컴파일 단계에서 함수 최적화를 수행할 수 없으며, 사전 재귀 등의 문제가 있습니다.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

Go는 마침내 위의 두 가지 방식을 통합하고 일반 구현을 위한 GC Shape Stenciling 방식을 제안했습니다. Type의 GC Shape 단위로 함수코드를 생성합니다. 동일한 GC Shape를 가진 유형은 동일한 코드를 재사용합니다. 유형의 GC Shape는 Go 메모리 할당자/가비지 수집기의 표현을 참조합니다. 모든 포인터 유형은 *uint8 유형을 재사용합니다. 동일한 GC Shape를 갖는 유형의 경우 공유된 인스턴스화된 함수 코드가 사용됩니다. 또한 이 체계는 동일한 GC 형태를 가진 다양한 유형을 구별하기 위해 인스턴스화된 각 함수 코드에 dict 매개변수를 자동으로 추가합니다.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
로그인 후 복사
로그인 후 복사
로그인 후 복사

3.3 차이점

인터페이스{}와 제네릭의 기본 구현 원칙에서 두 가지의 주요 차이점은 인터페이스{}가 런타임 중에 다양한 데이터 유형 처리를 지원하는 반면, 제네릭은 컴파일 단계에서 정적으로 다양한 데이터 유형 처리를 지원한다는 점을 알 수 있습니다. 실제 사용에는 주로 다음과 같은 차이점이 있습니다.
(1) 성능 차이: 다양한 유형의 데이터가 인터페이스에 할당되거나 인터페이스에서 검색될 때 수행되는 박싱 및 언박싱 작업은 비용이 많이 들고 추가 오버헤드가 발생합니다. 대조적으로, 제네릭에는 박싱 및 언박싱 작업이 필요하지 않으며 제네릭에 의해 생성된 코드는 특정 유형에 최적화되어 런타임 성능 오버헤드를 방지합니다.
(2) 유형 안전성: 인터페이스{} 유형을 사용할 때 컴파일러는 정적 유형 검사를 수행할 수 없으며 런타임 시 유형 어설션만 수행할 수 있습니다. 따라서 일부 유형 오류는 런타임에만 발견될 수 있습니다. 반면 Go의 일반 코드는 컴파일 타임에 생성되므로 일반 코드는 컴파일 타임에 유형 정보를 얻을 수 있어 유형 안전성이 보장됩니다.

4. 제네릭 시나리오

4.1 적용 가능한 시나리오

  • 일반 데이터 구조를 구현할 때: 제네릭을 사용하면 코드를 한 번 작성하고 다른 데이터 유형에 재사용할 수 있습니다. 이를 통해 코드 중복이 줄어들고 코드 유지 관리성과 확장성이 향상됩니다.
  • Go에서 기본 컨테이너 유형을 작업하는 경우: 함수가 슬라이스, 맵, 채널 등 Go 내장 컨테이너 유형의 매개변수를 사용하고 함수 코드가 컨테이너의 요소 유형에 대해 특정 가정을 하지 않는 경우 , 제네릭을 사용하면 컨테이너 알고리즘을 컨테이너의 요소 유형에서 완전히 분리할 수 있습니다. 제네릭 구문을 사용할 수 있기 전에는 일반적으로 리플렉션을 구현에 사용했지만 리플렉션으로 인해 코드 가독성이 떨어지고 정적 유형 검사를 수행할 수 없으며 프로그램의 런타임 오버헤드가 크게 증가합니다.
  • 다른 데이터 유형에 대해 구현된 메소드의 논리가 동일한 경우: 서로 다른 데이터 유형의 메소드가 동일한 함수 논리를 갖고 유일한 차이점이 입력 매개변수의 데이터 유형인 경우 제네릭을 사용하여 코드 중복을 줄일 수 있습니다.

4.2 해당되지 않는 시나리오

  • 인터페이스 유형을 유형 매개변수로 바꾸지 마세요. 인터페이스는 특정한 일반 프로그래밍 감각을 지원합니다. 특정 유형의 변수에 대한 작업이 해당 유형의 메서드만 호출하는 경우 제네릭을 사용하지 않고 인터페이스 유형을 직접 사용하면 됩니다. 예를 들어, io.Reader는 인터페이스를 사용하여 파일 및 난수 생성기에서 다양한 유형의 데이터를 읽습니다. io.Reader는 코드 관점에서 읽기 쉽고 효율성이 높으며, 함수 실행 효율성의 차이가 거의 없어 타입 매개변수를 사용할 필요가 없습니다.
  • 데이터 유형별로 메소드 구현 내용이 다른 경우: 유형별로 메소드 구현이 다른 경우 제네릭 대신 인터페이스 유형을 사용해야 합니다.
  • 런타임 역학이 강한 시나리오: 예를 들어 스위치를 사용하여 유형 판단을 수행하는 시나리오에서는 인터페이스{}를 직접 사용하는 것이 더 나은 결과를 얻을 수 있습니다.

5. 제네릭의 함정

5.1 없음 비교

Go 언어에서는 유형 매개변수가 컴파일 타임에 유형 검사를 받는 반면 nil은 런타임에 특별한 값이기 때문에 유형 매개변수를 nil과 직접 비교할 수 없습니다. 컴파일 타임에는 유형 매개변수의 기본 유형을 알 수 없으므로 컴파일러는 유형 매개변수의 기본 유형이 nil과의 비교를 지원하는지 여부를 확인할 수 없습니다. 따라서 유형 안전성을 유지하고 잠재적인 런타임 오류를 방지하기 위해 Go 언어에서는 유형 매개변수와 nil 간의 직접 비교를 허용하지 않습니다.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

5.2 유효하지 않은 기본 요소

기본 요소의 T 유형은 기본 유형이어야 하며 인터페이스 유형일 수 없습니다.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

5.3 잘못된 Union 유형 요소

Union 유형 요소는 유형 매개변수가 될 수 없으며, 비인터페이스 요소는 쌍으로 분리되어야 합니다. 요소가 두 개 이상인 경우 비어 있지 않은 메소드가 있는 인터페이스 유형을 포함할 수 없으며 비교 가능하거나 비교 가능 항목을 포함할 수 없습니다.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
로그인 후 복사
로그인 후 복사
로그인 후 복사

5.4 인터페이스 유형은 재귀적으로 포함될 수 없습니다.

type Op interface{
       int|float 
}
func Add[T Op](m, n T) T { 
       return m + n
} 
// After generation =>
const dict = map[type] typeInfo{
       int : intInfo{
             newFunc,
             lessFucn,
             //......
        },
        float : floatInfo
} 
func Add(dict[T], m, n T) T{}
로그인 후 복사

6. 모범 사례

제네릭을 효과적으로 활용하려면 사용 시 다음 사항에 유의해야 합니다.

  1. 지나치게 일반화하지 마세요. 제네릭은 모든 시나리오에 적합한 것은 아니며 어떤 시나리오에 적합한지 신중하게 고려해야 합니다. 적절한 경우 리플렉션을 사용할 수 있습니다. Go에는 런타임 리플렉션이 있습니다. 리플렉션 메커니즘은 특정한 일반 프로그래밍 감각을 지원합니다. 특정 작업이 다음 시나리오를 지원해야 하는 경우 반영을 고려할 수 있습니다. (1) 인터페이스 유형을 적용할 수 없는 메소드 없는 유형에서 작동합니다. (2) 타입별 연산 로직이 다른 경우 제네릭은 적용되지 않습니다. 예를 들어 인코딩/json 패키지의 구현이 있습니다. 인코딩할 각 유형이 MarshalJson 메서드를 구현하는 것은 바람직하지 않으므로 인터페이스 유형을 사용할 수 없습니다. 그리고 유형별로 인코딩 논리가 다르기 때문에 제네릭을 사용하면 안 됩니다.
  2. T가 포인터 유형, 슬라이스 또는 맵을 나타내도록 하는 대신 *T, []T 및 map[T1]T2를 분명히 사용하세요. C의 유형 매개변수는 자리 표시자이고 실제 유형으로 대체된다는 사실과 달리 Go의 유형 매개변수 T의 유형은 유형 매개변수 자체입니다. 따라서 포인터, 슬라이스, 맵 등의 데이터 유형으로 표현하면 사용 중 아래와 같이 예상치 못한 상황이 많이 발생하게 됩니다.
type V interface{
        int|float|*int|*float
} 
func F[T V](m, n T) {}
// 1. Generate templates for regular types int/float
func F[go.shape.int_0](m, n int){} 
func F[go.shape.float_0](m, n int){}
// 2. Pointer types reuse the same template
func F[go.shape.*uint8_0](m, n int){}
// 3. Add dictionary passing during the call
const dict = map[type] typeInfo{
        int : intInfo{},
        float : floatInfo{}
} 
func F[go.shape.int_0](dict[int],m, n int){}
로그인 후 복사

위 코드는 오류를 보고합니다. 잘못된 작업: ptr(*int | *uint로 제한되는 T 유형 변수)의 포인터는 동일한 기본 유형을 가져야 합니다. 이 오류가 발생하는 이유는 T가 유형 매개변수이고 유형 매개변수가 포인터가 아니며 역참조 작업을 지원하지 않기 때문입니다. 이 문제는 정의를 다음과 같이 변경하여 해결할 수 있습니다.

// Wrong example
func ZeroValue0[T any](v T) bool {
    return v == nil  
}
// Correct example 1
func Zero1[T any]() T {
    return *new(T) 
}
// Correct example 2
func Zero2[T any]() T {
    var t T
    return t 
}
// Correct example 3
func Zero3[T any]() (t T) {
    return 
}
로그인 후 복사

요약

전체적으로 제네릭의 장점은 세 가지 측면으로 요약할 수 있습니다.

  1. 유형은 컴파일 기간 동안 결정되어 유형 안전성을 보장합니다. 넣은 것이 곧 꺼지는 것입니다.
  2. 가독성이 향상되었습니다. 실제 데이터 유형은 코딩 단계에서 명시적으로 알려져 있습니다.
  3. 제네릭은 동일한 유형의 처리 코드를 병합하여 코드 재사용률을 높이고 프로그램의 전반적인 유연성을 높입니다. 그러나 일반 데이터 유형에는 제네릭이 반드시 필요한 것은 아닙니다. 실제 사용 상황에 따라 제네릭 사용 여부를 신중하게 고려해야 합니다.

Leapcell: Go 웹 호스팅, 비동기 작업 및 Redis를 위한 고급 플랫폼

Go Generics: A Deep Dive

마지막으로 Go 서비스 배포에 가장 적합한 플랫폼인 Leapcell을 소개하겠습니다.

1. 다국어 지원

  • JavaScript, Python, Go 또는 Rust를 사용하여 개발하세요.

2. 무료로 무제한 프로젝트 배포

  • 사용한 만큼만 지불하세요. 요청이나 요금이 부과되지 않습니다.

3. 탁월한 비용 효율성

  • 유휴 비용 없이 사용한 만큼만 지불하세요.
  • 예: $25는 평균 응답 시간 60ms에서 694만 개의 요청을 지원합니다.

4. 간소화된 개발자 경험

  • 손쉬운 설정을 위한 직관적인 UI.
  • 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
  • 실행 가능한 통찰력을 위한 실시간 지표 및 로깅.

5. 손쉬운 확장성과 고성능

  • 자동 확장을 통해 높은 동시성을 쉽게 처리할 수 있습니다.
  • 운영 오버헤드가 전혀 없습니다. 구축에만 집중하세요.

문서에서 더 자세히 알아보세요!

리프셀 트위터: https://x.com/LeapcellHQ

위 내용은 Go Generics: 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿