Go의 실험 기능이 무엇인지, 어떻게 동작하는지, 현재 어떤 실험들이 제공되는지, 그리고 GOEXPERIMENT로 이를 활성화하거나 비활성화하는 방법을 설명합니다.
Go는 릴리스의 일부로 실험적 기능 을 함께 제공하는 경우가 많습니다.
이러한 실험적 기능은 여러 형태를 가질 수 있습니다. 때로는 표준 라이브러리의 완전히 새로운 패키지이기도 하고, 때로는 컴파일러나 런타임의 변경이기도 하며, 또는 — 아주 드물게는 — Go 동작에 대한 호환성 파괴 변경일 수도 있습니다.
대부분의 경우 실험적 기능의 목적은 어떤 기능이 일반 제공 단계로 올라가 Go의 영구적인 일부가 되기 전에 사용자들로부터 실제 환경의 피드백을 얻는 것입니다. 기능이 회귀를 일으키거나 커뮤니티로부터 부정적인 피드백을 받으면, 최종 확정되기 전에 변경될 수 있고 — 심지어 완전히 폐기될 수도 있습니다.
Go 실험이 다룰 수 있는 범위를 보여주기 위해 최근의 몇 가지 예를 살펴보겠습니다.
Go 1.24는 새로운 testing/synctest 패키지에 대한 실험적 지원을 포함해 출시되었습니다(동시성 코드 테스트를 지원합니다). 피드백을 받은 뒤 패키지 API가 약간 조정되었고, Go 1.25에서 일반 제공 단계로 올라갔습니다.
Go 1.25는 더 나은 성능을 가진 새로운 가비지 컬렉터 설계에 대한 실험적 지원을 포함해 출시되었습니다. 피드백이 반영된 뒤, 새 가비지 컬렉터는 Go 1.26에서 기본값이 되었습니다.
Go 1.21은 루프 변수 의미론에 대한 동작 변경을 실험적으로 도입했습니다. 이 변경은 이전에 Go 코드에서 흔했던 버그를 막아 주었지만, 기술적으로는 언어에 대한 호환성 파괴 변경이었습니다. 이 변경을 실험으로 제공함으로써, 새로운 동작이 Go 1.22에서 기본값이 되기 전에 사람들이 자신의 코드를 테스트할 기회를 가질 수 있었습니다.
실험에 대해 하나의 고정된 생명주기가 있는 것은 아니지만, 몇 가지 공통적인 패턴은 있습니다.
대부분의 실험은 처음에 기본 비활성화 상태로 제공됩니다. 기능을 사용해 보려면 명시적으로 opt-in 해야 하며, 보통 GOEXPERIMENT 환경값을 설정하는 방식입니다(이 부분은 잠시 후 더 자세히 다루겠습니다).
상황이 좋게 흘러가면, 한두 번의 릴리스 뒤에 실험 기능은 확정되고 일반 제공 단계로 올라가며 기본 활성화 상태가 됩니다.
실험이 어떤 것의 동작에 영향을 주는 경우, 일반 제공 단계로 올라간 뒤에는 때때로 — 하지만 항상은 아닙니다 — 일시적으로 이를 비활성화하고 이전 동작을 사용할 수 있는 전환 유예 기간이 제공됩니다. 예를 들어 Go 1.26에서는 새로운 가비지 컬렉터 설계(위에서 잠깐 언급했습니다)가 일반 제공 단계로 올라가 기본 활성화 상태가 되었지만, 필요하다면 여전히 이를 비활성화하고 이전 가비지 컬렉터를 사용할 수 있습니다.
이것이 가장 흔한 패턴이지만, 때로는 더 오래 걸리거나 다르게 전개되기도 합니다. 예를 들면 다음과 같습니다.
Go 1.22는 컴파일러의 인라이닝 로직에 대한 실험적 구현을 포함해 출시되었는데, 2년이 넘은 지금도 여전히 기본 비활성화 상태로 평가 중입니다.
같은 릴리스에는 메모리 아레나 실험도 포함되어 있었습니다. 사용자들의 부정적인 피드백과 우려 이후, 이 기능은 여전히 기본 비활성화 상태이며 무기한 보류 상태이고, 결국 완전히 제거될 수도 있습니다.
또는 마지막으로, Go 팀이 어떤 변경에 충분한 확신이 있다면 피드백 단계를 건너뛰고 바로 일반 제공 단계로 갈 수도 있습니다. 다만 이 경우에도 일시적으로 비활성화할 수 있는 전환 유예 기간은 여전히 존재할 수 있습니다.
좋은 예가 Go 1.24에서 맵 구현을 Swiss tables로 변경했을 때입니다. Go 팀은 이 구현과 성능 이점에 충분한 확신이 있었기 때문에 이것은 곧바로 일반 제공 단계로 들어가 기본 활성화가 되었습니다. 하지만 — 적어도 현재로서는 — 원한다면 여전히 opt-out 해서 이전 맵 구현을 사용할 수 있습니다.
따라서 실제로는 크게 세 가지 실험 상태가 있습니다.
Go에는 일반적인 의미의 “실험”이라고 보기는 어려운 소수의 실험적 기능도 있습니다.
이들은 기본 비활성화 기능이지만, 평가 중도 아니고, 피드백을 구하는 것도 아니며, 언젠가 일반 제공 단계로 올라가 기본 활성화가 되리라는 기대도 없습니다.
다른 실험들과 마찬가지로 GOEXPERIMENT 환경설정으로 제어되지만, 실제로는 특정한 상황에서 사용하고 싶을 수 있는 선택적 Go 기능에 더 가깝습니다.
이 글의 나머지 부분에서는 이를 "영구 실험"이라고 부르겠습니다.
예를 들어 어떤 struct 필드가 접근되는지를 추적하는 field tracking 진단 기능이 있습니다. 이 기능은 10년 동안 제공되어 왔고, 이것이 일반 제공 단계로 올라갈 의도는 없습니다. 또는 Go 런타임에서 잠재적인 데드락을 찾기 위한 진단 기능인 static lock ranking 기능도 있습니다.
현재 어떤 실험 기능을 사용할 수 있고 각각의 상태가 어떤지 알아내는 일은 놀랄 만큼 어렵습니다.
안타깝게도 공식 Go 문서나 Go Wiki에는 실험 상태를 추적하는 페이지가 없고, 이 글을 위해 저는 여러 곳의 정보를 짜맞춰야 했습니다. 같은 작업을 직접 하고 싶다면 다음과 같이 할 수 있습니다.
$ go doc goexperiment.Flags 를 실행하면 사용 가능한 모든 실험 목록을 얻을 수 있습니다.
어떤 실험이 기본 활성화인지 알려면 src/internal/buildcfg/exp.go 의 소스 코드를 읽어보면 됩니다. 특히 ParseGOEXPERIMENT() 함수 안의 baseline 변수 선언을 보면 됩니다.
실험 이름을 Go 릴리스 노트와 대조하고 GitHub 이슈를 검색해 현재 상태를 파악할 수 있습니다.
제가 확인한 바로는, Go 1.26 기준 현재 사용 가능한 영구 실험은 다음과 같습니다.
| 실험 이름 | 설명 | 상태 |
|---|---|---|
FieldTrack | 어떤 struct 필드가 접근되는지 추적하는 진단 기능 | 기본 비활성화, 영구 고정 기능 |
StaticLockRanking | 데드락을 잡아내기 위해 락 획득 순서를 검증하는 진단 기능 | 기본 비활성화, 영구 고정 기능 |
CgoCheck2 | cgo 포인터 전달 규칙을 검사하는 진단 기능. 기본으로 실행하기엔 비용이 너무 큼 | 기본 비활성화, 영구 고정 기능 |
BoringCrypto | Go의 crypto를 FIPS 검증을 받은 BoringSSL로 대체. Go 1.24 이후로는 더 이상 관련성이 적음 | 기본 비활성화, 영구 고정 기능이지만 곧 제거 예정 |
PreemptibleLoops | 스케줄러가 루프의 백에지에서 goroutine을 선점할 수 있게 함. 일반적으로 Go 1.14 이후로는 관련성이 적지만, 여전히 그 외의 선점이 지원되지 않는 플랫폼에서는 유용할 수 있음 | 기본 비활성화, 영구 고정 기능 |
다음은 현재 기본 비활성화 상태인 실험들과 그 상태입니다.
| 실험 이름 | 설명 | 상태 |
|---|---|---|
HeapMinimum512KiB | 최소 힙 크기를 4MB에서 512KiB로 줄임. 제약이 있는 환경에서 유용할 수 있음 | 기본 비활성화, 아마도 휴면 상태 |
Arenas | 메모리 아레나 구현 | 부정적인 피드백 이후 기본 비활성화 및 보류 상태 |
NewInliner | 더 나은 호출 지점 휴리스틱을 갖춘 재작성된 컴파일러 인라이너 | 기본 비활성화 및 평가 중(Go 1.22부터 제공) |
JSONv2 | 향상된 JSON 인코딩/디코딩 함수를 제공하는 새로운 encoding/json/v2 패키지 | 기본 비활성화 및 평가 중(Go 1.25부터 제공) |
RuntimeSecret | 메모리를 0으로 지우는 함수를 포함한 새로운 runtime/secret 패키지. Linux amd64/arm64에서만 사용 가능 | 기본 비활성화 및 평가 중(Go 1.26부터 제공) |
GoroutineLeakProfile | goroutineleak pprof 프로파일 타입을 추가 | 기본 비활성화 및 평가 중(Go 1.26부터 제공) |
SIMD | 아키텍처별 SIMD 연산에 접근할 수 있게 해주는 새로운 simd/archsimd 패키지. amd64에서만 사용 가능 | 기본 비활성화 및 평가 중(Go 1.26부터 제공) |
RuntimeFreegc | 안전한 경우 GC 사이클을 기다리지 않고 메모리를 즉시 재사용할 수 있게 함 | 기본 비활성화 및 평가 중(Go 1.26부터 제공되지만, 상태 정보는 #74299 참고) |
SizeSpecializedMalloc | 크기 클래스별로 특화된 malloc 구현을 활성화 | 기본 비활성화 및 평가 중(Go 1.26부터 제공되지만, 상태 정보는 #74299 참고) |
다음은 현재 기본 활성화 상태인 실험들입니다.
| 실험 이름 | 설명 | 상태 |
|---|---|---|
LoopVar | 반복마다 별도 범위를 갖는 루프 변수 스코프 | Go 1.22부터 기본 활성화, 다만 예외적인 경우를 위해 opt-out 유지 |
Dwarf5 | DWARF 5 디버그 정보 생성. 바이너리 크기를 줄임 | 기본 활성화이며 일시적 opt-out 가능(opt-out은 향후 릴리스에서 제거될 수 있음) |
RandomizedHeapBase64 | 보안 조치로 시작 시 힙 베이스 주소를 무작위화 | 기본 활성화이며 일시적 opt-out 가능(opt-out은 향후 릴리스에서 제거될 예정) |
GreenTeaGC | 성능이 개선된 새로운 가비지 컬렉터. darwin/ios/aix에서는 사용 불가 | 기본 활성화이며 일시적 opt-out 가능(opt-out은 Go 1.27에서 제거될 예정) |
RegabiWrappers | ABI0와 ABIInternal 함수 사이 호출을 위한 ABI 래퍼. 64비트 아키텍처에서만 사용 가능 | 기본 활성화이며 일시적 opt-out 가능. 다만 opt-out은 s390x에서만 유효하며, Go 1.27에서 제거될 예정 |
RegabiArgs | 컴파일된 모든 Go 함수에서 레지스터 인자/결과를 활성화. 64비트 아키텍처에서만 사용 가능 | 기본 활성화이며 일시적 opt-out 가능. 다만 opt-out은 s390x에서만 유효하며, Go 1.27에서 제거될 예정 |
실험은 GOEXPERIMENT 환경설정으로 제어합니다.
기본 비활성화 상태인 실험들 중 사용해 보고 싶은 것이 있다면, 실험 이름을 쉼표로 구분한 소문자 값으로 GOEXPERIMENT 에 포함하면 됩니다. 예를 들어 JSONv2 와 GoroutineLeakProfile 실험을 활성화한 상태로 애플리케이션을 빌드하고 싶다면 다음과 같이 하면 됩니다.
$ GOEXPERIMENT=jsonv2,goroutineleakprofile go build ./...
기본 활성화 상태인 실험을 끄고 싶다면, 소문자 실험 이름 앞에 no 를 붙이면 됩니다. 예를 들어 GreenTeaGC 와 RandomizedHeapBase64 실험을 끈 상태로 애플리케이션을 빌드하고 싶다면 다음과 같이 하면 됩니다.
$ GOEXPERIMENT=nogreenteagc,norandomizedheapbase64 go build ./...
활성화하는 실험과 비활성화하는 실험을 섞어도 전혀 문제없습니다.
$ GOEXPERIMENT=jsonv2,nogreenteagc go build ./...
같은 패키지를 서로 다른 GOEXPERIMENT 값으로 빌드하면, Go는 이를 서로 다른 빌드로 취급하고 빌드 캐시에 별도 항목으로 저장한다는 점에 유의하세요.
위 예시에서는 go build 를 사용했지만, go run 이나 go test 를 사용할 때도 완전히 같은 패턴을 사용할 수 있습니다. 직접 시도해 보고 싶다면, 실험적인 encoding/json/v2 패키지를 사용하는 다음 프로그램을 만들어 보세요.
package main
import (
"encoding/json/v2"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
City string `json:"city"`
}
func main() {
p := Person{Name: "Ada", Age: 36, City: "Vienna"}
data, _ := json.Marshal(p, json.StringifyNumbers(true))
fmt.Println(string(data))
}
이를 평소처럼 실행하면 프로그램은 컴파일되지 않고, 다음과 비슷한 오류 메시지가 나타납니다.
$ go run main.go
package command-line-arguments
imports encoding/json/v2: build constraints exclude all Go files in /usr/local/go/src/encoding/json/v2
하지만 JSONv2 실험을 활성화하면 프로그램은 예상대로 실행됩니다.
$ GOEXPERIMENT=jsonv2 go run main.go
{"name":"Ada","age":"36","city":"Vienna"}
저처럼 주로 Go 자체를 다루기보다 Go로 프로그램을 작성하는 평범한 Gopher라면, 제공되는 대부분의 실험은 아마 크게 관련이 없을 것입니다.
아마 가장 흥미롭고 관련 있는 것들은 다음과 같습니다.
GreenTeaGC – Go 1.26을 사용 중이라면 이미 이것을 기본값으로 사용하고 있습니다. 하지만 성능이나 동작 문제를 발견한다면, 여전히 이를 비활성화할 수 있다는 점을 알고 있는 것이 중요합니다(그리고 이슈도 제출해야 합니다).
Dwarf5 – 이 역시 Go 1.25 이상을 사용 중이라면 이미 기본값으로 사용하고 있습니다. 하지만 문제가 발생하면 여전히 비활성화할 수 있다는 점을 아는 것이 유용합니다.
JSONv2 – 이것이 일반 제공 단계로 올라가기 전까지는 전환을 권하지 않습니다. 하지만 JSON을 많이 다루는 코드를 작성한다면, 새로운 encoding/json/v2 패키지를 실험해 보고, 앞으로 올 변화에 익숙해지고, 문제가 보이면 피드백을 주는 것은 충분히 가치가 있습니다.
GoroutineLeakProfile – goroutine 누수를 의심하고 이를 디버깅해야 한다면, 이것은 즉시 유용하며 활성화해 볼 만합니다.
RuntimeSecret – 암호학 코드를 작성하거나 민감한 데이터를 다뤄야 한다면 실험해 보고 피드백을 줄 가치가 있습니다.
RuntimeFreegc – 가비지 컬렉터에 크게 의존하는 애플리케이션이 있다면, 이것을 활성화한 상태로 코드를 벤치마킹해 성능이 개선되는지 확인해 보고, 문제가 보이면 피드백을 주는 것이 좋을 수 있습니다.
마지막으로, 실험적 기능은 Go의 호환성 보장 대상이 아니라는 점을 강조할 가치가 있습니다. API, 동작, 성능 특성은 모두 바뀔 수 있으므로, 일반적으로는 너무 일찍 도입하거나 최종 확정되기 전에 실험 기능에 의존하는 것은 피하는 편이 좋습니다.
하지만 실험적 기능은 종종 Go의 가장 큰 변화들에 대한 미리보기 역할을 합니다. 어떤 실험이 결국 일반 제공되고 기본 활성화되었을 때 자신이나 자신의 코드에 영향을 줄 가능성이 높다는 것을 안다면, 미리 시도해 보고, 적절한 경우 벤치마크를 실행하고, 문제를 발견하면 피드백을 주는 것이 좋습니다.
어떤 실험이 제공되고 있고 상태가 어떤지 계속 추적하고 싶다면, Go 릴리스 노트가 최근에는 실험적 기능과 사용 방법을 훨씬 더 잘 문서화하기 시작했다는 점도 언급할 만합니다. 이 블로그 글과 새 Go 릴리스가 나올 때 릴리스 노트를 함께 살펴보면, 현재 어떤 일이 벌어지고 있는지 꽤 괜찮은 감을 잡을 수 있을 것입니다.