Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.
제네릭 타입
- 타입을 파라미터를 이용해서 여러 타입에 동작하는 새로운 타입을 만들 수 있음.
type Node[T any] struct {
val T
next *Node[T]
}
- Node 구조체는 타입 파라미터를 사용해서 val 필드 타입이 어떤 타입이든 가능하도록 정의함.
package main
import "fmt"
type Node[T any] struct {
val T
next *Node[T]
}
func NewNode[T any](v T) *Node[T] { // T타입의 val 필드값을 갖는 객체 생성
return &Node[T]{val: v}
}
func (n *Node[T]) Push(v T) *Node[T] { // Node[T] 타입의 메서드 정의
node := NewNode(v)
n.next = node
return node
}
func main() {
node1 := NewNode(1) // 객체 생성 *Node[int]
node1.Push(2).Push(3).Push(4)
for node1 != nil {
fmt.Print(node1.val, " - ")
node1 = node1.next
}
fmt.Println()
node2 := NewNode("Hi") // 객체 생성 *Node[string]
node2.Push("Hello").Push("How are you")
for node2 != nil {
fmt.Print(node2.val, " - ")
node2 = node2.next
}
fmt.Println()
}
// 결과
1 - 2 - 3 - 4 -
Hi - Hello - How are you -
- 제네릭 함수를 사용해 T 타입의 val 빌드 값을 갖는 Node 객체를 생성함.
- Node[T] 타입의 메서드를 정의하고, 이렇게 제네릭 타입은 타입명 뒤에 [T]와 같이 붙여 메서드를 추가할 수 있음.
tpye Node[T1 any, T2 any] strucy {
val1 T1
val1 T2
next *Node[T]
}
func (n *Node[T1, T2] Push(val1 T1, val2 T2) *Node[T1, T2] { ... }
- 위와 같이 타입 파라미터가 두 개이면 Node[T1, T2]와 같이 필요한 타입 파라미터를 모두 적어서 메서드를 정의함.
- NewNode(1)로 새로운 Node 객체를 생성함. 1은 int 타입이므로 생성된 node1 변수 타입은 *Node[int]가 됨.
따라서 node1 변수의 val 필드 타입은 int가 됨.
- NewNode("Hi")로 새로운 Node 객체를 생성함. "Hi"가 string 타입이므로 이때 생성된 Node2 변수 타입은 *Node[string]이 됨.
따라서 node2 변수의 val 필드 타입은 string이 됨.
※ 제네릭 타입 선언할 때 제네릭 타입 파라미터를 넣을 수 있는데, 메서드에는 타입 파라미터를 넣을 수 없음.
package main
import "fmt"
type Node[T any] struct {
val T
next *Node[T]
}
func (n *Node) Push[T any](a T) {
}
- 메서드로 Push를 사용하여 위 처럼 사용하는 타입 파라미터를 받는 메서드는 사용할 수 없음! (지원하지 않음)
- 함수는 타입 파라미터를 넣을 수 있음.
→ 그러나, 타입 선언할 때는 타입 파라미터를 넣을 수 있음.
성능차이
- 타입 파라미터를 사용할 때와 인터페이스를 사용할 때는 성능 차이가 발생함.
var v1 int = 3
var v2 interface{} = v1 //boxing
var v3 int = v2.(int) //unboxing
- 기본 타입 값을 빈 인터페이스 변수에 대입할 때 Go에서는 빈 인터페이스를 만들어 기본 타입값을 가리키도록 함.
= 박스에 넣는 것과 같다고 하여 이를 박싱(Boxing)이라고 함.
박싱할 때 빈 인터페이스 객체를 사용하게 됨.
- 다시 값을 꺼낼 때는 박스에서 꺼내는 것과 같다고 하여 언박싱(Unboxing)이라고 함.
package main
import "fmt"
func main() {
var v1 int = 3
var v2 interface{} = &v1 // boxing
var v3 int = *(v2.(*int)) // unboxing
fmt.Printf("v1: %x %T\n", &v1, &v1)
fmt.Printf("v2: %x %T\n", &v2, &v2)
fmt.Printf("v3: %x %T\n", &v3, &v3)
}
// 결과
v1: c00009a040 *int
v2: c00009c050 *interface {}
v3: c00009a048 *int
- 예제와 같이 박싱을 한 v2와 v1이 서로 다른 주소값을 가지고 있음 = 서로 다른 객체
✓ 박싱한다는 것은 빈 인터페이스 박스 안에 실젯값인 int 타입 값인 3을 넣는다고 볼 수 있음.
- 값을 감싸는 박스 객체를 만들어야 함.
이 값을 감싸는 빈 인터페이스 박스는 크기가 매우 작기 때문에 성능상 큰 문제가 되지 않지만 많아지면, 박싱/언박싱을 위해
임시로 사용되는 박스 수가 늘어나기 때문에 문제가 될 수 있음.
→ 제네릭 프로그래밍을 사용하면, 타입 파리머터에 의해 타입이 고정되기 때문에 박싱/언박싱이 필요 없어짐.
그렇다고, 제네릭을 사용하는게 무조건적인 이득은 아님!
func add[T constraints.Integer | constraints.Float](a, b T) T {
return a + b
}
add(1, 3)
add(3.14, 1.43)
- 위 예제에서 add(1, 3)과 add(3.14, 1.43)은 하나의 함수를 서로 다른 타입으로 두 번 호출한 것 처럼 보이지만 그렇지 않음.
→ add(1,3)은 add[int](1,3)을 호출한 것이고, add(3.14, 1.43)은 add[float64](3.14, 1.43) 함수를 호출한 것.
- 제네릭 함수나 타입의 경우 하나의 함수나 타입처럼 보이지만, 실제 컴파일 타임에 사용한 타입 파라미터별로 새로운 함수나 타입을 생성해서 사용하게 됨.\
→ 제네릭 프로그래밍을 많이 사용하면, 컴파일 타임에 생성해야 할 함수와 타입 개수가 늘어나고 컴파일 시간도 더 걸림.
또한, 코드양이 증가되어 실행 파일 크기가 늘어나게 됨.
실행 파일 크기는 일반적인 프로그램에서는 문제가 되지 않지만 용량의 제한이 있는 임베디드 프로그램에서는 문제가 될 수 있음.
'Language > Golang' 카테고리의 다른 글
Golang (Go언어) Go로 만드는 웹 (2) (2) | 2025.01.03 |
---|---|
Golang (Go언어) Go로 만드는 웹 (1) (0) | 2025.01.02 |
Golang (Go언어) 제네릭 프로그래밍(Generic Programming) 2/3 (0) | 2025.01.01 |
Golang (Go언어) 제네릭 프로그래밍(Generic Programming) 1/3 (1) | 2024.12.27 |
Golang (Go언어) 채널과 컨텍스트(Channel & Context) (0) | 2024.12.11 |