Go 1.22 릴리스
(go.dev)for루프 변수가 반복마다 새로 생성되도록 바뀌어 의도치 않은 공유 버그를 피하는 동작으로 변경, 정수에 대한range도 가능해짐- 호환성은 Go 1 호환성 약속을 유지하며, 거의 모든 Go 프로그램이 이전처럼 컴파일·실행될 것으로 기대됨
- 워크스페이스에서
go work vendor로 생성한 vendor 디렉터리를 사용할 수 있으며, 워크스페이스vendor가 있으면-mod=vendor가 기본값이 됨 GO111MODULE=off인 legacyGOPATH모드에서 모듈 밖go get지원 중단,go build와go test같은 다른 빌드 명령은 legacyGOPATH프로그램에서 계속 동작함- 런타임이 힙 객체 가까이에 타입 기반 GC 메타데이터를 두도록 바뀌어 Go 프로그램의 CPU 성능이 1–3% 개선되고, 다수 프로그램의 메모리 오버헤드가 약 1% 감소함
- 일부 객체 주소 정렬이 기존 16바이트 이상에서 8바이트 정렬로 바뀔 수 있으며, 더 높은 정렬을 가정한 어셈블리 코드는 수정 필요, 임시 우회로
GOEXPERIMENT=noallocheaders사용 가능 - PGO 빌드가 더 많은 호출을 devirtualize할 수 있으며, 대표 Go 프로그램 집합에서 PGO 활성화 시 런타임 성능이 2–14% 개선됨
- 표준 라이브러리에 첫
v2패키지인math/rand/v2추가,Read미승계, 무조건 랜덤 시드되는 전역 생성기,ChaCha8와PCG소스,N제네릭 함수 제공 net/http.ServeMux라우팅 패턴이 메서드와 와일드카드를 지원하며,{}가 포함된 패턴과 이스케이프 경로 처리에서 작은 하위 호환성 깨짐이 있고GODEBUG=httpmuxgo121=1로 이전 동작 복원 가능crypto/tls서버의 기본 최소 버전이 미지정 시 TLS 1.2로 변경되고, pre-TLS 1.3 핸드셰이크에서 ECDHE 없는 cipher suite가 기본 제공되지 않음- 실행 트레이서가 전면 개편되어 대부분 플랫폼에서 OS 시계를 사용하고, 스트리밍 처리가 가능한 분할 trace, 전체 syscall duration, goroutine이 실행된 OS thread 정보 제공
trace도구 웹 UI가 새 tracer 지원 작업의 일부로 갱신되고, Go 1.22 이상으로 빌드된 프로그램의 trace에서 thread-oriented view와 전체 syscall duration 표시 지원vet이 Go 1.22 루프 변수 의미에 맞춰 loop closure 경고를 조정하고, 값 없는append,defer안의 비지연time.Since,log/slogkey/value 불일치 경고 추가- 부트스트랩에 Go 1.20의 최종 point release 이상 필요, Go 1.24는 Go 1.22의 최종 point release 이상을 요구할 것으로 예상됨
darwin/amd64에서 Go toolchain이 기본적으로 PIE를 생성하며, Go 1.22는 macOS 10.15 Catalina에서 실행되는 마지막 릴리스이고 Go 1.23은 macOS 11 Big Sur 이상 필요go/types에Alias타입과Unalias함수가 추가됐으며,gotypesalias=1에서는 클라이언트가Alias타입을 처리해야 함slices.Delete,DeleteFunc,Compact,CompactFunc,Replace가 축소된 구간의 요소를 zero 처리하고,Insert는i가 범위를 벗어나면 항상 panic 발생- Windows 디렉터리 읽기 함수들이 배치로 항목을 읽어 시스템 호출 수를 줄이며, 성능이 최대 30% 개선됨
댓글과 토론
Hacker News 의견들
-
지난 3년은 주로 TypeScript를 쓰다가 최근 Go를 쓰기 시작했는데, 처음엔 배열 함수 부족이나 덜 유연한 타입 시스템 때문에 조금 걱정했음
그런데 4~5명 규모 팀용 TypeScript 프로젝트를 다시 세팅해 보니, 린팅, 서버 라우팅 라이브러리 선택, 서버 선택, 코딩 표준, 기본 오류 처리, 중첩try/catch지옥을 피하려고 커스텀 오류나Result타입 강제, 테스트와 모킹, Prisma 설정 등에 시간이 너무 많이 듦
TypeScript를 좋아하긴 하지만, 훌륭한 표준 라이브러리, 제대로 된 타입 안전성, 내장된 코딩 표준이 있는 언어를 쓰는 게 정말 편함. Go도 quirks는 있지만 꽤 괜찮고, 라우팅까지 표준 라이브러리로 들어온 건 신경 쓸 게 하나 더 줄어든 셈이라 좋음. 이제 슬라이스용map/filter/find함수만 기다리는 중- CTO 뉴스레터에서 최근 TypeScript와 Go 릴리스를 비교했고 Go 1.22를 예로 들었음
TypeScript는 릴리스마다 파워 유저를 위해 점점 복잡해지는 반면, Go는 정수에 대한range처럼 빠져 있던 것을 넣어 쓰기 더 단순하게 만듦
게임 후속작이 설정과 메커니즘을 계속 늘리다가 신규 유저를 위해 리셋이 필요해지는 것과 비슷한데, 프로그래밍 언어는 그렇게 하기 어려우니 Go가 이를 의식하는 게 좋음 - 비슷하게 느낌. Go에 머무르게 만든 건 놀라울 정도로 통일된 빌드 도구 체인이었고,
go:embed와go:generate로 할 수 있는 일은 다른 프로젝트에서 볼 때마다 인상적임
golang.org/x패키지도 거의 모든 인터넷 RFC 관련 구현이 바로 쓸 수 있게 준비되어 있어 좋음 - 슬라이스용
map/filter/find가 오기 전까지는 lodash에서 영감을 받은 lo 라이브러리를 쓸 수 있음
Go 프로젝트에서 애용하는 만능 도구임: https://github.com/samber/lo - 기능이 엄청 많지 않아서 오히려 작은 결정들이 줄어들고, 직관과 반대로 꽤 생산적임
- TypeScript를 쓰는 고객사에서 한동안 일했는데, 나중엔 “Tricks Driven Development”라고 부르게 됨
뭔가 하려고 문서를 읽으면, 곧 누군가 문서에는 없는 쓸 수 있는 트릭을 알려주는 식이 반복됐음
- CTO 뉴스레터에서 최근 TypeScript와 Go 릴리스를 비교했고 Go 1.22를 예로 들었음
-
공식 릴리스 노트가 조금 딱딱하다면, 인터랙티브 버전을 만들어 뒀음: https://antonz.org/go-1-22
for루프 변수 공유 문제가 처음엔 바로 이해되지 않았는데, 직접 실행해서 예상 밖 결과를 보니 도움이 됨Compact와Replace예제가 헷갈리는데, 이게 맞는 건지 모르겠음
-
Go를 9년 넘게 썼고, 최근 4년은 Flutter 때문에 Dart를 많이 썼는데 두 언어는 복잡성에 대한 태도가 정반대라고 봄. Dart는 가능한 모든 기능을 추가하려 하고, Go는 반대임
첫째, Dart에서는 클래스 안에서 값을 초기화하는 방법이 너무 많아 시간을 많이 쓰게 됨.final/const/static/late접두어에 따라 생성자, 팩토리, FlutterStatefulWidget의initState()등 초기화 위치가 달라지고, 리팩터링하면 초기화를 크게 손봐야 함. 함수가 변수처럼 보이는 getter도 새 코드베이스에서는 혼란을 더함
둘째, Dart에는 맵, 스트림 등 한 줄에 몰아넣기 좋은 기능이 많아서, 이해하기 어려운 한 줄짜리 코드를 빠르게 만들고 싶어짐. 하지만 디버깅하거나 주니어 개발자가firstWhereOrNull같은 해킹과orElse: () => null같은 선택 매개변수를 만나면 매우 혼란스러워함. 난해한 null 안전성이나 타입 오류가 멈춰 세울 때도 많고, 이를 단순한 Go식for루프로 바꾸면 정말 안도감이 듦- Dart 팁으로, UI에 직접 관련 없는 일에는
initState를 쓰지 않는 게 좋음. 예를 들어 텍스트 필드 힌트를 설정하는 정도가 아니라면 피하는 편임
대부분의 Flutter 페이지는 할 일이 아주 적거나, 부모가MyPageController객체를 만들고 원하는 방식으로 초기화한 뒤 자식 페이지가 그 컨트롤러에 따라 동작하게 함. 예를 들어 목록 페이지에서 사용자가 항목을 편집하려 할 때 컨트롤러를 만들고 항목을 넣어 자식 페이지로 보내면, 돌아온 뒤 부모가 컨트롤러의 항목이나 변수·콜백을 보고 UI 변경을 결정할 수 있음
이렇게 하면InheritedWidget같은 것을 건드리지 않고도 위젯 간 상호작용을 세밀하게 제어할 수 있음. 물론 상태 관리 라이브러리와 함께 쓰는 게 좋지만, 실제로는 안 쓰는 경우도 많음 - Go의 단순함이 가장 큰 장점이라는 데 동의함.
range도 마찬가지로, 결국 타이핑하는 문자 수는forEach(f)같은 컴프리헨션과 비슷해도 세 줄로 퍼져 있어서 더 길어 보일 뿐이고, 직접 풀어 써 보면 별일 아님
다만 초기화와 관련해서는 Go에 구조체 필드를 특정 값으로 초기화하는 관용적 방법이 있었으면 함. 생성자가 없는 건 괜찮지만, 가끔bool이 기본적으로true로 초기화되길 바랄 때가 있음 initState는StatefulWidget의 오버라이드이고 다른 프레임워크의 생명주기 이벤트와 다르지 않으며, Flutter 쪽 개념이라 생성자와 헷갈릴 성질은 아님
일반 생성자와 팩토리 중 무엇을 써야 할지 헷갈릴 수는 있겠지만,initState때문에 Go가 우월하다고 보기는 어려움- 비싼 전문가들이 서로 간결하게 소통할 수 있는 능력을 최적화해야 함. 초보자 손에서 강력한 문법 도구를 빼앗는 건 그들이 전문성을 키우는 걸 막을 뿐임
- Dart 팁으로, UI에 직접 관련 없는 일에는
-
io.Copy가TCPConn에서UnixConn으로 복사할 때 가능하면 Linux의splice(2)시스템 호출을 쓰고, 새 메서드TCPConn.WriteTo를 사용하게 됨
인터페이스 업그레이드로 또 한 번 투명하게 무복사 I/O가 늘어난 셈임.io패키지에서 이 패턴으로 얻어내는 효과가 정말 큼- Go 표준 라이브러리에는 이런 식의 조정이 정말 많음. 꼭 나쁜 건 아니지만, 이런 특수 처리는 확장 가능하지 않아서 비표준 라이브러리와의 통합은 덜 만족스러워짐
그래도 기대 범위 안에 있긴 함
- Go 표준 라이브러리에는 이런 식의 조정이 정말 많음. 꼭 나쁜 건 아니지만, 이런 특수 처리는 확장 가능하지 않아서 비표준 라이브러리와의 통합은 덜 만족스러워짐
-
향상된 라우팅 패턴은 Gorilla Mux의 미래가 불안정한 지금 좋은 개선임
다만 이 변경은 작은 방식으로 하위 호환성을 깨는데,{와}가 있는 패턴 동작이 달라지는 명확한 부분도 있고 이스케이프된 경로 처리처럼 덜 명확한 부분도 있음.GODEBUG의httpmuxgo121필드로 제어하고,httpmuxgo121=1을 설정하면 이전 동작으로 되돌릴 수 있음
그런데 “Go 1 명세로 작성된 프로그램은 그 명세의 수명 동안 변경 없이 계속 컴파일되고 올바르게 실행된다”는 Go 1 호환성 약속과 충돌하지 않는 이유가 궁금함- Go 1 호환성 약속을 우회하는 방식은
go.mod의go지시문 값에 이런 하위 비호환 변경을 걸어 두는 것임
1.22 이상이면 새 라이브러리 동작을 받고, 아니면 예전 동작을 받음. 실제로 얼마나 잘 작동할지는 지켜봐야 함 - 보통 Go 팀은 뭔가를 깨기 전에 GitHub와 오픈소스 프로그램을 긁어 사람들이 어떻게 쓰는지 확인함. 아마 HTTP 핸들러 경로에서
{와}를 쓰는 경우가 거의 없다고 봤을 것 같음
새 동작을 끌 수 있는 방법도 제공하니 코드 변경을 강제하진 않지만, 새 환경 변수를 설정해야 하긴 함
이번 릴리스의for루프 의미 변경도 비슷한 예로, 사실상 깨지는 변경임. 그래도 모든 Go 프로그램은 계속 컴파일되고 실행되며, 일부 동작이 조금 달라질 뿐임. Go가 실용적인 접근을 택했고, 그게 성공 이유 중 하나였다고 봄 - 이건 Go에서 드문 실수라고 봄.
go.mod를 이용해 버전별로 깨뜨릴 거라면 적어도 더 나은 방식으로 깨뜨려야 함
go.mod를 1.22로 바꿨을 때 미묘한 런타임 이슈를 찾아내는 것보다, 차라리 코드가 컴파일 실패하는 편이 훨씬 낫겠음 - 그러지 않으면 짧고 강한 광고 문구를 잃게 됨. 작든 아니든 호환되지 않는 변경임
- Go 1 호환성 약속을 우회하는 방식은
-
Go 1.22에서는 루프의 각 반복이 새 변수를 만들게 됨
이 내용은 이전에도 여기서 논의됐음: https://news.ycombinator.com/item?id=33160236 - Go: Redefining For Loop Variable Semantics (2022) -
구식일 수도 있지만 함수에 대한
range추가는 마음에 들지 않음. 언어에 추가되는 복잡성을 정당화할 만큼 편의성이 크지 않고, 함수형 스타일이 Go의 명시적이고 명령형이며 장황하지만 기능이 적은 스타일과 어긋나 보임. 그게 Go의 큰 장점 중 하나였다고 봄
같은 이유로 정수에 대한range도 잘못된 방향이라고 생각함. Go의 얇은 기능 집합과 낮은 인지 부하는 늘 주요 강점이자 차별점이었음- 현재 이 기능은 선택적·실험적이며 명시적으로 켜야 함. 표준으로 켜게 된다면, 흔히 쓰이지 않을 때 개발자가 쓰지 않도록 유도하는 끄기 메커니즘이 있었으면 함
소프트웨어 개발에서 큰 문제 중 하나는 개발자가 복잡성을 추가하거나 실력을 과시하려는 걸 막는 일임. 특히 경력 초반 개발자는 문제를 직설적이지만 덜 우아하고 장황하게 풀고 넘어가기보다, 해법을 과도하게 복잡하게 만드는 경향이 있음 - 이 기능은 아직 이른 느낌임. 이상적으로는 람다와 제네릭 매개변수 팩이 도입된 뒤 들어와야 함
현재 Go의 제네릭 지원은 이를 받쳐 줄 만큼 정교하지 않아서 이상하게 강제된 제한이 생김 - 나도 애매하게 봄. 인라인 함수를 넘기는 것과 비교해 코드 줄 수가 줄지도 않음
- 현재 이 기능은 선택적·실험적이며 명시적으로 켜야 함. 표준으로 켜게 된다면, 흔히 쓰이지 않을 때 개발자가 쓰지 않도록 유도하는 끄기 메커니즘이 있었으면 함
-
sql.Null[T]추가는 좋음. 새 프로젝트에서는 아마 쓰기 시작할 듯함
현재 작업물에서는 API가 아주 비슷한 sqlboiler의 null [0]에 의존하고 있음.sql.Null처럼 동작하지만, 값이 명시적으로 설정된 적 있는지 알려주는IsSet() bool메서드가 추가로 있어 “의도적 null”과 “초기화되지 않아서 null”을 구분하는 데 도움을 줌. 좋아 보이지만 실제로는 한 번도 써 본 적 없음- 언제 null이 의도적인지 알아야 하는지 궁금함. 데이터베이스에는 그에 대응하는 타입이 없지 않나
-
표준 라이브러리 mux를 다시 살펴보고 chi 제거도 가능할지 기대 중임. 이런 기능이 표준 라이브러리로 들어오는 게 정말 좋음
- 똑같은 말을 하러 왔음. chi는 몇몇 프로젝트에서 정말 훌륭했고, 사실 존재를 잊을 정도로 자연스러웠음
이런 기능이 표준 라이브러리로 들어가면 항상 유지보수되고, 많은 Go 프로그램에서 같은 접근이 쓰이도록 보장됨
- 똑같은 말을 하러 왔음. chi는 몇몇 프로젝트에서 정말 훌륭했고, 사실 존재를 잊을 정도로 자연스러웠음
-
Go를 프로덕션에서 쓰는 경우 최신 버전으로 빨리 갈아탈 수 있는지, 아니면 오래된 릴리스에 묶이는지 궁금함
공개된 프로젝트를 보면 비교적 새로운 기능을 피하는 경우가 많아 보임. 제네릭을 쓰지 않더라도 예전의interface{}대신 Go 1.18에서 나온any를 쓰고 싶은데, 후자가 더 “관용적”이라는 말도 들어 봄- Square에서는 보통 Go 모노레포의 기본 버전을 올리기 전에 첫 번째 포인트 릴리스를 기다림. 개별 애플리케이션 소유자는 더 빨리 올려도 됨
이번에는 약간 꼬여서, 우리가 기본값을 1.21로 올리기 전에 Go 팀이 1.22를 먼저 냈음. 이제 기다리는 건 거의 관례에 가깝고, Go 릴리스가 뭔가를 깨는 일은 드묾 - 새 기능은 잘 맞는 문제가 보일 때까지 실험을 기다리는 편이지만, 컴파일러 버전은 릴리스 직후 거의 바로 올림
예를 들어 제네릭은 실제로 쓰기까지 6개월 정도 기다리며 유용성, 개발자 경험, 빌드·테스트 속도 영향이 크지 않은지 확인했음 - 직장에서는 운영 중인 컨테이너, 즉 SaaS/API 서비스는 개발자들이 최신 빌드로 자유롭게 올릴 수 있음
하지만 Go로 최종 사용자 앱도 제공하는데, 이쪽은 오래된 클라이언트 OS와의 호환성 때문에 1.20에 머물러야 함. 1.21에서 상당한 컷이 있었기 때문에 몇 년은 1.20에 머물 가능성이 큼 - CI가 Go의
latest컨테이너로 빌드와 테스트를 하고, 테스트 스위트가 꽤 탄탄해서 사실상 자동 업데이트됨. 뭔가 안 맞으면 파이프라인이 깨지고, 그때 마지막으로 동작한 버전에 고정할지 아니면 깨지는 변경을 고칠 때까지 업데이트를 미룰지 결정함
몇 년에 한 번 정도는 새 버전과 서드파티 패키지 사이에서 미묘한 엣지 케이스 버그가 드러나기도 하지만, 그런 버그는 정의상 테스트 스위트를 피한 것이므로 수동 업그레이드였어도 잡지 못했을 가능성이 큼 - 바로 갈아탐
오래된 Go 컴파일러 지원이 걱정된다면, 예전 버전용 빌드 조건을 두고 빠진 것을 정의할 수 있음. 예를 들면// +build !go1.7아래에type any = interface{}를 둘 수 있음
- Square에서는 보통 Go 모노레포의 기본 버전을 올리기 전에 첫 번째 포인트 릴리스를 기다림. 개별 애플리케이션 소유자는 더 빨리 올려도 됨