Go 1.18
이 포스트는 Go 공식 블로그 내용을 참고하였습니다.
👉 Go 1.18 에서는?
go 1.18 에서는 다음 세 가지 기능이 추가되었습니다. (+ 성능 향상)
- Generics
- Fuzzing
- Workspace
지금부터 세 가지 기능에 대해 알아봅시다.
1️⃣ Generic
제네릭의 부재는 많은 사람들이 이야기하는 Go 의 불편한 점입니다.
실제로 2020 Go Developer Servey 에서 Generic 은 Go 의 불편한 점 조사에서 88%로 1위를 기록했습니다.
Go 언어 개발자들은 이러한 니즈를 기반으로 1.18에서 제네릭 문법을 추가하게 되었습니다.
👉 HOW TO USE GENERIC?
제네릭은 특정 자료형에 종속되지 않는 코드를 작성할 수 있도록 하는 문법입니다.
제네릭은 Go 문법에 다음 세가지 요소를 추가합니다:
- 함수와 자료형에 타입 파라미터를 붙일 수 있다.
- 기존 메소드만 추가할 수 있었던 인터페이스 타입에 타입의 집합을 정의할 수 있다.
- 타입 파라미터를 가진 함수 호출의 경우, 인자로 넘긴 변수 타입 기준으로 타입 파라미터의 타입을 추론한다.
🎉 타입 파라미터
아래는 제네릭 도입 없이 구현한 두 수 중에 최소값을 구하는 Min
함수들 입니다.
func MinFloat64(x, y float64) float64 {
if x < y {
return x
}
return y
}
func MinInt(x, y int) int {
if x < y {
return x
}
return y
}
// 그 외 다른 타입들.......
그리고 아래는, 제네릭을 도입한 GMin
함수입니다.
import "golang.org/x/exp/constraints"
func GenericMin[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
GenericMin
함수는 (
, )
로 감싸져 있는 파라미터 외에 [
, ]
로 감싸져 있는 타입 파라미터를 갖습니다.
타입 파라미터에는 T
라는 타입의 조건을 constraints.Ordered
라는 인터페이스로 제한해두고 있습니다.
constraints.Ordered
는 조금 후에 설명하도록 하고, 해당 함수를 호출하는 방법은 다음과 같습니다.
x := GenericMin[int](2, 3)
// 또는
fmin := GenericMin[float64]
m := fmin(2.71, 3.14)
타입 파라미터는 위의 경우처럼 함수에 올 수도 있지만 아래처럼 타입에도 붙을 수 있습니다.
type Tree[T interface{}] struct {
left, right *Tree[T]
value T
}
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }
var stringTree Tree[string]
🎉 인터페이스에 타입 집합 정의
constraints.Ordered
는 다음 패키지에 정의되어 있습니다.
https://golang.org/x/exp/constraints
아래는 constraints.go
파일의 일부입니다.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Integer interface {
Signed | Unsigned
}
type Float interface {
~float32 | ~float64
}
type Complex interface {
~complex64 | ~complex128
}
type Ordered interface {
Integer | Float | ~string
}
위 파일을 살펴보면, 인터페이스 정의 안에 |
를 기준으로 좌우에 타입명
이나 ~
+ 타입명
이 명시되어 있습니다.
여기서 ~
의 의미는 바로 뒤에 붙는 타입을 기반으로 하는 타입들을 모두 허용하겠다는 의미입니다.
예를들어 ~string
에는 type MyString string
으로 정의한 MyString
의 경우도 포함됩니다.
🎉 타입 파라미터 추론
제네릭 함수에서는 기존의 파라미터 이외에 타입 파라미터를 지정할 수 있습니다.
함수 정의에서 타입 파라미터를 지정한 경우 호출시 타입 정보를 타입 파라미터로 넘겨줘야 하는데,
이를 생략할 수 있는 경우가 있습니다.
함수의 인자로 해당 타입을 사용하는 인자를 넘겼을 경우입니다.
import "golang.org/x/exp/constraints"
func GMin[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
var a, b, m float64
m = GMin(a, b)
}
위의 경우 GMin
함수의 인자로 넘기는 a
, b
가 float64
타입이기 때문에
GMin
함수의 입장에서 ‘아 타입 파라미터에 정의된 T
타입은 이 경우 float64
이구나’ 라는 식으로
타입 파라미터를 추론할 수 있게 됩니다.
🎉 ~
연산자 주의사항
인터페이스 타입 집합 정의에서 새로이 등장한 ~
연산자가 있습니다.
이 연산자는 타입 앞에 붙어서 뒤에 오는 타입을 기반으로 하는 파생 타입을 포함한다는 의미입니다.
~
연산자를 사용하지 않은 다음 예제의 경우 컴파일 에러입니다.
func Scale[E constraints.Integer](s []E, c E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}
type Point []int32
func (p Point) String() string {
// Details not important.
}
// ScaleAndPrint doubles a Point and prints it.
func ScaleAndPrint(p Point) {
r := Scale(p, 2)
fmt.Println(r.String()) // DOES NOT COMPILE
}
위의 경우 Point
변수는 타입 파라미터 []E
의 조건을 만족하지 않습니다.
타입 파라미터 []E
는 E
타입의 슬라이스를 의미하는데, Point
는 그 int
타입의 슬라이스 자체를
하나의 Point
타입으로 보기 때문에 Point
는 []E
타입의 변수에 할당할 수 없는 것이죠.
이 경우 ~
연산자를 통해 해결할 수 있습니다. Scale
함수의 정의부를 약간 수정해보죠
// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}
이전 예제와 다른 점은, 타입 파라미터가 []E
에서 ~[]E
로 바뀐 것 뿐입니다.
이 경우 Point
는 []E
에서 파생된 타입이기 때문에 (type []int32 Point
) ~[]E
안에 할당될 수 있습니다.
이제 컴파일이 가능한 것이죠.
2️⃣ Fuzzing (퍼징, 퍼즈 테스트)
위키피디아에서는 퍼징을 다음과 같이 정의합니다.
퍼즈 테스팅(Fuzz testing) 또는 퍼징 (fuzzing)은 (종종 자동화 또는 반자동화된)
소프트웨어 테스트 기법으로서, 컴퓨터 프로그램에 유효한, 예상치 않은
또는 무작위 데이터를 입력하는 것이다.
이후 프로그램은 충돌이나 빌트인 코드 검증의 실패,
잠재적인 메모리 누수 발견 등 같은 예외에 대한 감시가 이루어진다.
퍼징은 주로 소프트웨어나 컴퓨터 시스템들의 보안 문제를 테스트하기 위해 사용된다.
이것은 하드웨어나 소프트웨어 테스트를 위한 무작위 테스팅 형식이다.
위 정의에서 볼 수 있듯, 퍼징은 개발자가 미처 생각하지 못한 버그가 있는 엣지 케이스를 찾는 데 사용하는 테스트 기법입니다.
퍼즈 테스트는 Fuzz
로 시작하고 인자로 *testing.F
를 받는 함수를 *_test.go
파일에 정의하면 사용할 수 있습니다.
다음은 퍼즈 테스트 함수의 틀입니다.
func FuzzSomething(f *testing.F) {
// 기본 테스트 케이스 퍼즈 테스트에 추가
// []byte, string, bool, byte,
// rune, float32, float64, int,
// int8, int16, inr32, int64,
// uint, uint8, uint16, uint32, uin64
// 22년 4월 현재, 위의 자료형만 퍼즈 테스트에 추가할 수 있다.
f.Add("hello")
f.Add("world")
// 퍼즈 테스트 정의
f.Fuzz(func(t *testing.T, item string) {
// 테스트하려는 함수 호출
_, err := DoSomething(item)
// 테스트 결과 정의
if err != nil {
t.Errorf("function DoSomething occured error (input: %v)", item)
}
})
}
위의 함수가 호출되면, DoSomething()
함수에 "hello"
, "world"
두 테스트 케이스와
랜덤하게 생성된 string
변수들을 익명함수의 인자로 넘겨주어 퍼즈 테스트를 진행하게 됩니다.
🎉 실제 활용
문자열의 맨 앞부터 맨 뒤까지를 서로 뒤바꾸는 Reverse()
함수가 다음과 같이 정의되어 있다고 가정합시다.
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b), nil
}
위 함수는 다음 테스트 함수로 간단한 테스트를 진행할 수 있습니다.
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev, _ := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
해당 테스트는 별 문제없이 통과를 내놓습니다.
$ go test
PASS
ok example/fuzz 0.013s
이제, Reverse()
함수를 다음 퍼즈 테스트로 테스트해봅시다.
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, _ := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
랜덤한 string
값을 무작위 대입한 결과 다음과 같은 테스트 결과가 나옵니다.
$ go test --fuzz=FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/77 completed
failure while testing seed corpus entry: FuzzReverse/07b4f351a172a5694405791c4ee125ed75b1a9417d5ea4b098127f7294a76bca
fuzz: elapsed: 0s, gathering baseline coverage: 3/77 completed
--- FAIL: FuzzReverse (0.03s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:42: Before: "䰲", after: "\xb2\xb0\xe4"
reverse_test.go:45: Reverse produced invalid UTF-8 string "\xb2\xb0\xe4"
FAIL
exit status 1
FAIL fuzz 0.393s
䰲
이라는 유니코드 문자는 utf-8
에서 한글자지만, 2바이트 이상의 공간을 차지하기 때문에
[]byte
기준으로 리버스하는 우리 함수로 앞뒤를 뒤집을 경우, "\xb2\xb0\xe4"
라는 값이 나오게 됩니다.
퍼즈 테스트는 이처럼 특정 엣지 케이스를 찾아내도록 도와주는 테스트 툴입니다.
3️⃣ Go Workspace
오른쪽과 같은 디렉토리 구조가 있다고 가정해봅시다.
prog1
에서 pkg
디렉토리 안의 pkg1
, pkg2
를 임포트하려면 prog1
의 go.mod
파일에 다음을 적었어야 했습니다.
replace pkg/pkg1 => ../../pkg/pkg1
replace pkg/pkg2 => ../../pkg/pkg2
하지만 해당 패키지가 깃 저장소에 올라가면 위의 두 줄은 삭제해야 하는데,
이를 불편해하는 사람들이 많았습니다.
go workspace
는 이런 불편함을 해소하기 위해 등장한 툴입니다.
위의 경우 아래의 명령어를 최상위 디렉토리(workspace
디렉토리)에서 입력해
로컬 패키지 임포트를 쉽게 진행할 수 있습니다.
go work init
go work use cmd/prog1
go work use pkg/pkg1
go work use pkg/pkg2
위 명령어의 결과로 다음과 같은 go.work 파일이 생성되는데,
go 1.18
use (
./cmd/prog1
./pkg/pkg1
./pkg/pkg2
)
다음 명령어로 손쉽게 pkg1
, pkg2
를 임포트한 prog1
프로그램을 빌드할 수 있습니다.
cd workspace
go build cmd/prog1
'Go' 카테고리의 다른 글
Go 에러 객체 비교 errors.Is() (2) | 2024.11.27 |
---|---|
Go 언어 입문 자료 모음 (1) | 2022.06.02 |