Go 1.27, 또는 제네릭 메서드를 두려워하지 않고 사랑하게 된 과정


Go 1.27은 2026년 8월에 출시되며, 이번에는 언어 자체가 변화했습니다. "slices에 새 함수를 추가했다" 정도의 변화가 아니라 실제로 언어가 바뀌었습니다. 제네릭 메서드가 도입되었고, 11년 된 이슈가 마침내 종결되었습니다. encoding/json은 비행 중인 비행기 안에서 조용히 새로운 엔진으로 교체되었습니다.
아래의 모든 예제는 go1.27rc3 (darwin/arm64)에서 실행되었습니다. 모든 출력 블록은 오류 메시지를 포함하여 실제 출력을 그대로 복사한 것입니다. 무언가 이상해 보인다면, 그것은 컴파일러가 실제로 그렇게 출력했기 때문입니다.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. 언어가 변화했다 (정말로)
1.1. 제네릭 메서드
Go 1.18부터 제네릭이 있었고, Go 1.18부터 "The Conversation"이라는 대화도 있었습니다. 대략 다음과 같습니다.
"슬라이스 타입에
Map메서드를 하나 작성해 보겠습니다—"
method must have no type parameters"...알겠습니다. 패키지 수준 함수로 가겠습니다."
메서드는 _리시버_가 선언한 타입 매개변수만 사용할 수 있었습니다. 메서드 자체는 새로운 타입 매개변수를 도입할 수 없었습니다. 그래서 모든 제네릭 변환 로직은 패키지 스코프로 밀려났고, 그곳에서 MapSlice의 여러 변형으로 이름 붙여진 열일곱 개의 자유 부동 헬퍼 함수와 나란히 놓이게 되었습니다.
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
따라서 제네릭 메서드와 동적 디스패치는 함께 사용할 수 없습니다. 이는 Go 팀이 인색해서가 아니라, 제네릭 메서드는 무한히 많은 인스턴스화를 가지는 반면 인터페이스 메서드 테이블은 유한하고 컴파일 시점에 알려져 있어야 하기 때문입니다. 무한한 것을 유한한 테이블에 넣을 수는 없습니다. 이는 자연의 법칙이 허락하지 않는 일입니다.
실용적으로 해석하면, 제네릭 메서드는 유틸리티 형태의 API를 가진 구체 타입을 위한 것입니다. 인터페이스 뒤에 숨어야 하는 것은 여전히 기존 방식을 사용해야 합니다.
표준 라이브러리도 이를 이미 활용했습니다. 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만 제네릭이었으며, 이는 전역 소스를 사용하는 것을 의미했습니다. 이제 직접 시드를 설정한 *Rand도 동일한 편의성을 얻습니다. 작은 변화이지만 좋은 변화입니다.
1.2. 구조체 리터럴 필드 선택자, 또는: 이슈 #9859가 마침내 해결되다
임베딩을 사용하면 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: 승격된 필드를 직접 사용
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"} // invalid field name Object.name in struct literal
2_ = Line{p.x: 1} // invalid field name p.x in struct literal
따라서 이 기능은 "키가 이제 필드 접근처럼 동작한다"는 것이 아니라, "암시적으로 승격된 필드 이름을 허용한다"는 것입니다. 헤드라인이 암시하는 것보다 다소 좁은 기능입니다.
(b) 임베디드 필드와 그로부터 승격된 항목을 동시에 지정할 수 없습니다.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
합리적입니다. 같은 메모리에 대해 두 가지 상충하는 내용을 지정했고, 컴파일러는 추측하기를 거부했습니다.
(c) 포인터 임베딩은 권장되지 않습니다.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach 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. 크기 특화 할당
컴파일러는 이제 작은 객체에 대해 크기 특화 할당 루틴을 호출하도록 코드를 생성합니다. 릴리스 노트는 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
영구적으로 멈춘 세 개의 고루틴이 있으며, 문제를 일으킨 정확한 위치를 가리키는 파일과 줄 번호가 함께 표시됩니다.
이 기능의 원리는 정말 영리합니다. 가비지 컬렉터의 도달 가능성 분석을 재사용합니다. 고루틴 G가 프리미티브 P에서 막혀 있고, P가 실행 가능한 어떤 고루틴(또는 그 고루틴들이 깨울 수 있는 어떤 것)에서도 도달할 수 없다면, 아무도 P를 다시 건드릴 수 없으므로 G는 결코 깨어나지 않습니다. 이는 휴리스틱이 아니라 증명입니다.
같은 설계는 그 한계도 공짜로 제공합니다. 채널이나 뮤텍스가 전역 변수를 통해서, 또는 아직 실행 중인 어떤 고루틴의 지역 변수를 통해 도달 가능하다면, GC는 여전히 그것을 볼 수 있으므로 런타임은 아무 결론도 내릴 수 없습니다. 위의 간단한 예제조차 보고를 위해 runtime.GC() 호출을 두 번 필요로 한다는 점에 주목하십시오. 모든 것을 잡아내지는 못합니다.
그러나 "컨텍스트 취소를 잊어서 워커 고루틴이 영원히 사는" 고전적인 문제는 잡아낼 것입니다. 이는 대부분의 시간에 가장 흔한 누수입니다.
net/http/pprof를 임포트하면 /debug/pprof/goroutineleak에서도 확인할 수 있습니다. 이를 스테이징 환경에 연결하고 매주 점검하면서 조용히 경악하십시오.
이 작업은 Uber의 Vlad Saioc가 기여했으며, 그는 원하는 음료 한 잔을 받을 자격이 있습니다.
2.3. 트레이스백이 이제 어떤 요청이 죽었는지 알려 줌
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: duplicate object member name "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: invalid UTF-8 after offset 1
중복 키와 잘못된 UTF-8은 거부됩니다. 둘 다 단순히 지저분한 수준이 아니라 실제로 위험합니다. 두 파서가 어떤 중복 키가 이기는지에 대해 의견이 다르면 보안 버그가 발생합니다. CouchDB의 CVE-2017-12635가 바로 이런 경우였습니다. roles 키가 두 개 있는 JSON 본문에서, 검증기는 하나를 읽었고 저장 계층은 다른 하나를 읽었습니다. 거부하는 것이 올바른 선택입니다.
옵션은 가변 인자로 전달됩니다.
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: cannot unmarshal JSON string into Go main.Config:
23// unknown object member name "nope"
Deterministic, MatchCaseInsensitiveNames, StringifyNumbers, FormatNilSliceAsNull, OmitZeroStructFields 등 이전에 서드파티 라이브러리나 직접 작성한 MarshalJSON으로 해결했던 대부분의 문제가 이제 플래그로 제공됩니다.
그리고 마이그레이션 방식이 가장 좋은 부분입니다. 나중에 지정된 옵션이 우선하므로, v2의 엄격함을 한 번에 하나의 동작씩 도입할 수 있습니다.
1// v1 의미론은 유지하되, v2처럼 중복 키는 거부
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
중복 키를 더 이상 받아들이지 않기 위해 20만 줄짜리 코드베이스를 v2로 포팅할 필요가 없습니다. 옵션 하나만 바꾸면 됩니다. 규모가 큰 프로젝트에서는 이것이 현실적인 경로입니다.
성능: 마샬링은 대략 동등한 수준이며, 언마샬링은 상당히 빨라졌습니다. 문제가 생기면 GOEXPERIMENT=nojsonv2로 이전 구현을 복원할 수 있습니다. 이 옵트아웃 역시 결국 제거 대상이므로, 그대로 두지 말고 이슈를 등록하십시오.
jsontext는 저수준 계층입니다. Token과 Value 형태로 JSON을 순회하는 Encoder/Decoder가 있으며, 상태 머신이 코드를 정직하게 유지합니다. Go 값을 전혀 구체화하지 않고 스트리밍 변환기나 JSON 필터를 작성할 때 사용하십시오.
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을 사용하십시오. 시간 순서 정렬 덕분에 B-트리 인덱스가 파편화되지 않으며, v4 UUID가 악명 높게 나쁜 부분이 바로 이것입니다.
이제 의존성 하나를 삭제할 수 있습니다. v1/v3/v5가 필요하거나 더 복잡한 파싱 옵션이 필요하다면, 서드파티 라이브러리는 여전히 역할이 있습니다.
3.3. crypto/mldsa: 양자 내성 서명, 그리고 크기가 거대함
Go 1.24는 양자 내성 키 교환을 위한 crypto/mlkem을 제공했습니다. Go 1.27은 나머지 절반인 ML-DSA 서명을 제공하며, 이는 FIPS 204로 표준화되었습니다.
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: invalid signature
이 수치들을 보십시오. 서명 하나가 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는 자료 구조가 아니라 인터페이스라는 것입니다. 이를 소비하는 표준 해시 테이블이나 블룸 필터는 아직 없습니다. 이는 향후 컨테이너 패키지를 위한 기반 작업입니다. 오늘날에는 자체 구조를 만들 때 사용하거나, types.Type을 맵 키로 사용할 수 있게 하고 Identical을 준수하는 go/types.Hasher와 같은 기존 구현을 사용할 수 있습니다.
시드 관리는 여러분의 몫입니다. 위의 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입니다. "스케줄러 지터를 고려하면 최소 3초" 같은 것이 아니라 정확히 3초입니다. 가짜 시계는 지터가 없습니다.
synctest.Sleep은 특정한 이유로 존재합니다. 테스트가 테스트 대상 코드와 같은 시간 동안 잠들면, 어느 쪽이 먼저 깨어날지는 아무도 알 수 없습니다. synctest.Sleep은 잠든 뒤, 버블 안의 다른 모든 고루틴이 확실하게 블록될 때까지 기다리므로, 시스템이 안정된 후의 상태를 관찰할 수 있습니다.
타임아웃, 재시도, 서킷 브레이커, 레이트 리미터 등 시간에 의존하는 모든 HTTP 클라이언트 테스트를 빠르고 결정론적으로 만들 수 있습니다. 여러분의 테스트 스위트가 현재 time.Sleep(100 * time.Millisecond)와 희망으로 간신히 유지되고 있다면, 이것이 탈출구입니다.
3.7. 실제로 프로덕션에 영향을 주는 net/http 변경 사항
조용하지만, 여러분의 메트릭에 나타날 것입니다.
HTTP/1 응답 본문은 Close 시 자동으로 드레인됩니다. 읽지 않은 내용은 (보수적인 한계까지) 본문을 닫을 때 드레인되므로 연결을 재사용할 수 있습니다. 즉, 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**의 읽기 메서드는 이제 io.EOF를 net.OpError로 감싸지 않고 직접 반환합니다. 이제 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)**이 GCC 호환 형식으로compile,link,asm,cgo,cover,pack에서 지원됩니다. 명령줄 길이 제한을 넘어서는 빌드 시스템을 위한 기능입니다. 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를 설치하고 테스트 스위트를 실행해 보십시오. 이 체크리스트의 모든 항목은 장애가 발생한 중에 발견하는 것보다 화요일 오후에 발견하는 것이 훨씬 더 즐겁습니다.
참고 자료