제네릭이 도입되기 전에는 다양한 데이터 유형을 지원하는 제네릭 함수를 구현하는 여러 가지 접근 방식이 있었습니다.
접근법 1: 데이터 유형별 함수 구현
이 접근 방식은 코드가 극도로 중복되고 유지 관리 비용이 높아집니다. 수정하려면 모든 기능에서 동일한 작업을 수행해야 합니다. 게다가 Go 언어는 동일한 이름의 함수 오버로딩을 지원하지 않기 때문에 이러한 함수를 외부 모듈 호출에 노출시키는 것도 불편합니다.
접근법 2: 범위가 가장 큰 데이터 유형 사용
코드 중복을 피하기 위해 또 다른 방법은 가장 큰 범위의 데이터 유형을 사용하는 것입니다(예: 접근법 2). 일반적인 예는 두 숫자 중 더 큰 숫자를 반환하는 math.Max입니다. 다양한 데이터 유형의 데이터를 비교할 수 있도록 math.Max는 Go에서 숫자 유형 중 범위가 가장 넓은 데이터 유형인 float64를 입력 및 출력 매개변수로 사용하여 정밀도 손실을 방지합니다. 이렇게 하면 코드 중복 문제가 어느 정도 해결되지만 모든 유형의 데이터는 먼저 float64 유형으로 변환되어야 합니다. 예를 들어 int와 int를 비교할 때 여전히 타입 캐스팅이 필요하므로 성능이 저하될 뿐만 아니라 부자연스러워 보입니다.
접근 방법 3: 인터페이스{} 유형 사용
인터페이스{} 유형을 사용하면 위의 문제가 효과적으로 해결됩니다. 그러나 인터페이스{} 유형은 런타임에 유형 어설션이나 유형 판단이 필요하기 때문에 특정 런타임 오버헤드를 도입하며, 이로 인해 성능이 저하될 수 있습니다. 또한 인터페이스{} 유형을 사용할 때 컴파일러는 정적 유형 검사를 수행할 수 없으므로 일부 유형 오류는 런타임에만 발견될 수 있습니다.
Go 1.18에는 제네릭 지원이 도입되었는데, 이는 Go 언어 오픈 소스 이후로 큰 변화입니다.
제네릭은 프로그래밍 언어의 기능입니다. 이를 통해 프로그래머는 프로그래밍에서 실제 유형 대신 일반 유형을 사용할 수 있습니다. 그런 다음 실제 호출 중에 명시적 전달 또는 자동 추론을 통해 제네릭 유형을 대체하여 코드 재사용 목적을 달성합니다. 제네릭을 사용하는 과정에서 연산할 데이터 타입을 파라미터로 지정한다. 이러한 매개변수 유형을 각각 클래스, 인터페이스, 메소드에서 제네릭 클래스, 제네릭 인터페이스, 제네릭 메소드라고 합니다.
제네릭의 주요 장점은 코드 재사용성과 유형 안전성을 향상시키는 것입니다. 기존 형식 매개변수와 비교하여 제네릭은 범용 코드 작성을 더욱 간결하고 유연하게 만들어 다양한 유형의 데이터를 처리할 수 있는 기능을 제공하고 Go 언어의 표현력과 재사용성을 더욱 향상시킵니다. 동시에 특정 유형의 제네릭은 컴파일 타임에 결정되므로 유형 검사를 제공하여 유형 변환 오류를 방지할 수 있습니다.
Go 언어에서 인터페이스{}와 제네릭은 모두 여러 데이터 유형을 처리하기 위한 도구입니다. 차이점을 논의하기 위해 먼저 인터페이스{}와 제네릭의 구현 원칙을 살펴보겠습니다.
인터페이스{}는 인터페이스 유형에 메서드가 없는 빈 인터페이스입니다. 모든 유형은 인터페이스{}를 구현하므로 모든 유형을 수용할 수 있는 함수, 메서드 또는 데이터 구조를 생성하는 데 사용할 수 있습니다. 런타임 시 인터페이스{}의 기본 구조는 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") }
인터페이스{}가 런타임 시 박싱 및 언박싱 작업을 통해 여러 데이터 유형에 대한 작업을 지원하는 것을 볼 수 있습니다.
Go 핵심 팀은 Go 제네릭의 구현 계획을 평가할 때 매우 신중했습니다. 총 3가지 구현 계획이 제출되었습니다.
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") }
인터페이스{}와 제네릭의 기본 구현 원칙에서 두 가지의 주요 차이점은 인터페이스{}가 런타임 중에 다양한 데이터 유형 처리를 지원하는 반면, 제네릭은 컴파일 단계에서 정적으로 다양한 데이터 유형 처리를 지원한다는 점을 알 수 있습니다. 실제 사용에는 주로 다음과 같은 차이점이 있습니다.
(1) 성능 차이: 다양한 유형의 데이터가 인터페이스에 할당되거나 인터페이스에서 검색될 때 수행되는 박싱 및 언박싱 작업은 비용이 많이 들고 추가 오버헤드가 발생합니다. 대조적으로, 제네릭에는 박싱 및 언박싱 작업이 필요하지 않으며 제네릭에 의해 생성된 코드는 특정 유형에 최적화되어 런타임 성능 오버헤드를 방지합니다.
(2) 유형 안전성: 인터페이스{} 유형을 사용할 때 컴파일러는 정적 유형 검사를 수행할 수 없으며 런타임 시 유형 어설션만 수행할 수 있습니다. 따라서 일부 유형 오류는 런타임에만 발견될 수 있습니다. 반면 Go의 일반 코드는 컴파일 타임에 생성되므로 일반 코드는 컴파일 타임에 유형 정보를 얻을 수 있어 유형 안전성이 보장됩니다.
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 }
기본 요소의 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 }
Union 유형 요소는 유형 매개변수가 될 수 없으며, 비인터페이스 요소는 쌍으로 분리되어야 합니다. 요소가 두 개 이상인 경우 비어 있지 않은 메소드가 있는 인터페이스 유형을 포함할 수 없으며 비교 가능하거나 비교 가능 항목을 포함할 수 없습니다.
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
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{}
제네릭을 효과적으로 활용하려면 사용 시 다음 사항에 유의해야 합니다.
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 }
전체적으로 제네릭의 장점은 세 가지 측면으로 요약할 수 있습니다.
마지막으로 Go 서비스 배포에 가장 적합한 플랫폼인 Leapcell을 소개하겠습니다.
문서에서 더 자세히 알아보세요!
리프셀 트위터: https://x.com/LeapcellHQ
위 내용은 Go Generics: 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!