Go 1.27, 또는 내가 어떻게 걱정을 멈추고 Generic Methods를 사랑하게 되었는가

Go 1.27이 2026년 8월에 출시되며, 이번에는 언어 자체에 변화가 생겼습니다. 단순히 slices 패키지에 새로운 함수가 추가된 수준이 아니라, 실제로 언어의 형태가 바뀌었습니다. 제네릭 메서드(Generic methods)가 도입되었고, 11년 동안 해결되지 않았던 이슈가 종료되었습니다. encoding/json은 비행 중 엔진을 교체하듯 조용히 새로운 엔진으로 대체되었습니다.
아래의 모든 예제는 go1.27rc3 (darwin/arm64) 환경에서 실행되었습니다. 모든 출력 블록은 에러 메시지를 포함하여 실제 결과를 그대로 복사한 것입니다. 만약 결과가 이상하게 보인다면, 그것은 컴파일러가 실제로 그렇게 출력했기 때문입니다.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. 언어의 변화 (정말로 바뀌었습니다)
1.1. 제네릭 메서드 (Generic Methods)
Go 1.18부터 제네릭이 도입되었고, 그와 동시에 '그 논의(The Conversation)'가 시작되었습니다. 내용은 다음과 같습니다.
"슬라이스 타입에
Map메서드를 작성해 보겠습니다—"
method must have no type parameters(메서드는 타입 매개변수를 가질 수 없습니다)"...좋습니다. 패키지 수준 함수로 작성하죠."
메서드는 오직 _수신자(receiver)_에 의해 선언된 타입 매개변수만 사용할 수 있었습니다. 메서드 자체가 새로운 타입 매개변수를 도입할 수 없었습니다. 따라서 모든 제네릭 변환 작업은 패키지 범위로 밀려났고, 그곳에서 MapSlice와 같은 이름의 수많은 자유 부동(free-floating) 헬퍼 함수들과 함께 자리 잡게 되었습니다.
Go 1.27은 이를 해결했습니다:
1type List[E any] []E
2
3// F는 메서드 자체에 의해 선언된 타입 매개변수입니다
4func (l List[E]) Apply[F any](f func(E) F) List[F] {
5 r := make(List[F], len(l))
6 for i, x := range l {
7 r[i] = f(x)
8 }
9 return r
10}
11
12func main() {
13 l := List[int]{1, 2, 3}
14 fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15 // [10 20 30]
16}
수신자가 반드시 제네릭일 필요도 없습니다. 일반적인 구조체도 제네릭 메서드를 가질 수 있습니다:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
오늘 오후에 당장 코드베이스 전체를 리팩토링하기 전에, 두 가지 제약 사항이 있다는 점을 기억하십시오. 이는 생각보다 중요합니다:
- 인터페이스 메서드는 타입 매개변수를 선언할 수 없습니다.
- 제네릭 메서드는 인터페이스 메서드를 구현할 수 없습니다.
두 번째 제약은 여러분을 곤란하게 만들 수 있습니다:
1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2 G does not implement Adder (wrong type for method Add)
3 have Add[T any](T) T
4 want Add(int) int
즉, 제네릭 메서드와 동적 디스패치(dynamic dispatch)는 함께 사용할 수 없습니다. 이는 Go 팀이 인색해서가 아니라, 제네릭 메서드는 무한히 많은 인스턴스화가 가능한 반면, 인터페이스 메서드 테이블은 컴파일 타임에 유한하고 고정되어 있어야 하기 때문입니다. 유한한 테이블에 무한한 것을 넣을 수는 없습니다. 우주가 허락하지 않는 일입니다.
실질적인 의미는 이렇습니다: 제네릭 메서드는 유틸리티 형태의 API를 가진 구체 타입(concrete types)을 위한 것입니다. 인터페이스 뒤에 숨어야 하는 모든 것들은 여전히 기존 방식을 사용해야 합니다.
표준 라이브러리는 이미 이를 활용하고 있습니다. math/rand/v2의 Rand에는 제네릭 메서드가 추가되었습니다:
1func (r *Rand) N[Int intType](n Int) Int
1r := rand.New(rand.NewPCG(1, 2))
2fmt.Println(r.N(int32(100))) // 76
3fmt.Println(r.N(time.Second)) // 616.436223ms
이전에는 패키지 수준의 rand.N만 제네릭이었기 때문에 전역 소스를 사용해야 했습니다. 이제는 여러분이 직접 시드(seed)를 설정한 *Rand에서도 동일한 편의성을 누릴 수 있습니다. 작은 변화지만, 아주 유용합니다.
1.2. 구조체 리터럴 필드 선택자, 혹은: 마침내 해결된 이슈 #9859
임베딩(Embedding)을 사용하면 u.ID와 같이 접근할 수 있습니다. 하지만 임베딩은 User{ID: 1}과 같은 초기화를 허용하지 않았습니다. 대신 User{Base: Base{ID: 1}}와 같이 작성해야 했으며, 이는 Go가 여러분이 정말 의도한 것이 맞는지 재차 확인하는 방식이었습니다.
이슈 #9859는 2015년에 제기되었습니다. 이제 이 이슈는 종료되었습니다. 어딘가에서 이직을 두 번이나 했을지도 모를 고퍼(gopher) 한 명이 GitHub 알림을 받게 될 것입니다.
1type Object struct{ name, color string }
2type Point3D struct {
3 Object
4 x, y, z float64
5}
6type Line struct {
7 Object
8 p, q Point3D
9}
10
11// Go 1.27: 승격된(promoted) 필드를 직접 사용 가능
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name은 Line의 필드가 아닙니다. 임베딩된 Object의 필드입니다. 이전에는 Line{Object: Object{name: "diagonal"}, ...}와 같이 작성해야 했습니다.
이제 주의사항을 살펴보겠습니다. 이 내용은 제가 직접 컴파일을 시도하며 확인한 것입니다.
(a) 키는 여전히 단순 식별자여야 합니다. 임의의 선택자 경로를 작성할 수는 없습니다.
1_ = Line{Object.name: "x"} // 구조체 리터럴에서 잘못된 필드명 Object.name
2_ = Line{p.x: 1} // 구조체 리터럴에서 잘못된 필드명 p.x
따라서 이 기능은 "키가 필드 접근처럼 작동한다"는 것이 아니라, "암시적으로 승격된 필드명을 허용한다"는 것입니다. 제목이 시사하는 것보다 약간 더 좁은 범위의 기능입니다.
(b) 임베딩된 필드와 그로부터 승격된 필드를 동시에 지정할 수 없습니다.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// 승격된 필드명과 그를 포함하는 임베딩된 필드 Object를 동시에 지정할 수 없음
합리적인 제약입니다. 동일한 메모리 영역에 대해 서로 충돌하는 두 가지 지시를 내렸기 때문에 컴파일러가 추측을 거부한 것입니다.
(c) 포인터 임베딩은 지원되지 않습니다.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// name에 접근하기 위한 잘못된 암시적 포인터 간접 참조
포인터를 따라가려면 따라갈 포인터가 필요한데, 리터럴 생성 시점에는 아직 존재하지 않습니다. 이 또한 타당한 제약입니다.
좋은 소식은 go fix가 여러분의 오래된 리터럴을 자동으로 수정해 준다는 점입니다. 이에 대해서는 나중에 더 자세히 다루겠습니다.
1.3. 함수 타입 추론이 더 명확해졌습니다
제네릭 함수의 타입 추론은 이전까지 어떤 곳에서는 작동하고 어떤 곳에서는 작동하지 않았으며, 그 기준이 모호했습니다. 변수에 할당할 때는 괜찮았지만, 같은 함수를 구조체 필드에 넣으면 컴파일 에러가 발생하여 2022년처럼 double[int]라고 명시해야 했습니다.
Go 1.27은 대상 타입이 명확하게 알려진 모든 컨텍스트에서 추론이 작동하도록 개선했습니다:
1func double[T ~int | ~float64](v T) T { return v * 2 }
2
3type S struct{ f func(int) int }
4type A [1]func(int) int
5
6func main() {
7 s := S{f: double} // 1.26: double[int]가 필요했음
8 a := A{double} // 1.26: double[int]가 필요했음
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: double[int]가 필요했음
12
13 var fn func(float64) float64 = double // 이것은 항상 작동했음
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
하나의 double 함수가 컨텍스트에 따라 세 곳에서는 func(int) int로, 네 번째 곳에서는 func(float64) float64로 인스턴스화됩니다. 옵션 구조체나 함수 값을 담은 핸들러 테이블을 많이 사용한다면, 코드베이스에서 [T]로 인한 복잡함이 대거 사라질 것입니다.
2. 런타임: 무료 성능 향상과 메모리 누수 탐지기
2.1. 크기 특화 할당 (Size-Specialized Allocation)
이제 컴파일러는 작은 객체에 대해 크기별로 특화된 할당 루틴을 호출합니다. 릴리스 노트에 따르면 80바이트 미만의 할당에서 최대 30%의 성능 향상이 있으며, 할당이 많은 프로그램에서는 전체적으로 약 1%의 향상이 있다고 합니다.
측정하지 않은 릴리스 노트는 믿지 않기에 직접 테스트해 보았습니다:
1type small struct{ a, b, c, d int }
2
3var sink any
4
5func BenchmarkAlloc(b *testing.B) {
6 for b.Loop() {
7 sink = &small{}
8 }
9}
10
11func BenchmarkSlice(b *testing.B) {
12 for b.Loop() {
13 sink = make([]byte, 48)
14 }
15}
1# 기본값 (크기 특화 malloc 활성화)
2BenchmarkAlloc-12 3000000 5.358 ns/op
3BenchmarkSlice-12 3000000 13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12 3000000 8.722 ns/op
7BenchmarkSlice-12 3000000 20.81 ns/op
8
Apple Silicon에서 할당만 수행하는 마이크로 벤치마크 기준 30~35% 더 빨라졌습니다. 즉, 이는 실험실 환경에서 성능 수치를 좋게 보이게 만든 최상의 경우입니다. 실제 프로그램에서는 가비지 컬렉션(GC)과 실제 작업들이 이 성능 향상의 대부분을 상쇄할 것입니다. 약 1% 향상이라는 주장이 정직한 수치입니다.
비용은 바이너리 크기입니다. Hello World 예제의 크기는 다음과 같습니다:
12413202 bytes (기본값)
22361826 bytes (nosizespecializedmalloc)
프로그램에 관계없이 약 50KB가 고정적으로 증가합니다. GOEXPERIMENT=nosizespecializedmalloc을 통해 이를 비활성화할 수 있지만, 이 탈출구는 Go 1.28에서 제거될 예정이므로 버그 리포트를 위한 임시 방편으로만 사용하십시오.
2.2. 고루틴 누수 프로파일링이 현실화되었습니다
Go 1.26에서 실험적으로 도입되었던 기능이 1.27에서 정식으로 제공됩니다. goroutineleakprofile GOEXPERIMENT는 사라졌으며, 이제 기본적으로 작동합니다.
1func leak() {
2 ch := make(chan int) // 아무도 보내지 않을 것입니다. 절대로요.
3 go func() {
4 <-ch
5 }()
6}
7
8func main() {
9 for range 3 {
10 leak()
11 }
12 runtime.GC()
13 runtime.GC()
14 pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
15}
1goroutineleak profile: total 3
23 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
3# 0x104e374f3 main.leak.func1+0x23 /tmp/go127/leak/main.go:12
세 개의 고루틴이 영구적으로 멈춰 있으며, 파일명과 라인 번호가 정확히 어디에서 문제가 발생했는지 가리키고 있습니다.
이 기능의 이면에는 독창적인 기술이 숨겨져 있습니다. 바로 가비지 컬렉터의 도달 가능성 분석(reachability analysis)을 재사용하는 것입니다. 고루틴 G가 프리미티브 P에서 차단되어 있고, P가 실행 가능한 어떤 고루틴(또는 그 고루틴들이 깨울 수 있는 어떤 것)으로부터도 도달할 수 없다면, 그 어떤 것도 다시는 P에 접근할 수 없으므로 G는 절대 깨어날 수 없습니다. 이는 휴리스틱이 아닌 증명입니다.
동일한 설계 덕분에 한계도 명확합니다. 채널이나 뮤텍스가 전역 변수나 여전히 실행 중인 고루틴의 지역 변수를 통해 도달 가능하다면, GC는 이를 여전히 볼 수 있으므로 런타임은 아무런 결론을 내릴 수 없습니다. 위 예제에서도 두 번의 runtime.GC() 호출이 필요하다는 점에 주목하십시오. 모든 것을 잡아낼 수는 없습니다.
하지만 "컨텍스트 취소 실패로 인해 워커 고루틴이 영원히 살아남는" 전형적인 누수는 잡아낼 수 있습니다. 이는 대부분의 누수 사례 중 가장 흔한 경우입니다.
net/http/pprof를 임포트하면 /debug/pprof/goroutineleak 경로에서도 확인할 수 있습니다. 스테이징 환경에 이를 연결하고 매주 확인하며 조용히 경악하십시오.
이 작업은 Uber의 Vlad Saioc이 기여했으며, 그는 원하는 음료를 마실 자격이 있습니다.
2.3. 트레이스백(Traceback)에서 이제 어떤 요청이 죽었는지 알려줍니다
Go 1.27 이상을 선언하는 모듈의 경우, 트레이스백 헤더 라인에 runtime/pprof 고루틴 레이블이 포함됩니다.
1func handle(ctx context.Context) {
2 var wg sync.WaitGroup
3 wg.Go(func() {
4 buf := make([]byte, 4<<10)
5 os.Stdout.Write(buf[:runtime.Stack(buf, true)])
6 })
7 wg.Wait()
8}
9
10func main() {
11 labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12 pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3 /tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5 ...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...
중요한 세부 사항은 레이블이 자식 고루틴으로 상속된다는 것입니다. 고루틴 3은 레이블을 설정한 적이 없지만, 부모로부터 레이블을 물려받았습니다.
따라서 다음에 프로덕션에서 데드락이 발생하여 누군가 프로세스에 SIGQUIT 신호를 보낼 때, 모두 비슷해 보이는 4,000개의 고루틴 대신, 각각 어떤 요청과 어떤 테넌트에 속하는지 태그가 붙은 4,000개의 고루틴을 확인할 수 있게 됩니다. 요청 진입점에 pprof.Do 세 줄만 추가하면 이 기능을 얻을 수 있습니다.
레이블에 stderr로 출력하고 싶지 않은 정보가 포함될 수 있으므로, GODEBUG=tracebacklabels=0을 통해 비활성화할 수 있으며, 이 설정은 영구적으로 유지될 예정입니다.
2.4. asynctimerchan은 영원히 사라졌습니다
Go 1.23에서는 타이머 채널을 버퍼링되지 않는(동기식) 방식으로 변경하고, 이전 동작으로 돌아갈 방법으로 asynctimerchan=1을 제공했습니다. 1.27에서 이 설정은 영구적으로 제거되었습니다. time 패키지의 채널은 이제 동기식이며, 협상의 여지가 없습니다.
흥미로운 점은 그와 함께 도입된 정책입니다. go.mod에 남아 있는 제거된 GODEBUG 설정은 즉시 빌드를 깨뜨리지 않으며, 오직 이전 값으로 설정되어 있을 때만 오류를 발생시킵니다:
1# go.mod 포함 내용: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
4
5# go.mod 포함 내용: godebug asynctimerchan=0
6(정상적으로 빌드됨)
즉, 제거된 동작에 실제로 의존하던 사람들만 경고를 받게 됩니다. 결국 기본값이 될 설정으로 지정해두고 잊어버린 사람들은 계속 신경 쓰지 않아도 됩니다. 이는 아주 사려 깊은 API 고고학적 배려입니다.
3. 표준 라이브러리
3.1. encoding/json/v2: 비행 중 엔진 교체
가장 큰 변화입니다. 2년 넘게 진행된 제안 논의가 드디어 결실을 맺었습니다.
이제 세 개의 패키지가 존재하며, 그 차이를 이해하는 것이 핵심입니다:
| 패키지 | 역할 |
|---|---|
encoding/json | 여러분이 알고 있는 v1 API. 동작 100% 동일. 이제 v2 기반으로 구현됨 |
encoding/json/v2 | 의미론적 처리. Go 값 ↔ JSON |
encoding/json/jsontext | 구문론적 처리. 토큰 스트림으로서의 JSON |
핵심은 encoding/json이 완전히 다른 구현으로 재구축되었음에도 동일하게 작동한다는 것입니다. 방법은 다음과 같습니다:
1// Go 1.27의 encoding/json.Unmarshal, 요약
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
모든 레거시 v1의 기묘한 동작들이 옵션으로 인코딩되어 DefaultOptionsV1()에 묶였고, v1 API는 항상 이 묶음을 적용합니다. 따라서 다음 코드는 1.26과 1.27에서 동일하게 작동합니다:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
네, v1은 여전히 중복 키를 조용히 허용하고 마지막 값을 취합니다. 항상 그랬고, 앞으로도 그렇습니다. 호환성이란 나쁜 부분까지도 호환된다는 의미입니다.
반면 v2는 확실한 의견을 가집니다:
1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: 중복된 객체 멤버 이름 "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: 오프셋 1 이후 잘못된 UTF-8
중복 키와 잘못된 UTF-8은 거부됩니다. 두 파서가 어떤 중복 키를 우선할지 의견이 갈리면 보안 버그가 발생하므로, 이는 단순히 깔끔하지 않은 수준을 넘어 위험한 동작입니다. CouchDB의 CVE-2017-12635가 정확히 이 경우였습니다. 하나는 roles 키를 읽고 다른 하나는 다른 것을 읽었기 때문입니다. 거부하는 것이 올바른 결정입니다.
옵션은 가변적입니다:
1type Config struct {
2 Name string `json:"name"`
3 Tags []string `json:"tags,omitzero"`
4 Timeout int `json:"timeout,omitzero"`
5}
6
7out, _ := jsonv2.Marshal(Config{Name: "api"})
8// {"name":"api"}
9
10// 정렬된 맵 키 + 들여쓰기
11out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
12 jsonv2.Deterministic(true), jsontext.WithIndent(" "))
13// {
14// "a": 1,
15// "b": 2
16// }
17
18// 알 수 없는 필드 거부 — 이전에는 Decoder와 DisallowUnknownFields()가 필요했음
19var c Config
20err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
21 jsonv2.RejectUnknownMembers(true))
22// json: JSON 문자열을 Go main.Config로 언마샬할 수 없음:
23// 알 수 없는 객체 멤버 이름 "nope"
Deterministic, MatchCaseInsensitiveNames, StringifyNumbers, FormatNilSliceAsNull, OmitZeroStructFields 등 이전에 서드파티 라이브러리나 직접 작성한 MarshalJSON으로 해결했던 대부분의 문제들이 이제는 플래그로 제공됩니다.
마이그레이션 전략은 가장 큰 장점입니다. 나중에 설정된 옵션이 우선하므로, v2의 엄격함을 점진적으로 도입할 수 있습니다:
1// v1 의미론을 유지하되, v2처럼 중복 키는 거부
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// 중복된 객체 멤버 이름 발생
중복 키를 막기 위해 20만 라인의 코드베이스를 v2로 통째로 포팅할 필요가 없습니다. 옵션 하나만 바꾸면 됩니다. 대규모 프로젝트에서는 이것이 가장 현실적인 경로입니다.
성능: 마샬링은 대략 비슷하며, 언마샬링은 훨씬 빠릅니다. 문제가 발생하면 GOEXPERIMENT=nojsonv2를 통해 이전 구현으로 되돌릴 수 있지만, 이 역시 결국 제거될 예정이므로 적응하기보다는 이슈를 제기하십시오.
jsontext는 저수준 레이어입니다. Encoder/Decoder가 JSON을 Token과 Value로 순회하며 상태 머신이 일관성을 유지합니다. 스트리밍 변환기나 JSON 필터를 작성할 때, 혹은 Go 값을 구체화하고 싶지 않을 때 사용하십시오.
3.2. 표준 uuid 패키지
드디어 왔습니다. RFC 9562가 표준 라이브러리에 포함되었으며, go get은 필요 없습니다.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: 상위 48비트가 타임스탬프이므로 생성 순서대로 정렬됨
8 a, b := uuid.NewV7(), uuid.NewV7()
9 fmt.Println(a.Compare(b) < 0) // true
10
11 u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
12 fmt.Println(u, err)
13 // 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>
14
15 fmt.Println(uuid.Nil(), uuid.Max())
16 // 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
17}
전체 API는 New, NewV4, NewV7, Nil, Max, Parse, MustParse와 type UUID [16]byte 타입 하나뿐입니다. 커피가 식기 전에 패키지 문서를 모두 읽을 수 있습니다.
알아두어야 할 세부 사항:
- **
UUID는[16]byte**입니다. 따라서==비교가 작동하며 맵 키로 직접 사용할 수 있습니다.google/uuid와 같은 설계입니다. Nil과Max는 변수가 아닌 함수입니다. 패키지 수준의var Nil UUID는 발등을 향한 장전된 총과 같아서, 누군가 언젠가는 그 값을 변경할 것이기 때문입니다.- 난수는 암호학적으로 안전한 생성기에서 가져옵니다.
encoding.TextMarshaler/TextUnmarshaler/TextAppender를 구현하므로 JSON 구조체에 바로 넣을 수 있습니다.NewV7은 데이터베이스 기본 키로 가장 적합합니다. 시간 순 정렬이 가능하므로 v4 UUID에서 고질적으로 발생하는 B-tree 인덱스 파편화 문제가 없습니다.
이제 의존성 하나를 제거할 수 있습니다. v1/v3/v5나 더 복잡한 파싱 옵션이 필요하다면 여전히 서드파티 라이브러리의 역할이 남아 있습니다.
3.3. crypto/mldsa: 양자 내성 서명, 그리고 거대한 크기
Go 1.24에서 양자 내성 키 교환을 위한 crypto/mlkem이 도입되었습니다. Go 1.27은 나머지 절반인 FIPS 204 표준화된 ML-DSA 서명을 가져옵니다.
1sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
2pk := sk.PublicKey()
3
4msg := []byte("release the gophers")
5opts := &mldsa.Options{Context: "gosuda.org/blog"}
6
7sig, _ := sk.Sign(rand.Reader, msg, opts)
8
9fmt.Println("private key seed:", len(sk.Bytes()), "bytes") // 32
10fmt.Println("public key:", len(pk.Bytes()), "bytes") // 1952
11fmt.Println("signature:", len(sig), "bytes") // 3309
12
13fmt.Println(mldsa.Verify(pk, msg, sig, opts))
14// <nil>
15fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
16// mldsa: 잘못된 서명
숫자를 보십시오. 서명 하나가 3,309바이트입니다. Ed25519는 64바이트입니다. 50배 증가했으며, 공개 키는 2KB에 달합니다. 인증서 체인에 몇 개만 넣어도 TLS 핸드셰이크가 자체 MTU 전략을 고민해야 할 지경입니다.
이것이 오늘날 양자 내성의 실제 비용이며, 모두가 당장 전환하지 않는 이유입니다. 하지만 이제 표준 라이브러리에 포함되었으므로, 필요하기 전에 미리 준비할 수 있습니다.
Options.Context는 도메인 분리입니다. 동일한 키를 서로 다른 목적으로 사용할 때, 각각 다른 컨텍스트를 사용하면 한 컨텍스트의 서명은 다른 곳에서 검증되지 않습니다. 위 예제는 같은 키, 같은 메시지라도 컨텍스트가 다르면 거부됨을 보여줍니다.
PrivateKey는 crypto.Signer를 구현하므로 기존 인터페이스에 바로 적용됩니다. crypto/x509는 ML-DSA 키와 서명을 처리하며, crypto/tls는 TLS 1.3에서 MLDSA44/MLDSA65/MLDSA87 서명 체계를 지원합니다.
난수를 생략하는 SignDeterministic도 제공되어 테스트와 재현 가능한 빌드에 유용합니다.
3.4. simd: 어셈블리 없는 벡터 명령어
Go 1.26에서는 아키텍처별 simd/archsimd가 실험적으로 도입되었습니다. Go 1.27은 이식 가능하고 벡터 너비에 구애받지 않는 simd를 추가했습니다. GOEXPERIMENT=simd로 활성화하십시오.
1// dst = a*x + y
2func axpy(dst, x, y []float32, a float32) {
3 va := simd.BroadcastFloat32s(a)
4 w := va.Len()
5
6 i := 0
7 for ; i+w <= len(x); i += w {
8 vx := simd.LoadFloat32s(x[i:])
9 vy := simd.LoadFloat32s(y[i:])
10 vx.MulAdd(va, vy).Store(dst[i:])
11 }
12 // 부분 로드/스토어로 꼬리 처리 — 스칼라 정리 루프 불필요
13 if i < len(x) {
14 vx, _ := simd.LoadFloat32sPart(x[i:])
15 vy, _ := simd.LoadFloat32sPart(y[i:])
16 vx.MulAdd(va, vy).StorePart(dst[i:])
17 }
18}
1$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
2vector bits: 128 emulated: false
3[4 7 10 13 16 19 22 25 28 31]
중요한 설계 선택은 벡터 너비가 하드코딩되지 않는다는 점입니다. 런타임에 va.Len()을 묻습니다. VectorBitSize()는 실제 너비를 알려주고, Emulated()는 실제 하드웨어를 쓰는지 아니면 정중한 소프트웨어 에뮬레이션을 사용하는지 알려줍니다. 제 Mac은 128비트 NEON을 보고했고, AVX-512 머신은 더 큰 값을 보고할 것이며, 아무것도 없는 머신은 에뮬레이션을 보고하지만 코드는 여전히 실행됩니다.
LoadFloat32sPart/StorePart는 특별히 언급할 가치가 있습니다. 수동으로 작성된 SIMD에서 가장 성가신 부분은 항상 배열 끝의 불균일한 꼬리 처리인데, 이 기능은 별도의 스칼라 루프 없이 이를 처리합니다.
아직 실험적이고 API가 불안정하므로 프로덕션에는 사용하지 마십시오. 하지만 이것이 어셈블리나 cgo가 아닌 Go 코드라는 점은 매우 큰 사건입니다.
3.5. hash/maphash.Hasher
값 타입과 해시 기반 컨테이너 사이의 계약을 기술하는 새로운 인터페이스입니다:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
왜 필요할까요? Go의 내장 맵은 comparable 키만 허용합니다. 슬라이스를 키로 사용할 수 없고, "대소문자 구분 없이 동일"과 같은 정의도 내릴 수 없습니다. Hasher는 이 둘을 동시에 해결합니다:
1type CaseInsensitive struct{}
2
3func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
4 h.WriteString(strings.ToLower(s))
5}
6func (CaseInsensitive) Equal(x, y string) bool {
7 return strings.ToLower(x) == strings.ToLower(y)
8}
9
10var seed = maphash.MakeSeed()
11
12func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
13 var h maphash.Hash
14 h.SetSeed(seed) // 동일한 시드여야 의미가 있음
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
일반적인 == 의미론을 위해서는 ComparableHasher[T]가 있습니다:
1hashOf(maphash.ComparableHasher[int]{}, 42)
주의할 점: Hasher는 인터페이스이며 데이터 구조가 아닙니다. 아직 이를 소비하는 표준 해시 테이블이나 Bloom 필터는 없습니다. 이는 미래의 컨테이너 패키지를 위한 기반 작업입니다. 오늘날에는 직접 구조체를 만들 때 사용하거나, go/types.Hasher(Identical을 존중하며 types.Type을 맵 키로 사용 가능)와 같은 기존 구현을 소비할 수 있습니다.
시드 관리는 여러분의 책임입니다. 만약 위 hashOf가 호출될 때마다 새로운 시드를 만들었다면 동일한 값도 다르게 해싱되어 false가 출력되었을 것입니다. 이것이 제가 첫 번째 시도에서 저지른 버그였습니다. 컨테이너당 하나의 시드를 사용하십시오. (시드는 해시 플러딩 DoS 공격을 방어하기 위해 랜덤화되므로 상수가 아닙니다.)
3.6. httptest.NewTestServer + synctest.Sleep: 최고의 히트작
이번 릴리스에서 딱 하나만 채택해야 한다면, 이것을 선택하십시오.
testing/synctest는 Go 1.25에서 정식화되었으며 동시성 코드 테스트를 위한 가짜 시계를 제공했습니다. 하지만 테스트가 실제 네트워크에 닿는 순간 그 환상은 깨졌습니다. Go 1.27의 httptest.NewTestServer는 메모리 내 가짜 네트워크를 사용하므로 환상이 깨지지 않습니다. 그리고 synctest.Sleep (= time.Sleep + synctest.Wait)이 이를 완성합니다.
지수 백오프를 포함한 재시도 테스트 예제입니다:
1func TestRetryWithBackoff(t *testing.T) {
2 synctest.Test(t, func(t *testing.T) {
3 var hits int
4 srv := httptest.NewTestServer(t, http.HandlerFunc(
5 func(w http.ResponseWriter, r *http.Request) {
6 hits++
7 if hits < 3 {
8 w.WriteHeader(http.StatusServiceUnavailable)
9 return
10 }
11 io.WriteString(w, "ok")
12 }))
13
14 client := srv.Client()
15 start := time.Now()
16 var body string
17 for attempt := range 5 {
18 resp, err := client.Get(srv.URL)
19 if err != nil {
20 t.Fatal(err)
21 }
22 b, _ := io.ReadAll(resp.Body)
23 resp.Body.Close()
24 if resp.StatusCode == http.StatusOK {
25 body = string(b)
26 break
27 }
28 synctest.Sleep(time.Duration(1<<attempt) * time.Second)
29 }
30
31 if body != "ok" {
32 t.Fatalf("got %q", body)
33 }
34 // 백오프는 정확히 1초 + 2초 = 3초여야 함
35 if elapsed := time.Since(start); elapsed != 3*time.Second {
36 t.Fatalf("elapsed = %v, want 3s", elapsed)
37 }
38 t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
39 })
40}
1=== RUN TestRetryWithBackoff
2 x_test.go:48: hits=3 elapsed=3s
3--- PASS: TestRetryWithBackoff (0.00s)
두 번 읽어보십시오. 실제 HTTP 서버를 대상으로 3초간의 백오프를 시뮬레이션하고 0.00초 만에 끝났습니다. 그리고 단언문은 elapsed == 3*time.Second입니다. 스케줄러의 지터(jitter)가 섞인 "대략 3초"가 아니라 정확히 3초입니다. 가짜 시계는 지터가 없습니다.
synctest.Sleep은 특정 이유로 존재합니다. 테스트가 테스트 대상 코드와 동일한 시간 동안 잠들면, 누가 먼저 깨어날지 아무도 알 수 없습니다. synctest.Sleep은 잠든 후 버블 내의 모든 다른 고루틴이 완전히 차단될 때까지 기다리므로, 시스템이 안정된 후에 관찰할 수 있습니다.
타임아웃, 재시도, 서킷 브레이커, 속도 제한기 등 모든 시간 의존적 HTTP 클라이언트 테스트를 빠르고 결정론적으로 만들 수 있습니다. 현재 여러분의 테스트 스위트가 time.Sleep(100 * time.Millisecond)와 희망으로 버티고 있다면, 이것이 해결책입니다.
3.7. 프로덕션에 실질적인 영향을 주는 net/http 변경 사항
조용하지만 메트릭에 나타날 것입니다.
HTTP/1 응답 본문의 Close 시 자동 드레인(auto-drain). 본문을 닫을 때 읽지 않은 콘텐츠가 (보수적인 제한까지) 자동으로 드레인되어 연결을 재사용할 수 있게 됩니다. 이제 2016년부터 Stack Overflow 답변에서 복사해 붙여넣던 그 주문을 드디어 삭제할 수 있습니다:
1// 이제 필요 없음
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
대부분의 프로그램에서는 아무 효과가 없거나 작은 이득입니다. 만약 상황이 악화된다면, 릴리스 노트가 정중하게 설명하는 범주에 속할 가능성이 높습니다: Transport.MaxIdleConns가 0으로 설정되어 있거나, 유휴 연결 제한을 완전히 우회하여 요청당 새 Client를 생성하는 경우입니다. Transport.DisableKeepAlives = true로 덮을 수는 있지만, 릴리스 노트의 실제 조언은 "더 깊이 들여다보는 것이 유익할 것"이며, 이는 Go 팀의 언어로 "당신에게는 더 큰 문제가 있습니다"라는 뜻입니다.
HTTP/2 클라이언트 우선순위 (RFC 9218). 서버가 이제 클라이언트 우선순위 신호를 존중합니다. 이전의 라운드 로빈 스케줄링을 선호한다면 Server.DisableClientPriority = true를 사용하십시오.
Server.MaxHeaderValueCount. 서버가 수락할 헤더 값의 개수를 제한하며 기본값은 DefaultMaxHeaderValueCount입니다. "만 개의 헤더를 보내고 무슨 일이 일어나는지 지켜보는" 공격에 닫힌 문 하나가 더 늘었습니다.
사용자 제공 연결에 대한 ALPN. net.Conn이 ConnectionState() tls.ConnectionState를 구현하면 Transport와 Server가 그 위에서 TLS ALPN 협상을 수행합니다. 따라서 프록시를 통과하는 사용자 정의 다이얼러도 HTTP/2를 협상할 수 있습니다.
3.8. 기쁨을 주는 작은 것들
strings.CutLast / bytes.CutLast. Cut은 첫 번째 구분자에서 분리합니다. "마지막" 변형이 없어서 모두가 LastIndex와 슬라이싱을 직접 작성했고, 약 30%가 첫 시도에서 오프바이원(off-by-one) 오류를 범했습니다.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
파일 확장자나 host:port 파싱에 아주 좋습니다. IPv6 주소는 콜론으로 가득 차 있으므로 마지막 콜론을 찾아야 하기 때문입니다.
math/big.Int.Divide. 명시적인 반올림 모드가 있는 나눗셈입니다. "Quo인가 Div인가?"라는 동전 던지기를 대체합니다:
1x, y := big.NewInt(-7), big.NewInt(2)
2
3new(big.Int).Divide(x, y, new(big.Int), big.Trunc) // q=-3 r=-1
4new(big.Int).Divide(x, y, new(big.Int), big.Floor) // q=-4 r=1
5new(big.Int).Divide(x, y, new(big.Int), big.Round) // q=-4 r=1
6new(big.Int).Divide(x, y, new(big.Int), big.Ceil) // q=-3 r=-1
규정에 반올림 규칙이 명시된 도메인에서 일한다면 여러분을 위한 기능입니다.
url.URL.Clone 및 url.Values.Clone. 딥 카피입니다. 이전의 *u 얕은 복사는 Userinfo 포인터를 공유하여 하루 종일 찾고 5초 만에 고치는 버그를 유발하곤 했습니다.
1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.Println(orig.Host, clone.Host) // gosuda.org example.com
net.UnixConn 읽기 메서드는 이제 net.OpError로 감싸는 대신 io.EOF를 직접 반환합니다. err == io.EOF가 이제 항상 그랬어야 했던 대로 작동합니다.
database/sql.ConvertAssign 및 driver.RowsColumnScanner. 드라이버 작성자를 위한 기능입니다. 전자는 Rows.Scan이 수행하는 타입 변환을 노출하고, 후자는 드라이버가 중간 할당을 건너뛰고 사용자의 목적지로 직접 스캔할 수 있게 합니다.
unicode 15 → 17. 한 번에 두 버전을 건너뛰었습니다. 문자 분류와 정규화 동작이 미묘하게 바뀔 수 있습니다. 여기에 의존하는 테스트가 있다면 확인할 필요가 있습니다.
compress/flate가 더 빨라졌습니다 — 그리고 출력 바이트가 Go 1.26과 다를 수 있습니다. 이는 archive/zip, compress/gzip, compress/zlib, image/png로 이어집니다. 압축된 출력의 해시를 검증하는 골든 파일 테스트가 있다면 깨질 것이며, 이는 회귀가 아닙니다. 사고 검토 중에 깨닫지 말고 업그레이드 전에 확인하십시오.
4. 툴체인
4.1. go fix에 더 많은 현대화 도구 추가
go fix는 조용히 코드 현대화 도구로 변모하고 있습니다. Go 1.27은 atomictypes, embedlit, slicesbackward, unsafefuncs를 추가했습니다. -diff로 미리 볼 수 있습니다:
1package fixdemo
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Base struct{ ID int }
9type User struct {
10 Base
11 Name string
12}
13
14func New() User {
15 return User{Base: Base{ID: 1}, Name: "gopher"}
16}
17
18var counter int64
19
20func Incr() { atomic.AddInt64(&counter, 1) }
21
22func Reverse(s []string) {
23 for i := len(s) - 1; i >= 0; i-- {
24 _ = s[i]
25 }
26}
27
28func Spawn(wg *sync.WaitGroup) {
29 wg.Add(1)
30 go func() {
31 defer wg.Done()
32 }()
33}
1$ go1.27rc3 fix -diff ./fixdemo
1 import (
2+ "slices"
3 "sync"
4 "sync/atomic"
5 )
6
7 func New() User {
8- return User{Base: Base{ID: 1}, Name: "gopher"}
9+ return User{ID: 1, Name: "gopher"}
10 }
11
12-var counter int64
13+var counter atomic.Int64
14
15-func Incr() { atomic.AddInt64(&counter, 1) }
16+func Incr() { counter.Add(1) }
17
18 func Reverse(s []string) {
19- for i := len(s) - 1; i >= 0; i-- {
20- _ = s[i]
21+ for _, v := range slices.Backward(s) {
22+ _ = v
23 }
24 }
25
26 func Spawn(wg *sync.WaitGroup) {
27- wg.Add(1)
28- go func() {
29- defer wg.Done()
30- }()
31+ wg.Go(func() {
32+ })
33 }
작은 파일 하나에 네 가지 현대화 기능이 적용되었습니다:
embedlit— §1.2에서 소개된 새로운 문법을 사용하여 임베딩 리터럴을 재작성합니다.atomictypes—atomic.AddInt64(&x, 1)이atomic.Int64.Add(1)로 바뀌어 원자적이지 않은 접근을 불가능하게 만들고, 여러분이 몰랐던 32비트 정렬 버그를 조용히 수정합니다.slicesbackward— 역방향 루프가slices.Backward로 변경됩니다.waitgroupgo—Add/go/Done의식이wg.Go로 바뀝니다 (모호성을 피하기 위해 1.26의waitgroup에서 이름이 변경됨).
go tool fix help는 26가지 모든 기능을 나열하며, go tool fix help <name>은 하나를 설명합니다. 또한 fmtappendf는 "스타일적 우려"로 인해 제거되었습니다. 이슈 스레드에서 무슨 일이 있었는지 묘사하는 아주 외교적인 표현입니다.
오래된 코드베이스에서 go fix ./...를 실행하는 것은 정말 만족스러운 오후를 보낼 방법입니다. 물론 diff를 먼저 읽어보십시오.
4.2. go test가 기본적으로 stdversion을 실행합니다
여러분의 하루를 방해할 가능성이 가장 높은 변화입니다.
go test는 이제 기본적으로 stdversion vet 체크를 실행하여 go.mod의 go 지시문보다 최신인 표준 라이브러리 심볼을 사용하면 플래그를 표시합니다:
1// go.mod 내용: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast는 1.27 심볼
8 return ext
9}
1$ go1.27rc3 test ./...
2# sv
3./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
4FAIL sv [build failed]
이 기능은 모든 것이 로컬(최신 툴체인)에서는 잘 작동하는데 CI나 사용자의 오래된 환경에서 폭발하는 고전적인 실패 모드를 방지합니다. 보수적인 go 지시문을 사용하는 라이브러리를 배포한다면 이는 선물과 같습니다.
4.3. go test -json에 OutputType 추가
"Action":"output" 라인에 이제 선택적인 "OutputType" 필드가 포함됩니다: "error", "error-continue", 또는 "frame".
1'frame' '=== RUN TestFail\n'
2None ' x_test.go:6: hello\n'
3'error' ' x_test.go:7: boom\n'
4'frame' '--- FAIL: TestFail (0.00s)\n'
5'frame' 'FAIL\n'
t.Log 출력(필드 없음), t.Error 출력(error), 프레임워크 생성 라인(frame)을 이제 구분할 수 있습니다. 어떤 테스트 출력 라인이 "실제"인지 알기 위해 정규식을 작성했던 모든 사람들은 이제 그것을 삭제할 수 있습니다.
4.4. go doc 개선
package@version 문법. 모듈에 추가하지 않고도 특정 버전의 문서를 읽을 수 있습니다:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
업그레이드 전 변경 사항을 확인하기에 편리합니다.
-ex 및 예제 소스 출력:
1$ go1.27rc3 doc -ex strings | grep Example
2 func ExampleClone()
3 func ExampleCompare()
4 func ExampleContains()
5 func ExampleCut()
6 func ExampleCutPrefix()
7 ...
8
9$ go1.27rc3 doc strings.ExampleCut
10package main
11
12import (
13 "fmt"
14 "strings"
15)
16
17func main() {
18 show := func(s, sep string) {
19 before, after, found := strings.Cut(s, sep)
20 fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
21 }
22 show("Gopher", "Go")
23 ...
24}
25
26Output:
27Cut("Gopher", "Go") = "", "pher", true
예제 소스와 예상 출력을 브라우저 탭을 열지 않고도 터미널에서 확인할 수 있습니다.
4.5. go mod tidy가 require 블록을 정리합니다
go 1.27 이상을 사용하는 모듈의 경우, go mod tidy는 흩어진 require 블록을 최대 두 개(직접 및 간접)로 병합하며 첨부된 주석을 보존합니다.
1// 이전
2module tidydemo
3
4go 1.27
5
6require golang.org/x/sync v0.10.0
7
8// networking
9require golang.org/x/net v0.33.0
10
11require (
12 golang.org/x/sys v0.28.0 // indirect
13)
14
15require golang.org/x/text v0.21.0 // indirect
1// 이후: go mod tidy
2module tidydemo
3
4go 1.27
5
6require (
7 // networking
8 golang.org/x/net v0.33.0
9 golang.org/x/sync v0.10.0
10)
// networking 주석이 살아남았다는 점에 주목하십시오. 이는 주로 Git 병합 충돌 해결 후 발생하는 흩어진 require 블록을 정리합니다. 팀에 의존성을 추가하는 사람이 세 명 이상이라면, go.mod 충돌이 눈에 띄게 조용해질 것입니다.
4.6. 기타 변경 사항
bzr지원 제거. 이 기능이 영향을 미친다면 정말 사연을 들어보고 싶습니다.go tool trace -http가 포트만 주어지면 localhost에 바인딩됩니다.-http=:6060은 더 이상 모든 인터페이스에서 수신하지 않습니다. 의도했다면-http=0.0.0.0:6060을 사용하십시오. 이제go tool pprof와 일치하며 가끔 발생하는 공개 프로파일러 노출 사고를 방지합니다.- **응답 파일(
@file)**이compile,link,asm,cgo,cover,pack에서 GCC 호환 형식으로 지원됩니다. 명령줄 길이 제한을 넘어가는 빌드 시스템(Bazel 등)을 위한 기능입니다.
5. 컴파일러, 링커, 포트
컴파일러. //line 지시문의 상대 파일명은 이제 go/scanner와 일치하도록 포함된 파일의 디렉토리를 기준으로 해석됩니다. 코드 생성기를 작성한다면 관련이 있습니다.
함수 리터럴(클로저) 심볼 이름도 이제 더 단순해졌습니다. 인라인 여부와 관계없이 동일한 이름을 가지며, 동일한 리터럴의 여러 인스턴스가 바이너리에서 코드를 공유할 수 있습니다. 기능적인 변화는 없지만, reflect.Value.Pointer를 통해 함수 정체성을 비교하는 코드는 이전보다 "동일하다"고 판단할 가능성이 높습니다. 그러한 비교는 원래 유효하지 않았지만, 코드가 있다면 이제 더 크게 거짓말을 하게 될 것입니다.
링커. 새로운 -macos 및 -macsdk 옵션은 macOS LC_BUILD_VERSION 로드 명령에서 OS 및 SDK 버전을 설정합니다.
포트.
- Darwin은 이제 macOS 13 Ventura 이상을 요구합니다. 1.26에서 예고된 대로입니다. CI 러너를 확인하십시오.
- PowerPC (
GOOS=linux GOARCH=ppc64)는 ELFv2 ABI로 전환되었습니다. Linux 커널 3.13 이상이 필요합니다(RHEL7은 3.10으로 백포트됨). Cgo, PIE, 외부 링킹이 이제 지원됩니다. cgo를 사용하지만 정적 순수 Go 바이너리가 필요하다면CGO_ENABLED=0을 설정하십시오.
6. 업그레이드 체크리스트
Go 1.27은 호환성을 진지하게 생각하지만, 버전을 올리기 전에 다음을 확인하십시오:
- 압축 출력에 대한 골든 파일 테스트.
compress/flate인코더 변경으로 gzip/zip/png 바이트가 다를 수 있습니다. - JSON 에러 메시지 문자열을 매칭하는 테스트. 동작은 동일하지만, 에러 텍스트는 다릅니다.
-
go.mod에서 제거된 GODEBUG.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— 이들은 이전 값으로 설정되어 있을 때만 빌드를 실패하게 합니다. -
stdversion위반.go test가 이제 이를 잡아냅니다. 보수적인go지시문을 사용하는 라이브러리를 유지 관리한다면 미리 실행하십시오. - macOS 12 이하의 CI 러너. 지원이 종료되었습니다.
- 함수 리터럴 심볼 이름을 단언하는 테스트, 그리고
reflect.Value.Pointer함수 비교. -
Transport.MaxIdleConns = 0또는 요청당Client생성. 자동 드레인 응답 본문과 결합되어 느려질 수 있습니다.
맺음말
Go 1.27은 미뤄두었던 유지보수 작업들이 한꺼번에 해결된 릴리스입니다.
제네릭 메서드는 1.18 제네릭 작업의 빠진 조각이었습니다. 구조체 리터럴 필드 선택자는 11년 된 이슈를 해결했습니다. encoding/json/v2는 2년 동안의 제안 논의였습니다. 그리고 uuid는 거의 모든 Go 프로젝트가 10년 동안 같은 서드파티 의존성으로 패치해 오던 구멍을 메웠습니다.
이 릴리스에서 실질적인 가치를 얻기 위한 우선순위는 다음과 같습니다:
httptest.NewTestServer+synctest.Sleep— 시간 의존적 HTTP 테스트가 빠르고 결정론적으로 바뀝니다. 전체 릴리스에서 노력 대비 보상 비율이 가장 높습니다.- 고루틴 누수 프로파일링 — 스테이징 환경에서 엔드포인트 하나를 노출하고 겸손해질 준비를 하십시오.
- 트레이스백 고루틴 레이블 — 요청 진입점에
pprof.Do세 줄만 추가하면 다음 새벽 3시의 스택 덤프를 읽을 수 있게 됩니다. go fix ./...— 도구가 현대화를 수행하게 하십시오.encoding/json/v2— 서두를 필요 없습니다. 옵션 하나씩 도입하십시오.
go1.27rc3를 가져와 지금 테스트 스위트를 실행해 보십시오. 체크리스트의 모든 항목은 사고 처리 중에 발견하는 것보다 화요일 오후에 미리 발견하는 것이 훨씬 유쾌합니다.
참고 문헌