C++의 template 과 같이 많은 언어들이 지원하는
generic
이 Go version 1.18 부터 지원하기 시작했다.
예를 들어 보자.
기존에 int형과 float형의 합을 각각 구하는 함수를 만들려면 두개의 함수를 만들어야 했다.
func SumInt(a, b int) int {
return a, b
}
func SumFloat64(a, b float64) float64 {
return a, b
}
Go 1.18 버전부터 지원하는 제네릭(generic)을 사용하면 하나의 함수로 둘 다 이용할 수 있다.
func SumIntOrFloat[T float64 | int] (a, b T) T {
return a+b
}
float64 | int
이 부분을타입 제한
을 통해 더 깔끔하게 만들 수 있다.
type Number interface {
float64 | int
}
func SumIntOrFloat[T Number] (a, b T) T {
return a+b
}
1.18 배포와 함께 추가된 golang.org/x/exp/constraints 에서 타입 제한자를 제공하고 있다. 함께 사용하면 유용해 보인다.
slice, map, channel과 같이 언어에서 정의된 컨테이너 타입을 사용할때 유용하다.
// MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
s := make([]Key, 0, len(m))
for k := range m {
s = append(s, k)
}
return s
}
// Tree is a binary tree.
type Tree[T any] struct {
cmp func(T, T) int
root *node[T]
}
// A node in a Tree.
type node[T any] struct {
left, right *node[T]
val T
}
// find returns a pointer to the node containing val,
// or, if val is not present, a pointer to where it
// would be placed if added.
func (bt *Tree[T]) find(val T) **node[T] {
pl := &bt.root
for *pl != nil {
switch cmp := bt.cmp(val, (*pl).val); {
case cmp < 0:
pl = &(*pl).left
case cmp > 0:
pl = &(*pl).right
default:
return pl
}
}
return pl
}
// Insert inserts val into bt if not already there,
// and reports whether it was inserted.
func (bt *Tree[T]) Insert(val T) bool {
pl := bt.find(val)
if *pl != nil {
return false
}
*pl = &node[T]{val: val}
return true
}
트리의 각 노드에는 타입 파라미터 T의 값이 포함된다. 트리가 특정 타입으로 인스턴스화되면 해당 타입의 값이 노드에 직접 저장된다. 인터페이스 유형으로 저장되지 않는다.
메소드의 코드를 포함하여, 트리 자료 구조가 타입 T에 매우 독립적이기 때문에 타입 parameter를 합리적으로 사용한다.
트리 자료 구조는 타입 T의 값을 비교하는 방법을 알아야 한다. 이를 위해 전달된 비교 함수를 사용한다. bt.cmp 호출하는 find 메소드의 네 번째 라인에서 확인할 수 있다. 그 외에는 type 파라미터가 전혀 중요하지 않다.
만약 비교하는 기능을 만들 때, 함수를 메소드보다 더 선호한다.
다시 말해서, 메소드를 타입에 추가하는 것보다 메소드를 함수로 바꾸는 것이 훨씬 간단 하다. 따라서 범용 데이터 유형의 경우 메서드가 필요한 제약 조건을 작성하는 것보다 함수를 선호한다.
다른 타입이 공통된 메소드의 실행이 필요할 때 유용하다.
// SliceFn implements sort.Interface for a slice of T.
type SliceFn[T any] struct {
s []T
less func(T, T) bool
}
func (s SliceFn[T]) Len() int {
return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) {
s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool {
return s.less(s.s[i], s.s[j])
}
제네릭을 사용하면 분명 여러가지 이점들이 많다.
하지만 무분별하게 사용하면 코드의 가독성이 떨어지게 된다.
Go
의 철학은 단순함과 실용성이다. 그렇기 때문에 generic을 그동안 지원하지 않았고, 개인적으로도 필요성을 크게 느끼지 못했다.
기존에 C++를 메인 언어로 사용했던 입장에서 template 과 같은 제네릭을 사용하면 컴파일러에서 해당 코드를 만들고 하는 과정에서 시간이 길어질 수 밖에 없다. 그렇기에 인터프리터언어로 느껴질 만큼 컴파일이 빠른 장점이 사라지는게 아닌가 싶었지만, 다행히 Go는 여전히 컴파일이 빠르다.
그렇기에 필요한 적재적소에 제네릭을 사용하면 좋을 것 같다.
Gopher로써 Go의 발전 방향이 기대된다.
https://go.dev/doc/go1.18
https://go.dev/blog/intro-generics
https://go.dev/doc/tutorial/generics
https://pkg.go.dev/golang.org/x/exp/constraints
https://go.dev/blog/when-generics