Language/Golang

Golang (Go언어) 제네릭 프로그래밍(Generic Programming) 1/3

HeeWorld 2024. 12. 27. 21:13

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

Golang 마스코트 Gopher(고퍼)

 

제네릭 프로그래밍(Generic Programming)

- Go 1.18 버전에 추가된 기능으로 타입 파라미터를 통해 하나의 함수나 타입이 여러 타입에 대응해 동작하도록 하는 기법

- 자바, C#, C++ 같은 언어에서는 이미 제공되던 기능임.

 

package main

import "fmt"

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func main() {
	var a int = 10
	var b int = 20
	fmt.Println(min(a, b))

	var c int16 = 10
	var d int16 = 20
	fmt.Println(min(c, d))
}

// 결과

./prog.go:19:18: cannot use c (variable of type int16) as int value in argument to min
./prog.go:19:21: cannot use d (variable of type int16) as int value in argument to min

 

→ a,b는 출력이 가능하나 c,d는 아래 결과로 보면 int16 타입을 int로 변환할 수 없어서 사용할 수 없다고 나옴.

- min은 int 타입인데, c,d는 int16 타입으로 서로 타입이 다르며, Go는 이를 변환시켜주지 않음(최강 타입 언어).

   = 변환하고 싶다면 Println부분에서 int를 사용하여 변환해주면 됨.

 

	var e float32 = 3.14
	var f float32 = 3.98
	fmt.Println(min(int(e), int(f)))

 

→ float32 실수의 경우 int로 변환하면 소수점 아래로 탈락하여 e도 3, f도 3이되어 비교가 불가능함.

- 이런 경우에는 실수 타입을 받을 수 있는 Func을 다시 정의 해야함.

package main

import "fmt"

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// float32 타입 재정의

func minF32(a, b float32) float32 {
	if a < b {
		return a
	}
	return b
}

func main() {
	var a int = 10
	var b int = 20
	fmt.Println(min(a, b))

	var c int16 = 10
	var d int16 = 20
	fmt.Println(min(int(c), int(d)))

	var e float32 = 3.14
	var f float32 = 3.98
	fmt.Println(minF32(e, f))
}

// 결과

10
10
3.14

 

✓ 이런 경우 타입 별로 func을 만들어줘야 하는 문제가 있어 다른 언어에서는 제네릭을 통해 해결하고 있었음.

    1.18 버전에서 제네릭 버전이 추가되어 이를 해결할 수 있게 됨.

 

 

제네릭 함수

func funcName[T constaint](p T) {

 

- funcName: func 함수 키워드를 적고, 그 뒤 함수명 적음(함수 선언)

- [T constaint]: 대괄호를 열고 타입 파라미터를 적음, 파라미터 이름과 타입 제한을 적음.

   > T가 파라미터 이름이고, constaint가 타입 제한

   > 타입 파라미터는 필요에 따라 컴마(,) 기호로 구분하여 여러 개를 적을 수 있음.

   > 소괄호를 열어 일반 함수처럼 입력과 출력을 씀, 타입 파라미터에 사용한 타입 파라미터 이름을 특정 타입대신 사용할 수 있음.

 

func Print[T any](a, b T) {
	fmt.Println(a, b)
}

 

- Print() 함수는 제네릭 함수로 하나의 타입 파라미터를 가지고 있고, [T any]라고 정의함.

- T는 타입 파라미터 이름이고(일반적으로 대문자 T를 사용함), any는 타입 제한으로 모든 타입이 가능하다는 의미.

- 입력 인수를 받아, 모두 T 타입으로 정의 되어있으며, T 타입은 앞에서 정의한 타입 파라미터이고, 모든 타입이 가능하다는 뜻.

package main

import "fmt"

func print[T any](a T) {
	fmt.Println(a)
}

func main() {
	var a int = 10
	print(a)
	var b float32 = 3.14
	print(b)
	var c string = "Hello"
	print(c)
}

// 결과
10
3.14
Hello

 

 

타입 제한

func add[T any](a, b T) T {
	return a + b
}

 

- 위 함수의 경우 add() 함수는 T타입 파라미터가 정의되어 있고, T 타입 제한은 any임.

- 따라서, a, b 두 개의 인수는 모든 타입을 할 수 있지만 빌드 에러가 발생함. 

 

→ 모든 타입에서 연산자를 제공하는 것이 아니기 때문에 해당 연산자를 사용할 수 없는 것.

→ 특정 조건을 정의하여 타입에 연산자를 지원하고 있음을 알려줘야 함.

 

func add[T int8 | int16 | int32 | int64 | int](a, b T) T {
	return a + b
}

 

- 제한자에 연산자를 사용할 수 있는 타입을 기재하면 됨.

   ex) int8, int16, int32, int ... 등

- T에는 해당 타입들 중에 하나가 올 수 있고, 가능함을 보여줘야 함.

 

매번 조건을 길게 적는 것은 어려움이 있으니, 타입 제한만 따로 정의할 수 있음.

// 타입 제한 선언
type Integer interface {
	int8 | int16 | int32 | int54 | int
}

func add[T Integer](a, b, T) T {
	return a + b
}

 

- Integer라는 이름으로 int 타입들을 포함한 타입 제한을 정의하고 add() 제네릭 함수에서 Integer 타입 제한을 사용하고 있음.

- 타입 제한을 정의할 때 interface 키워드를 사용하는데, 이는 정확하게는 인터페이스 처럼 동작하는 것임.

 

✓ 매번 타입 제한 선언하는 것도 귀찮(?)아서 그런가, 1.18 버전에 이미 정의된 몇 가지 타입 제한을 제공하고 있음.

> https://tip.golang.org/doc/go1.18

 

- golang.org/x/exp/constraints 패키지를 보면 6개의 제약 조건이 정의되어있는 것을 확인할 수 있음.

   (https://pkg.go.dev/golang.org/x/exp/constraints)

golang.org/x/exp/constraints 패키지

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func print[T constraints.Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

func main() {
	var a int = 10
	var b int = 20
	fmt.Println(min(a, b))

	var c int16 = 10
	var d int16 = 20
	fmt.Println(min(c, d))

	var e float32 = 3.14
	var f float32 = 3.98
	fmt.Println(min(e, f))
}


// 결과

10
10
3.14

 

- constraints.Ordered에 < <= >= > 비교 연산자 사용이 가능하여 위와 같은 결과를 얻을 수 있음.

 

* Ordered is a constraint that permits any ordered type: any type that supports the operators < <= >= >. *

 

 

타입 제한 더 알아보기

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

 

- 타입 앞에 틸더(~,tilde)가 붙어있는데, 의미는 해당 타입을 기본으로 하는 모든 별칭 타입들까지 포함한다는 의미.

package main

import (
	"fmt"
)

type Integer interface {
	int | int8 | int16 | int32 | int64
}

func print[T Integer](a, b T) T {
	if a < b {
		return a
	}
	return b
}

type MyInt int

func main() {
	var a int = 10
	var b int = 20
	fmt.Println(min(a, b))

	var c int16 = 10
	var d int16 = 20
	fmt.Println(min(c, d))

	var e MyInt = 10
	var f MyInt = 20
	fmt.Println(min(e, f))
}

 

※ 1.18 버전에서는 에러가 발생하는데, 1.23 버전에서는 틸더(~)를 사용하지 않아도 결과값이 출력 됨.

   별도로 릴리즈 노트를 찾아서 확인 필요 ※

 

- 1.18 버전 기준으로는 틸더(~)를 사용하지 않으면, 에러가 발생하고 Integer의 int 타입에 ~을 사용해야 에러가 발생하지 않음.

- 별칭 타입(MyInt)까지 포함하려고 틸더를 사용하고, 별칭 타입까지 포함한다고 보면 됨.

package main

import (
	"fmt"
)

type Integer interface {
	~int | int8 | int16 | int32 | int64
}

func print[T Integer](a, b T) T {
	if a < b {
		return a
	}
	return b
}

type MyInt int

func main() {
	var a int = 10
	var b int = 20
	fmt.Println(min(a, b))

	var c int16 = 10
	var d int16 = 20
	fmt.Println(min(c, d))

	var e MyInt = 10
	var f MyInt = 20
	fmt.Println(min(e, f))
}

// 결과

10
10
10